Locals, var and final

Fields get a default and locals do not — that is a compile error, and a good one. Plus what `final` actually freezes.

5 min read Java Fundamentals

A local variable is one declared inside a method. It lives on the stack, it dies when the method returns, and it behaves differently from a field in one way that catches everybody exactly once.

Fields get a default. Locals do not.

java
static int field;                 // never assigned
static String name;               // never assigned
 
public static void main(String[] a) {
    int local;
    System.out.println(local);    // ?
}

The fields are fine:

plaintext
an unassigned FIELD int    = 0
an unassigned FIELD String = null

The local is not — and the failure is at compile time, not run time:

plaintext
Unassigned.java:4: error: variable local might not have been initialized
        System.out.println(local);
                           ^

This is definite assignment, and it is one of the better decisions in the language. An object's fields must have some value the moment the object exists, so Java picks zero, false or null. A local has no such moment — so instead of inventing a value, the compiler refuses to let you read one you never wrote.

The consequence worth internalising: a local variable never silently holds a surprising zero. Whole categories of bug that exist in other languages do not exist here, and it costs you nothing but declaring things when you have a value for them.

Declare it where you use it

The style that survives is: declare a variable in the smallest block that needs it, as late as possible, and assign it immediately.

java
// what old code looks like: everything declared at the top
int total, count, i;
String name;
// ...forty lines...
 
// what reads well
int total = 0;
for (Order order : orders) {
    int amount = order.amount();      // exists only inside the loop
    total += amount;
}

The reason is not tidiness. A variable declared forty lines before its use is a variable that can be reused for something else in between, and that is how a loop counter ends up holding a customer id. Narrow scope makes the mistake impossible rather than unlikely, which is the same argument the blocks section made from the compiler's side.

It is also why for (int i = ...) declares i in the header: outside the loop, i does not exist and cannot be read by accident.

var says less, not nothing

Since Java 10 a local may have its type inferred:

java
var orders = new ArrayList<Order>();        // ArrayList<Order>
var total = 0;                              // int
var name = customer.getName();              // String? something else? read on

The variable is still statically typedvar is not a dynamic type, and the type is fixed at the declaration. It only removes the need to write it down.

The useful rule is about the reader, not the compiler: use var when the type is obvious from the right-hand side. new ArrayList<Order>() says it. customer.getName() does not — the reader now has to go and look. Locals only; var is not allowed on fields, parameters or return types, which keeps it out of the places where a written type is part of the contract.

final, and what a constant really is

final on a local means the variable may be assigned once:

java
final int limit = 10;
limit = 20;                     // does not compile

That is all it means, and the distinction the next example draws is one of the most commonly misunderstood things in Java:

java
final StringBuilder sb = new StringBuilder("hello");
sb.append(" world");            // allowed
sb = new StringBuilder();       // does not compile
plaintext
final reference, mutated   = hello world

final freezes the reference, not the object. The variable will always point at that same StringBuilder; the StringBuilder itself can change all day. For immutability you need a type that does not let you change it — String, List.copyOf(...), a record of immutable components — and final is not a substitute.

A constant, in the sense people usually mean, is the combination:

java
private static final int MAX_RETRIES = 3;
  • static — one per class, not one per instance.
  • final — assigned once.
  • SCREAMING_SNAKE_CASE — the convention that says constant to a reader.
  • A primitive or a genuinely immutable type — because private static final List<String> NAMES = new ArrayList<>(); is a shared mutable list wearing the word final, and anyone can add to it.

final on parameters and the compiler's view

Marking a parameter final prevents reassigning it inside the method. It is a matter of taste — some teams require it, most do not — and the honest summary is that it prevents a mistake nobody makes often.

What is not a matter of taste: a final on a primitive with a compile-time constant value is inlined by the compiler. static final int MAX = 3 is baked into every class that uses it, which means changing it requires recompiling those classes and not just this one — a real source of confusion when one jar is rebuilt and another is not.

And one practical use: a local captured by a lambda or an anonymous class must be final or effectively final — assigned once, whether or not the word is written. That rule is why a loop counter cannot be captured directly, and why the fix is to copy it into a new local inside the loop.

Progress is saved on this device and to your account when signed in.