☕ Java

var Keyword

The var keyword, introduced in Java 10 as local variable type inference, lets a local variable declaration omit its explicit type and have the compiler infer it from the initializer expression on the right-hand side, while the variable remains just as statically typed at compile time as if the type had been written out explicitly — var is purely a source-code convenience, not a shift toward dynamic typing. This is a narrower, more conservative feature than type inference in languages like C# (var) or Kotlin (val/var): it applies only to local variables with an initializer, never to fields, method parameters, or return types, and the compiler still performs full static type checking using the inferred type. This entry covers the motivation and explicit scope restrictions, how type inference actually resolves (including with generics, diamond operator interaction, and anonymous classes), the readability tradeoffs and style guidance the OpenJDK team itself published, and the specific cases where var cannot be used.

Motivation and Scope — What var Is and Deliberately Is Not

Before Java 10, every local variable declaration required its type to be written explicitly, even when that type was already fully evident from the initializer on the right-hand side of the assignment — particularly verbose for generic types, where a declaration like Map<String, List<Customer>> customerOrders = new HashMap<String, List<Customer>>() repeated the same generic type information twice in a single line (a redundancy partially alleviated, but not eliminated, by the diamond operator <> introduced in Java 7). JEP 286, delivered in Java 10, introduced var specifically to let the compiler infer the local variable's type from its initializer, eliminating this redundancy for the narrow case where it provides genuine, unambiguous benefit. Critically, var performs type inference, not dynamic or weak typing: the compiler determines the concrete static type at compile time from the initializer expression, and that inferred type is then fixed for the variable's entire scope exactly as if it had been written explicitly — assigning a value of an incompatible type to a var-declared variable later in the same scope is still a compile-time error, identical to what would happen with an explicitly-typed variable. The Java language designers deliberately scoped var far more narrowly than equivalent features in other JVM languages: var applies only to local variables that have an initializer at the point of declaration (including for-loop indices and try-with-resources resources, both of which always have an initializer), and explicitly does not apply to fields, method parameters, method return types, or any declaration without an initializer — each of those was considered, and rejected, because the type information is either part of a public contract that should remain explicit (fields, parameters, return types) or because there's no initializer expression for the compiler to infer from in the first place.
Java
// ── Before Java 10 — type written out in full, often redundantly ───────
Map<String, List<Customer>> customerOrders = new HashMap<String, List<Customer>>();
ArrayList<String> names = new ArrayList<String>();   // pre-diamond-operator era
StringBuilder sb = new StringBuilder();

// ── Java 10+ — var infers the type from the initializer ─────────────────
var customerOrders = new HashMap<String, List<Customer>>();   // inferred: HashMap<String, List<Customer>>
var names = new ArrayList<String>();                            // inferred: ArrayList<String>
var sb = new StringBuilder();                                   // inferred: StringBuilder

// ── var is static typing, NOT dynamic typing — type is fixed at compile time
var count = 10;          // inferred type: int, fixed for this variable's entire scope
// count = "hello";      // COMPILE ERROR — cannot assign a String to an int-typed variable,
                          // exactly the same error as if "int count = 10;" had been written

// ── Where var IS allowed — local variables WITH an initializer ─────────
for (var i = 0; i < 10; i++) { /* i inferred as int */ }
try (var reader = new BufferedReader(new FileReader("f.txt"))) { /* inferred as BufferedReader */ }

// ── Where var is NOT allowed — fields, parameters, return types, no initializer
class Example {
    // private var name;                 // COMPILE ERROR — fields cannot use var
    // public var process(var input) {   // COMPILE ERROR — params and return types cannot use var
    //     var x;                        // COMPILE ERROR — no initializer to infer from
    // }
}

Type Inference Resolution — Generics, Diamond Operator, and Anonymous Classes

When the initializer's type is itself generic, var infers the fully-parameterized generic type exactly as the compiler would have determined it from an explicit declaration — there is no loss of generic type information or any fallback to a raw type. A subtlety arises specifically when var is combined with the diamond operator <>: writing var list = new ArrayList<>() has no explicit type on the left-hand side for the diamond operator to infer its type arguments from, so the compiler falls back to inferring ArrayList<Object> rather than producing a useful parameterized type — this combination (var with an empty diamond) is a well-known pitfall precisely because it silently compiles to something far less useful than intended, and the fix is simply to specify the type argument explicitly inside the diamond when used together with var. var also has genuinely useful, novel interaction with anonymous classes: a variable declared with var and initialized with an anonymous class expression is inferred to have the anonymous class's own (otherwise unnameable) type, including any additional methods that anonymous class declares beyond what its supertype or interface defines — meaning code can call methods on that anonymous class instance that wouldn't be visible if the variable had been declared with the named supertype/interface type instead, since the named type's API doesn't include those extra methods. This is one of the few cases where var enables something that was previously awkward or impossible to express cleanly, rather than purely shortening already-expressible code. For array types, var works with array initializers but cannot be used with the "C-style" array declaration syntax (var arr[] = ...), since that legacy syntax with brackets after the variable name is incompatible with how var's type position works; the brackets must instead be part of the initializer's type itself.
Java
// ── Generic type inference — fully preserved, not erased to raw types ──
var orders = new HashMap<String, List<Customer>>();
// orders is fully typed as HashMap<String, List<Customer>> — generics work exactly as normal:
// orders.put("x", List.of());     // type-checked normally
// orders.put("x", "not a list");  // COMPILE ERROR — type mismatch, exactly as with explicit type

// ── PITFALL: var + empty diamond operator infers Object, not the intended type
var pitfallList = new ArrayList<>();    // inferred as ArrayList<Object> — NOT what was likely intended!
pitfallList.add("a string");
pitfallList.add(42);                    // compiles fine — element type is Object, no real type safety

// CORRECT — specify the type argument explicitly when combining var with diamond:
var properList = new ArrayList<String>();   // inferred as ArrayList<String> — full type safety restored
// properList.add(42);   // now correctly a COMPILE ERROR

// ── var with anonymous classes — exposes extra methods the named type doesn't have
var runner = new Runnable() {
    @Override public void run() { System.out.println("Running"); }
    public void extraDiagnosticMethod() { System.out.println("Extra info!"); }  // NOT on Runnable
};
runner.run();                    // fine — part of Runnable
runner.extraDiagnosticMethod();  // ALSO fine — var preserves the anonymous class's full type,
                                   // including methods beyond what Runnable itself declares

// If this had been written as "Runnable runner = new Runnable() {...}" instead,
// extraDiagnosticMethod() would NOT be callable on "runner" at all — Runnable's
// declared type doesn't include it, even though the actual object has it

// ── Array syntax — initializer-side brackets only, not "C-style" ───────
var arr = new int[]{1, 2, 3};   // OK — inferred as int[]
// var arr2[] = new int[]{1, 2, 3};   // COMPILE ERROR — C-style brackets after var not allowed

Readability Tradeoffs and Style Guidance

Because var removes the explicit type from the declaration site, readability now depends more heavily on the variable name and the initializer expression actually conveying what the type is, rather than the type being stated outright — this shifts a burden from the compiler/IDE display onto the code's author, and the OpenJDK team's own published style guidelines for var (JEP 286's accompanying guidance) explicitly recommend choosing var only when the resulting code is at least as readable as the explicit-type version, and choosing variable names that compensate for the missing type when var is used, rather than applying var indiscriminately to every eligible local variable. The guidance specifically discourages var when the initializer's type is not obvious from reading the right-hand side — for instance, var result = some.deeply.chained().method().call() gives a reader far less immediately-visible information about result's type than an explicit type would, whereas var list = new ArrayList<String>() loses essentially nothing, since the type is fully visible right there in the initializer. var is also discouraged in cases where the explicit type itself functions as useful in-line documentation distinguishing between similar-looking APIs (for example, an explicit Iterator<String> versus a more generic var being used across several nearby lines with subtly different element types, where the explicit type helps a reader scanning the method body quickly distinguish what's being iterated). A separate practical consideration is that var's scope is genuinely just local variables — it has no effect whatsoever on the bytecode produced, on runtime performance, or on the program's actual behavior, since by the time compilation finishes the inferred concrete type is baked into the bytecode exactly as it would be from an explicit declaration; the entire feature is a source-level convenience for the programmer, with zero runtime semantic difference.
Java
// ── Good var usage — type is fully evident from the initializer ───────
var customers = new ArrayList<Customer>();      // clear: List of Customer
var connection = DriverManager.getConnection(url);   // clear from context/name: a Connection
for (var entry : map.entrySet()) { /* clear: Map.Entry<K,V> */ }

// ── Discouraged var usage — initializer doesn't make the type obvious ──
var result = service.process(input);   // What IS "result"? Reader has to go look up process()'s
                                          // return type — var here REMOVES useful information
                                          // rather than removing useful redundancy

// Better — explicit type aids readability here, since chained/delegated calls
// don't make the resulting type visually obvious:
OrderConfirmation result2 = service.process(input);

// ── Discouraged — explicit type served as helpful in-line documentation ─
// Before: explicit types help distinguish similar-looking nearby variables
Iterator<String> nameIterator = names.iterator();
Iterator<Integer> idIterator = ids.iterator();

// After: var makes the two declarations look identical, losing that distinction
// at the declaration site (though the names still help somewhat):
var nameIterator2 = names.iterator();
var idIterator2 = ids.iterator();
// Style guidance: prefer the explicit form here, since the types being iterated
// are meaningfully different and not obvious purely from variable naming

// ── var has ZERO runtime/bytecode effect — purely a source-level convenience
var x = 42;          // compiles to bytecode IDENTICAL to:
int x2 = 42;          // ...this. No performance difference, no behavioral difference whatsoever.

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.