A Java program is made of types, methods, statements, and expressions. The compiler checks names, types, scopes, and control flow before the JVM executes the resulting bytecode.
Intermediate expression types, short-circuit rules, and loop bounds are semantics, not style. Code that compiles may still mishandle overflow, null, or boundary inputs.
Keep variable scopes narrow, reject invalid input with guard clauses, make branches complete, and verify real execution paths with boundary values and side effects.
What it is and why it exists
Java language fundamentals are the minimum rules for organizing an executable program: how names bind to declarations, how expressions calculate values, and how statements select what runs next. Java is statically typed, so the compiler exposes most name and type errors before execution. After compilation, the same rules determine runtime evaluation order, branch selection, and loop termination.
A source file usually declares one or more classes, interfaces, enums, or records. Methods put operations behind named boundaries, statements change local state or control execution, and expressions produce values. Beginner programs often enter through public static void main(String[] args), but the same fundamentals apply in web controllers, test methods, batch jobs, and library code.
A variable is not an untyped container. Every variable has a compile-time type, scope, and lifetime; a local variable must also be definitely assigned before it is read. Expressions combine variables, literals, method calls, and operators. Statements decide whether to use the results, repeat the calculation, or leave the current control structure early.
These rules exist so that the compiler and the reader agree about what a program means. Indentation helps people read code, but it does not define a Java block; braces do. Likewise, a method name can suggest intent, but it cannot change the short-circuit semantics of && or the rules of integer division.
This topic concentrates on program structure, variables, expressions, and control flow. The complete rules for primitive and reference types and numeric conversions belong to java/data-types; arrays, collections, strings, exceptions, and object-oriented design have their own topics. They appear here only where they are needed to explain control flow, without repeating their API catalogs.
How it works
From source file to entry method
javac reads source files, performs lexical, syntax, name, and type checks, and produces class files. java starts the JVM, loads the entry class, and invokes the main method that matches the launch protocol. Class-file names and runtime class names are case-sensitive, and the name of a public top-level type must match its source file.
In main, public lets the launcher access the method, static means no class instance is needed for the call, and void means the method returns no result to its caller. String[] args receives command-line arguments. The launcher may supply an array of length zero, but it does not pass null.
A method body is a block, and a block can contain local-variable declarations and statements. Each pair of braces creates a nested boundary, but it does not necessarily create an object or thread. A local name declared inside the block is no longer visible after execution leaves that block.
Method calls and pass-by-value
Java always passes arguments by value. For a primitive type, the parameter receives a copy of the primitive value; for a reference type, it receives a copy of the reference value. Reassigning a parameter inside the called method cannot redirect the caller’s variable to another object.
The copied reference still identifies the same object, so a method can use it to change mutable object state. Whether the caller observes that change follows from the shared object’s contract, not from Java suddenly passing by reference. Saying “a reference is passed by value” explains both why reassignment is ineffective and why object mutation is visible.
Arguments are evaluated from left to right before their results are passed to the matched method. The overload target depends on compile-time types and available conversions; the overridden implementation of an instance method also depends on the receiver’s runtime type. The complete overload and override rules belong to java/oop, but a fundamentals review should distinguish these two stages.
Every normally completing path in a non-void method must return a compatible value. return can end a method early, and a void method can use a return with no value. When an exit represents failure rather than a normal result, use an explicit exception or result model instead of an undocumented magic value.
Choose statement forms from semantics
The syntax should match the control intent. This table is not a style ranking; it is a short mapping from a requirement to a language construct.
| Requirement | Usual construct | Key check |
|---|---|---|
| Perform steps in order | Expression statements and calls | Side-effect and exception order |
| Select between two actions | if / else | Whether the condition really is boolean |
| Produce a value from finite categories | switch expression | Completeness and a result on every path |
| Repeat while a condition holds | while or ordinary for | Initial state, exit condition, and update |
| Visit every element | Enhanced for | Whether an index or structural change is needed |
More than one syntax can express the same result. Prefer the form that makes valid inputs, exit points, and the result type visible; nesting conditional operators merely to save lines usually hides that information.
Declarations, scope, and definite assignment
A declaration associates a name with a type and an optional initializer. A local variable’s scope starts at its declaration and continues to the end of its block; an inner block cannot redeclare a local name that is still in scope. Fields follow different member rules, so do not infer the local-variable rule from field shadowing or vice versa.
The Java compiler performs definite assignment analysis for local variables. A read is legal only when the compiler can prove that every path reaching it has assigned the variable. This is a compile-time control-flow judgment; the runtime does not silently fill an uninitialized local with zero or null.
A final local can be assigned only once, but an object it references does not become immutable. final List<String> names prevents names from being redirected to another list, yet it does not prevent names.add(...). If the program requires immutability, the type and construction mechanism must provide that contract too.
var is available only in supported local-variable positions with an initializer. It asks the compiler to infer one static type; it does not make Java dynamically typed, and var result; cannot defer inference until later. Spell out the type when the initializer leaves the intended abstraction unclear.
Expressions and evaluation order
Java evaluates operator operands from left to right, and it evaluates method arguments from left to right too. That guarantee matters when an expression contains method calls, increments, or exceptions. Even so, splitting state-changing steps into named statements usually costs less to review than packing several side effects into one expression.
Arithmetic depends on the operand types. Dividing two int values performs integer division, and multiplying two int values first produces an int, even when the result is later assigned to long. See java/data-types for the full promotion, narrowing, and overflow rules; fundamentals code should at least inspect intermediate expressions, not only the variable receiving the result.
&& and || implement short-circuit evaluation . left && right evaluates the right side only when the left side is true; left || right evaluates it only when the left side is false. Single & and | also accept Boolean operands, but both sides are evaluated, so they cannot replace short-circuit operators that guard against null or division by zero.
Assignment is an expression too, but embedding it in a condition is usually easy to misread. Equality comparison uses ==; assignment uses =. With references, == asks whether the references are identical, while business-value equality usually calls the equals method defined by the type.
Conditional branches
if selects a path from a boolean expression. Java does not implicitly treat numbers, empty strings, or objects as truthy or falsy, so the condition must directly produce boolean. Braces are worth retaining around even one statement: when logging or validation is added later, the new statement cannot accidentally land outside the branch.
An else belongs to the nearest syntactically available if. Clear braces and shallow nesting make that ownership visible. Invalid input is often best handled with a guard clause that returns early and leaves the main path at a shallower indentation level.
Modern switch can be either a statement or an expression that produces a value. Arrow rules do not fall through to the next branch, and several labels can be written as case 1, 2 ->. Every normally completing expression path must produce a compatible value; an expression rule supplies one directly, while a block rule uses yield.
A switch expression must cover every possible selector value. An open-ended selector type usually needs default; for an enum or sealed hierarchy, the compiler may prove completeness from the known constants or permitted subtypes. Completeness is a compiler guarantee, but future binary evolution still needs tests.
Boundaries of Boolean conditions
A condition should express a domain decision directly, such as hasStock && paymentAccepted. Hiding assignment, increment, or a remote call inside it tangles the Boolean result with side effects. When diagnostics matter, compute named results first and then combine the final condition.
Java does not support mathematical chained comparisons. Write a range check as 0 <= index && index < length, not 0 <= index < length. Short-circuit ordering should put a safe, cheap guard before the access that depends on it.
When negating a complex condition, apply Boolean algebra and keep the grouping explicit. Instead of making a reader expand !(active && authorized), use a named predicate when the domain supports one. Test both boundaries after the rewrite, because improved readability does not itself prove logical equivalence.
Loops and early exits
for fits loops whose initialization, condition, and update can be expressed together. Enhanced for fits sequential reads from an array or Iterable; an ordinary for is usually clearer when the code needs an index, reverse order, or in-place replacement. while tests before each iteration, while do-while guarantees at least one execution of its body.
Whether a loop terminates depends on state moving toward its exit condition. Review the initial value, condition, update, and integer boundary together. For array indices, the common range is 0 <= index && index < array.length; the upper bound cannot be <= array.length.
continue moves to the next iteration, break leaves the current loop or switch, and return ends the current method. A labeled break or continue can target an outer statement, but it often signals that nested logic should become a method. Exceptions also complete control flow abruptly; see java/exceptions for propagation rules.
Scope is not object lifetime
A local name’s scope is its compile-time visibility, while an object’s lifetime follows runtime reachability. A local name disappears after a method returns, but its object can remain alive if it was returned, stored in a field, or passed to another long-lived object.
Conversely, a local that remains in scope does not promise that an object is retained until the block ends; an implementation may reclaim an object once doing so cannot change program semantics. Business code must not depend on a collection time, and resources should use explicit mechanisms such as try-with-resources.
The main benefit of a narrow scope is less mutable state and fewer available names, not a direct memory-performance promise. Declaring a variable when it first becomes necessary helps a reader identify which statements can affect it and exposes state accidentally reused between loop iterations.
Examples
These four programs progress from sequential execution to branching, looping, and short-circuit evaluation. Each source file compiles and runs independently, and the output shown is from an actual local JDK run.
Variables, expressions, and output
The first program declares order data, computes a subtotal, and selects a delivery label with the conditional operator. Variables stay in the method that uses them, and their names expose the unit.
public class OrderSummary {
public static void main(String[] args) {
String customer = "Amina";
int unitPriceCents = 240;
int quantity = 3;
int subtotalCents = Math.multiplyExact(unitPriceCents, quantity);
boolean freeShipping = subtotalCents >= 500;
String delivery = freeShipping ? "standard-free" : "standard-paid";
System.out.printf("customer=%s subtotal=%d%n", customer, subtotalCents);
System.out.println("delivery=" + delivery);
}
}customer=Amina subtotal=720
delivery=standard-freeMath.multiplyExact states that an order subtotal may not overflow silently. The conditional operator fits this choice between two values; if each branch also changed several pieces of state, a regular if would be clearer.
Guard clauses and a switch expression
The second program rejects a negative count, handles an empty order, and then classifies the item count. Early returns move invalid and special cases out of the main decision path.
public class ShippingDecision {
static String classify(int itemCount, boolean expedited) {
if (itemCount < 0) {
return "invalid";
}
if (itemCount == 0) {
return "empty";
}
String band = switch (itemCount) {
case 1, 2 -> "small";
case 3, 4, 5 -> "medium";
default -> "large";
};
return expedited && itemCount <= 5
? band + "-express"
: band + "-standard";
}
public static void main(String[] args) {
for (int itemCount : new int[] {-1, 0, 2, 7}) {
System.out.println(itemCount + " -> " + classify(itemCount, true));
}
}
}-1 -> invalid
0 -> empty
2 -> small-express
7 -> large-standardThe arrow form cannot fall through from small into medium. default covers every integer above 5, while the guard clauses already handle negative numbers and zero, so each path has a visible meaning.
Skipping and stopping in a loop
The third program scans inventory changes. A zero change carries no information and is skipped with continue; after stock drops below the threshold, later data is irrelevant to this alert, so break stops the scan.
public class StockScan {
public static void main(String[] args) {
int[] changes = {3, 0, -2, 5, -8, 4};
int balance = 10;
for (int change : changes) {
if (change == 0) {
continue;
}
balance = Math.addExact(balance, change);
System.out.println("change=" + change + " balance=" + balance);
if (balance < 9) {
System.out.println("reorder");
break;
}
}
}
}change=3 balance=13
change=-2 balance=11
change=5 balance=16
change=-8 balance=8
reorderEnhanced for hides the index because this decision needs only each element’s value. Once break runs, the final 4 is not processed; a test expectation must include that fact.
A visible short-circuit path
The final program records the name of every Boolean check. After the second one fails, && does not execute the third; the same rule protects the later division.
import java.util.ArrayList;
import java.util.List;
public class EvaluationOrder {
static boolean check(List<String> events, String name, boolean result) {
events.add(name);
return result;
}
public static void main(String[] args) {
List<String> events = new ArrayList<>();
boolean accepted = check(events, "stock", true)
&& check(events, "credit", false)
&& check(events, "fraud", true);
int denominator = 0;
boolean safe = denominator != 0 && 100 / denominator > 2;
System.out.println("events=" + events);
System.out.println("accepted=" + accepted + " safe=" + safe);
}
}events=[stock, credit]
accepted=false safe=falseThe list preserves the actual evaluation order: fraud never appears. Replacing && with & in the second expression would still execute the right side and throw ArithmeticException, so the two operators are not stylistic alternatives.
Pitfalls
Fix: Give the variable a meaningful initial value at its declaration, or ensure every branch assigns it. Do not insert a fake zero merely to satisfy the compiler; if “not calculated yet” is valid, model that state explicitly.
Fix: Use equals according to the type contract, or Objects.equals when null is possible. Use reference == only when the actual question is whether both references identify the same object.
Fix: Promote an operand before the operation, or reject overflow with exact methods such as Math.addExact and multiplyExact. See java/data-types for the complete basis for choosing numeric boundaries.
Fix: Use the half-open range 0 <= index && index < values.length, and keep the update in the loop header. Switch to enhanced for when only element values are needed, and test empty, one-element, and termination boundaries.
Fix: Prefer arrow rules in new code and use a switch expression when a branch should produce a value. In a traditional switch, document and test intentional fall-through; otherwise end each statement group explicitly with break.
Definite assignment and reachability
Definite-assignment analysis tracks a variable’s state at each program point, not the value observed in one test run. When both branches of an if assign a local, a later read can be legal. When only one branch assigns it, the compiler rejects the read unless the other path returns or throws before reaching that point.
The analysis is deliberately conservative. The compiler proves state from language-defined expression and statement structure; it does not execute arbitrary business methods to guess that they always return a particular result. Hiding a crucial condition behind an ordinary Boolean method may convince a person that the path is safe without changing the local variable’s definite-assignment state.
A blank final local tightens the rule: it must be definitely assigned before a read and definitely unassigned before an assignment. Two mutually exclusive branches may each assign it once because any actual path performs one assignment. Assigning the same blank final local inside a loop usually cannot be proven to happen only once.
Reachability analysis answers a different question: whether a statement can possibly execute. An ordinary statement immediately after an unconditional return is unreachable and causes a compile error. This check catches some dead code, but it does not prove that business conditions are meaningful or replace coverage and boundary tests.
Pattern variables use control-flow scope too. On the right of value instanceof String text && !text.isBlank(), text is available because that side runs only after a successful match. Replacing && casually with & changes evaluation behavior and can also destroy the flow structure the compiler uses to establish that a pattern variable is available.
Expression semantics beneath the syntax
Left-to-right evaluation means an earlier operand’s side effect or exception occurs before a later operand. A method call evaluates the target reference first, then its arguments in order, and only then enters the method body. If any step completes abruptly, later operands and the method body do not run.
Short-circuit operators add conditional skipping to that general order. a != null && a.isReady() reads a and completes the null comparison first, then calls the method only when the result is true. Swapping the sides or using & changes the contract, not merely the formatting.
Compound assignment is more specific than it looks. target += value evaluates the left side once, performs the operation, and includes an implicit conversion back to the left-side type. It is not always mechanically interchangeable with target = target + value: the latter may require an explicit conversion, and a complex left side could be evaluated twice.
The conditional operator condition ? left : right evaluates only one selected result expression. It is suited to selecting a value, not hiding several state changes. Its result type also depends on both branches and the target context, so apparently similar branches can still trigger boxing or numeric conversion.
Increment and decrement expressions both produce a value and change a variable. Postfix index++ produces the value before the change; prefix ++index produces the changed value. The distinction rarely affects the next iteration when used alone as a loop update, but its timing directly affects results when embedded in an array access or method argument.
Loop invariants and boundaries
A loop invariant is a condition that should hold at the start or end of every iteration. When scanning values[0..index), for example, the half-open region can mean “already processed,” while index..length remains unprocessed. Putting that condition in a test or comment gives a reviewer more than the vague statement “iterate the array.”
Termination also needs a measure that moves monotonically toward a boundary. An index loop commonly decreases length - index each time; a retry loop needs a maximum attempt count, deadline, or external cancellation. “It breaks on success” does not prove that the failure path terminates.
The half-open interval [start, end) makes its length naturally equal to end - start, and start == end represents an empty interval. It matches the legal upper bound of an array and removes special arithmetic around the last element. A reverse loop needs its bounds derived again rather than a mechanical reversal of increment signs.
| Input shape | Behavior to prove |
|---|---|
| Empty input | The body runs zero times and the result is defined |
| One element | The sole element is processed exactly once |
| Exactly at the threshold | The comparison matches the inclusive or exclusive contract |
| Early match | No extra side effect occurs after break |
| No match | The loop still terminates and returns the specified result |
The loop variable in enhanced for receives the current element value. For a primitive array, reassigning that variable does not update the array element; for reference elements, mutating an object through the reference may still be visible. Use an explicit index when replacing array slots so the write location is clear.
The guaranteed first execution of do-while fits a contract that must show a menu or attempt an operation once before checking. When zero executions are valid, while is usually more direct. Answering how many times the body must run at minimum before choosing the loop form avoids duplicated initialization written merely to fit the syntax.
switch completeness and abrupt completion
A traditional switch statement permits fall-through between colon-labeled statement groups; that behavior is part of the original syntax. Arrow rules do not fall through, and their right side can be an expression, a block, or a throw statement. The forms serve different compatibility needs; choosing one consistently makes new code easier to review.
A switch expression is intended to produce a value, so it must be complete. A block rule cannot carry its result with an ordinary break; it uses yield. A rule that throws completes abruptly and does not need to provide a value on that path.
When every current enum constant is listed, the compiler can accept an expression without a source-level default. Recompiling after a new enum constant is added then exposes the missing branch. Running a newer enum class with older caller bytecode is still a binary-evolution scenario, however, so deployment compatibility tests remain necessary.
break, continue, return, and throw all make a statement or expression complete abruptly, but they have different targets. break leaves a loop or its target statement, continue advances a loop, return leaves a method, and throw searches the call stack for a handler. Naming the target precisely is more useful in nested control-flow review than calling all four operations “jumps.”
The maintenance cost of complex branching usually comes from path combinations, not the number of keywords. Rejecting invalid states with guard clauses before a main switch handles valid domain values reduces those combinations. If labeled jumps or shared mutable flags are still needed across several levels, extracting a named method often makes inputs, results, and early exits testable.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug