Skip to content

How to Use var in Java Without Hurting Readability

Learn how to use var in Java 10+ on a parcel tracker: where it helps, the diamond and ternary traps, 12 compiler errors forced on purpose, and 11 checks.

how-to-tutorials core-java modern-java
Gopi Gorantala
Reading Progress

On This Page

Before and after: the same report, two ways

Here's the report loop from the parcel tracker you'll build, written the old way and then with var. Both produce the same output, byte for byte, and the compiled code for the two is nearly identical:

// before
for (Map.Entry<String, List<TrackingEvent>> entry : service.eventsByParcel().entrySet()) {
    List<TrackingEvent> history = entry.getValue();
    TrackingEvent last = history.get(history.size() - 1);
    ...
}

// after
for (var entry : service.eventsByParcel().entrySet()) {
    var history = entry.getValue();
    var last = history.get(history.size() - 1);
    ...
}

And what the finished app prints:

P-1001 -> IN_TRANSIT @ Derby
P-1002 -> PICKED_UP @ York
total grams: 2000
true

That last true is the old and new report methods agreeing with each other.

If you're wondering how to use var in Java, the short answer is that it's a way to let the compiler fill in the type of a local variable from its initializer. The longer answer is a list of places where it quietly infers something you didn't mean. I'll walk through both, and every trap below was run, not guessed. This is for you if you know Java 8 and want to know when var is safe to reach for.

Check your setup

Tool Version I used Check it with
JDK OpenJDK 21.0.12.1 (Ubuntu build) java -version
javac 21.0.12.1 javac -version

Anything from Java 10 on has var. Lambda parameters need Java 11. I used no Maven, no Gradle and no test library; the checks are plain Java, so you can paste everything into a folder and compile it with the JDK alone. I'd normally reach for JUnit 5, and the next article in the series covers it. Here, I couldn't reach Maven Central from my build environment, so I kept it dependency-free.

I ran everything on Linux. On Windows, the commands are the same except for the path separators in the javac line.

Create the parcel tracker project

You need a folder and one package. That's all.

parcels/
└── src/com/example/parcels/
    ├── Parcel.java
    ├── TrackingEvent.java
    ├── ParcelService.java
    ├── Before.java
    ├── After.java
    ├── TreeSortedReport.java
    ├── Traps.java
    ├── Main.java
    └── Checks.java

From the command line. mkdir -p parcels/src/com/example/parcels && cd parcels.

In IntelliJ IDEA or Eclipse. Create a plain Java project (no build tool), set the project language level to 21, and make src the source folder. I didn't capture exact menu paths for either IDE in this run, so use your IDE's help for "new Java project" rather than trusting a path from me. Both show the inferred type of a var when you hover over the variable, and that's worth turning to constantly while you read the traps below.

Put var to work, one step at a time

Step 1: Write the domain records

Goal: two small records the rest of the code uses.

src/com/example/parcels/Parcel.java

package com.example.parcels;

import java.time.Instant;

public record Parcel(String id, String carrier, int weightGrams, Instant shippedAt) {
}

src/com/example/parcels/TrackingEvent.java

package com.example.parcels;

import java.time.Instant;

public record TrackingEvent(String parcelId, String status, String city, Instant at) {
}

Why records: they keep the example short, and the accessor names (id(), status()) are what var will infer types from later. Records are covered in how to use Java records.

Step 2: Write the service

Goal: a tiny in-memory store with something to loop over.

src/com/example/parcels/ParcelService.java

package com.example.parcels;

import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ParcelService {

    private final List<Parcel> parcels = new ArrayList<>();
    private final Map<String, List<TrackingEvent>> events = new HashMap<>();

    public void register(Parcel parcel) {
        parcels.add(parcel);
        events.put(parcel.id(), new ArrayList<>());
    }

    public void record(String parcelId, String status, String city, Instant at) {
        events.get(parcelId).add(new TrackingEvent(parcelId, status, city, at));
    }

    public List<Parcel> allParcels() {
        return List.copyOf(parcels);
    }

    public Map<String, List<TrackingEvent>> eventsByParcel() {
        return events;
    }

    public int totalWeight() {
        int total = 0;
        for (Parcel p : parcels) {
            total += p.weightGrams();
        }
        return total;
    }
}

Look at the field declarations. private final List<Parcel> parcels keeps its explicit type because fields can't use var (you'll see that error shortly). The new ArrayList<>() on the right is allowed to infer its type argument because the left side says what it is.

Checkpoint: javac -d out src/com/example/parcels/Parcel.java src/com/example/parcels/TrackingEvent.java src/com/example/parcels/ParcelService.java should print nothing. No output means it compiled.

Step 3: Write the report both ways

Goal: the same method, before and after.

src/com/example/parcels/Before.java

package com.example.parcels;

import java.util.List;
import java.util.Map;

/** The same report, written the pre-Java-10 way. */
public class Before {
    public static String report(ParcelService service) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, List<TrackingEvent>> entry : service.eventsByParcel().entrySet()) {
            List<TrackingEvent> history = entry.getValue();
            TrackingEvent last = history.get(history.size() - 1);
            sb.append(entry.getKey()).append(" -> ").append(last.status()).append('\n');
        }
        return sb.toString();
    }
}

src/com/example/parcels/After.java

package com.example.parcels;

public class After {
    public static String report(ParcelService service) {
        var sb = new StringBuilder();
        for (var entry : service.eventsByParcel().entrySet()) {
            var history = entry.getValue();
            var last = history.get(history.size() - 1);
            sb.append(entry.getKey()).append(" -> ").append(last.status()).append('\n');
        }
        return sb.toString();
    }
}

Notice that After has no imports. Map.Entry and List never get named, so there's nothing to import. That's the real payoff: the long generic type (Map.Entry<String, List<TrackingEvent>>) was repeating something the right-hand side already says.

I use var in exactly these three places: new SomeType(...) on the right, a long generic type that the call already shows, and the loop variable in a for over a typed collection. Everywhere else I ask whether a reader can see the type without hovering.

src/com/example/parcels/TreeSortedReport.java

package com.example.parcels;

import java.util.TreeMap;

public class TreeSortedReport {
    public String render(ParcelService service) {
        var sorted = new TreeMap<>(service.eventsByParcel());
        var sb = new StringBuilder();
        sorted.forEach((id, history) -> {
            var last = history.get(history.size() - 1);
            sb.append(id).append(" -> ").append(last.status()).append(" @ ").append(last.city()).append('\n');
        });
        return sb.toString();
    }
}

Here new TreeMap<>(otherMap) infers TreeMap<String, List<TrackingEvent>> from its constructor argument. That's the diamond working as it should, because the argument carries the type information. Hold that thought for Step 4.

Step 4: Write the traps class

Goal: one method per surprise, so you can run each and look at what var did.

src/com/example/parcels/Traps.java

package com.example.parcels;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;

public class Traps {

    public static String diamondTrap() {
        var list = new ArrayList<>();      // ArrayList<Object>, not ArrayList<String>
        list.add("P-1");
        list.add(42);                      // compiles: the element type is Object
        return list.toString();
    }

    public static String diamondFixed() {
        var list = new ArrayList<String>();
        list.add("P-1");
        // list.add(42);                   // would not compile
        return list.toString();
    }

    public static String concreteType() {
        var ids = new ArrayList<String>(); // type is ArrayList<String>, not List<String>
        ids.ensureCapacity(100);           // ArrayList-only method compiles
        List<String> asList = ids;         // what you wrote before var
        return ids.getClass().getSimpleName() + "/" + asList.size();
    }

    public static String literalTypes() {
        var a = 1;       // int
        var b = 1L;      // long
        var c = 1.0;     // double
        var d = 1.0f;    // float
        var e = 'x';     // char
        var f = 100_000 * 100_000; // int overflow, still int
        return Integer.valueOf(a).getClass().getSimpleName() + ","
                + Long.valueOf(b).getClass().getSimpleName() + ","
                + Double.valueOf(c).getClass().getSimpleName() + ","
                + Float.valueOf(d).getClass().getSimpleName() + ","
                + Character.valueOf(e).getClass().getSimpleName() + ","
                + f;
    }

    public static String ternaryTrap(boolean flag) {
        var x = flag ? 1 : "one";   // an intersection type of Serializable and Comparable
        return x.getClass().getSimpleName();
    }

    public static int anonymousClass() {
        var counter = new Object() {
            int hits = 0;
            void hit() { hits++; }
        };
        counter.hit();
        counter.hit();
        return counter.hits;            // only possible with var
    }

    public static int lambdaParams() {
        BiFunction<Integer, Integer, Integer> add = (var x, var y) -> x + y;
        return add.apply(2, 3);
    }

    public static String varAsName() {
        var var = "legal";            // var is not a keyword; this compiles
        return var;
    }
}

I want to spend a moment on three of these.

The diamond trap. var list = new ArrayList<>() is the single most common var mistake I see in code review. With a typed declaration the diamond picks up String from the left. With var there's no left, so the compiler falls back to Object. It compiles, list.add(42) works, and you've lost the type check that made generics worth having. The fix is to put the type argument on the right: new ArrayList<String>().

You get the concrete type, not the interface. var ids = new ArrayList<String>() is an ArrayList, not a List. That's why ensureCapacity compiles. Sometimes that's fine. If you wanted to program to the interface, write List<String> ids = new ArrayList<>() and keep the explicit type. I would.

Anonymous classes. This is the one thing var can do that an explicit type can't: counter.hits is reachable because the variable's type is the anonymous class itself. You can't name that type, so before Java 10 you couldn't hold onto it. I've needed this about twice in real code. Don't go looking for excuses to use it.

Checkpoint: compile what you have so far (Main.java and Checks.java come next, so list the files by name):

mkdir -p out
javac --release 21 -d out src/com/example/parcels/{Parcel,TrackingEvent,ParcelService,Before,After,TreeSortedReport,Traps}.java

It printed nothing for me.

Step 5: Write Main

src/com/example/parcels/Main.java

package com.example.parcels;

import java.time.Instant;

public class Main {
    public static void main(String[] args) {
        var service = new ParcelService();
        var t0 = Instant.parse("2026-10-20T08:00:00Z");
        service.register(new Parcel("P-1001", "DHL", 1200, t0));
        service.register(new Parcel("P-1002", "UPS", 800, t0));
        service.record("P-1001", "PICKED_UP", "Leeds", t0);
        service.record("P-1001", "IN_TRANSIT", "Derby", t0.plusSeconds(3600));
        service.record("P-1002", "PICKED_UP", "York", t0);
        System.out.print(new TreeSortedReport().render(service));
        System.out.println("total grams: " + service.totalWeight());
        System.out.println(Before.report(service).equals(After.report(service)));
    }
}

See it working

mkdir -p out
javac --release 21 -d out src/com/example/parcels/*.java
java -cp out com.example.parcels.Main
P-1001 -> IN_TRANSIT @ Derby
P-1002 -> PICKED_UP @ York
total grams: 2000
true

If you see true on the last line, the var version and the explicit version agree. The whole run finished in 0.07 seconds on my machine, which is the JVM starting up, not var costing anything.

Write the checks

I'd normally use JUnit 5 here. Since this build had no route to Maven Central, these are plain checks with a main method that exits non-zero on failure. They test the same things a JUnit class would.

src/com/example/parcels/Checks.java

package com.example.parcels;

import java.time.Instant;

public class Checks {
    static int passed = 0, failed = 0;

    static void check(String name, boolean ok) {
        if (ok) { passed++; System.out.println("PASS  " + name); }
        else { failed++; System.out.println("FAIL  " + name); }
    }

    public static void main(String[] args) {
        var service = new ParcelService();
        var t0 = Instant.parse("2026-10-20T08:00:00Z");
        service.register(new Parcel("P-1", "DHL", 1200, t0));
        service.register(new Parcel("P-2", "UPS", 800, t0));
        service.record("P-1", "PICKED_UP", "Leeds", t0);
        service.record("P-2", "PICKED_UP", "York", t0);

        check("Before and After produce the same report", Before.report(service).equals(After.report(service)));
        check("total weight is 2000 grams", service.totalWeight() == 2000);
        check("diamond trap accepts an Integer", Traps.diamondTrap().equals("[P-1, 42]"));
        check("explicit type argument gives ArrayList<String>", Traps.diamondFixed().equals("[P-1]"));
        check("var keeps the concrete type ArrayList", Traps.concreteType().equals("ArrayList/0"));
        check("literal types", Traps.literalTypes().equals("Integer,Long,Double,Float,Character,1410065408"));
        check("ternary with mixed types compiles (Integer)", Traps.ternaryTrap(true).equals("Integer"));
        check("ternary with mixed types compiles (String)", Traps.ternaryTrap(false).equals("String"));
        check("anonymous class members reachable through var", Traps.anonymousClass() == 2);
        check("var in lambda parameters", Traps.lambdaParams() == 5);
        check("var can be a variable name", Traps.varAsName().equals("legal"));

        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) System.exit(1);
    }
}
javac --release 21 -d out src/com/example/parcels/*.java
java -cp out com.example.parcels.Checks
PASS  Before and After produce the same report
PASS  total weight is 2000 grams
PASS  diamond trap accepts an Integer
PASS  explicit type argument gives ArrayList<String>
PASS  var keeps the concrete type ArrayList
PASS  literal types
PASS  ternary with mixed types compiles (Integer)
PASS  ternary with mixed types compiles (String)
PASS  anonymous class members reachable through var
PASS  var in lambda parameters
PASS  var can be a variable name
11 passed, 0 failed

The odd number in the literal check, 1410065408, is 100_000 * 100_000 overflowing an int. var f = 100_000 * 100_000 is an int, and nothing warns you. Write 100_000L * 100_000 if you wanted a long.

What the compiler does with var

var isn't a type. It's an instruction to the compiler: look at the initializer, work out its type, and write that type in for me. It happens once, at compile time. The class file doesn't know var existed.

I checked this with javap. For List<String> l = new ArrayList<String>() the bytecode calls List.add through invokeinterface. For var l = new ArrayList<String>() it calls ArrayList.add through invokevirtual. That one line is the whole difference, and it comes from the type being ArrayList instead of List, not from var itself.

Because the type comes from the right-hand side, three things follow:

  • No initializer, no var. var x; has nothing to infer from.
  • null, an array initializer {1,2,3}, a lambda, and a method reference all lack a type of their own, so none of them work either.
  • The variable's type is fixed after that. var x = 1; x = "s"; fails because x is an int.

var is also a restricted type name, not a keyword. That's why var var = "legal" compiles and class var {} doesn't. Code from before Java 10 that used var as a variable or method name still compiles.

For the same idea in the context of other recent additions, see how to use sealed classes and interfaces, and for var inside a stream pipeline see how to filter and transform a list with Java streams.

Compiler messages you'll meet

I forced each of these on purpose. Cause first, then the cure.

cannot use 'var' on variable without initializer (var x; x = 1;) means the compiler had nothing to infer from. Initialize it on the declaration line, or write the type.

variable initializer is 'null' (var x = null;) means null has no type of its own. Write the type.

array initializer needs an explicit target-type (var x = {1,2,3};) means the braces alone don't say whether this is an int[] or a long[]. Use var x = new int[]{1, 2, 3};.

lambda expression needs an explicit target-type (var f = () -> 1;) means a lambda needs a functional interface to be a lambda of. Declare it as Supplier<Integer> f = () -> 1;.

'var' is not allowed here appeared for a field (var field = 1;) and for a method return type (static var m()). var is for local variables, for loop variables, try-with-resources variables and lambda parameters. Fields and method signatures are public contracts, and the language wants those written out.

'var' is not allowed in a compound declaration (var x = 1, y = 2;). One var per declaration.

incompatible types: String cannot be converted to int (var x = 1; x = "s";) is the compiler doing its job. The variable is an int.

incompatible types: possible lossy conversion from double to int (var x = 10; x = 3.5;) is the same lesson with numbers. If you want a double, initialize with 10.0 or write double.

'var' not allowed here ... restricted type name (class var {}) means you can't name a type var any more.

cannot mix 'var' and implicitly-typed parameters ((var x, y) -> x + y) means in a lambda, either every parameter uses var or none does. Mixing them is out. The reason to write (var x, var y) at all is to attach an annotation to the parameter.

cannot find symbol class var is what you get if you compile with --release 9. Java 9 doesn't know var, so it treats it as a missing class. I checked: --release 10 compiles the same file cleanly.

variable s is already defined (two var s in one scope) is ordinary Java. Mentioning it because var doesn't give you a second chance to redeclare.

One that I expected to fail silently, and didn't

I wanted to show var hiding a change in return type. I wrote var kg = weightKg(); where weightKg() returned int, then changed it to return double, with a following line int rounded = kg / 2;. I expected it to compile and quietly change behaviour. It didn't: the explicit int rounded on the next line caught it, with possible lossy conversion from double to int. So var didn't hide the change here, because something downstream still named a type. If everything downstream also used var, nothing would complain, and the behaviour would change without an error. Your tests are what catch that, not the compiler.

Then there's integer division. I ran this:

var grams = 5; var parcels = 2;
var avg = grams / parcels;               // int
double avg2 = grams / (double) parcels;  // double

and it printed 2 2.5. Nothing about that is specific to var, but with an explicit double avg you'd have stopped and thought about the cast. With var the declaration doesn't prompt you.

A last small oddity from Mixed.java: var y = (a.length == 0) ? 1 : 2.5; printed 1.0. A ternary with an int and a double promotes both to double, so y is a double and the 1 became 1.0. And var x = true ? 1 : "one" isn't an Object but an intersection type; I confirmed x held an Integer at runtime.

Team rules worth writing down

Here's how I'd decide, using what the build above showed.

Use var when the right-hand side names the type (new StringBuilder(), new ParcelService(), Instant.parse(...)), when the type is a long generic you'd otherwise repeat, and for loop variables over a typed collection.

Keep the explicit type when the initializer is a method call whose return type isn't obvious from its name, when you want to program to an interface (List<String>, not ArrayList<String>), when you're working with numeric literals and the exact primitive type matters, and in anything that's a field, parameter or return type, where var isn't allowed anyway.

Never write var x = new ArrayList<>(); and var x = flag ? 1 : "one";.

Name variables well. Without a visible type, var data = load(); tells the reader nothing, and var overdueParcels = load(); tells them plenty. Most var complaints in code review are really naming complaints.

Style guides disagree on how aggressively to use it. The official OpenJDK style guidelines for var (JEP 286 and the local-variable type inference guide) lean cautious, and I'd follow them: a reader should never need an IDE to know what a variable is.

I didn't check exact IDE menu paths for showing inferred types or for converting between var and explicit types in this run, so I'm not going to invent them. Search your IDE's settings for "var" and you'll find both.

Checklist

  • You can use var for local variables, for and for-each variables, try-with-resources variables and lambda parameters (Java 11+). You can't use it for fields, parameters or return types.
  • The initializer decides the type. It must exist, and can't be null, a lambda, a method reference or a bare array initializer.
  • new ArrayList<>() with var gives you ArrayList<Object>. Put the type argument on the right.
  • var gives you the concrete type on the right, not the interface you may have wanted.
  • The compiled code is the same as with an explicit type. The only difference is which type the compiler picked.

Questions people ask about var

Is var the same as Object or dynamic typing? No. The type is fixed at compile time. var x = 1; x = "s"; doesn't compile because x is an int.

Can I use var in Java 8? No. It arrived in Java 10, and var in lambda parameters in Java 11. Compiling with --release 9 gives cannot find symbol class var.

Does var make code slower or use more memory? No. I compared the bytecode for List<String> l = new ArrayList<String>() and var l = new ArrayList<String>(): the same new and constructor call, and the only change was invokeinterface List.add versus invokevirtual ArrayList.add.

Can var be used for fields or method return types? No. Both produce 'var' is not allowed here.

Can a variable still be named var? Yes, var var = "legal" compiles. You can't name a class var.

References

how-to-tutorialscore-javamodern-java

Staff engineer architecting Kafka and Flink platforms for banking, where a dropped event is a reconciliation break, not a retry. Through javahandbook.com, I share production-tested guidance on building reliable event-driven systems.

Comments