JAVA / VARIABLES, PRIMITIVES AND TYPES
Inferring types with var and where it breaks down
Predict the exact type var infers from an initialiser, use it where that type is obvious, and recognise the declarations where Java rejects var.
What you will learn
- Read the initialiser to predict the type: 1 is int, 1L long, 1.0 double, 'A' char
- Know that var never applies the constant narrowing behind byte b = 100;
- Spot the illegal spots: no initialiser, null, fields, parameters, return types, catch
- Fix var list = new ArrayList<>(); which infers ArrayList<Object>, not what you meant
Understanding Inferring types with var and where it breaks down
var asks the compiler to fill in the type of a local variable from the expression to the right of the equals sign. It is pure compile-time shorthand: javac works out the type, records that exact type in the class file, and from then on the variable is as strictly typed as one you spelled out by hand. So var count = 0; and int count = 0; compile to the same thing, and nothing about the type is decided at runtime. var is also a reserved type name rather than a keyword, which is why old code that declares a variable called var still compiles.
Inference reads only the static type of the initialiser expression. It never inspects the value's range and never looks ahead at what you plan to do with the variable. That is why the literal 1 gives an int, 1L a long, 1.0 a double and 'A' a char, and why the rule that lets byte small = 100; compile disappears: assignment into a declared byte is permitted to narrow a constant that fits, but with var there is no declared target type to narrow towards, so the variable simply becomes int. The same literal-mindedness applies to objects, where new ArrayList<String>() infers the concrete class ArrayList<String> rather than the List interface, so the variable exposes ArrayList's own methods and cannot later be pointed at a LinkedList.
var breaks down wherever the right-hand side carries no usable type. A declaration with no initialiser, an initialiser of null, and the array shorthand {1, 2, 3} are all rejected outright, while a bare diamond is worse: new ArrayList<>() has no target type to copy, so it silently settles on Object instead of failing. var is further restricted to local variables, basic and enhanced for variables, try-with-resources resources and lambda parameters, so fields, method parameters, return types and catch parameters still need real types. The last failure mode is human: in var result = parse(line); the compiler knows the type and the reader does not, and that is the moment to write the type out.
public class VarInference {
static void describe(byte x) { System.out.println("byte: " + x); }
static void describe(int x) { System.out.println("int: " + x); }
static void describe(long x) { System.out.println("long: " + x); }
static void describe(double x) { System.out.println("double: " + x); }
static void describe(char x) { System.out.println("char: " + x); }
static void describe(Object x) { System.out.println("object: " + x.getClass().getSimpleName()); }
public static void main(String[] args) {
byte written = 100; // explicit target type, so narrowing the constant is allowed
var inferred = 100; // no target type, so the literal's own type wins
var big = 100L;
var frac = 1.5;
var letter = 'A';
var text = "A";
describe(written);
describe(inferred);
describe(big);
describe(frac);
describe(letter);
describe(text);
// var nothing; // no initialiser, nothing to infer from
// var empty = null; // null carries no usable type
// inferred = 100L; // inferred is an int, permanently
}
}var copies the static type of the initialiser expression into the declaration at compile time, so the variable stays strictly typed and the initialiser alone must reveal what that type is.
Worked examples
var in loops, and the diamond gap
Shows where var reads its type from in both kinds of for loop, and how a bare diamond leaves you with Object.
import java.util.ArrayList;
public class VarLoops {
public static void main(String[] args) {
var names = new ArrayList<String>();
names.add("ada");
names.add("grace");
for (var i = 0; i < names.size(); i++) {
System.out.println(i + " -> " + names.get(i));
}
for (var name : names) {
System.out.println(name.toUpperCase());
}
var loose = new ArrayList<>();
loose.add("ada");
Object first = loose.get(0);
System.out.println("element type at compile time: Object, real class: "
+ first.getClass().getSimpleName());
}
}Example explained
Line 1for (var i = 0; ...) infers int from the literal 0, so i < names.size() and i++ behave exactly as with int i = 0.
Line 2for (var name : names) takes its type from the collection's element type, String, which is why toUpperCase() resolves.
Line 3var loose = new ArrayList<>(); gives the diamond nothing to copy, so it infers ArrayList<Object>.
Line 4loose.get(0) is therefore typed Object even though the object stored inside really is a String.
The inferred type is fixed and invisible
Demonstrates that a var variable behaves exactly like its inferred type, including promotions and silent compound-assignment casts.
public class VarIsStillTyped {
public static void main(String[] args) {
var count = 10;
var total = 10.0;
var picked = count > 5 ? 1 : 2.0;
count += 2.75;
total += 2.75;
System.out.println(count);
System.out.println(total);
System.out.println(picked);
// count = 12.75; // rejected: count is an int
}
}Example explained
Line 1var count = 10; infers int, so count += 2.75 uses the implicit cast that compound assignment allows and stores 12.
Line 2var total = 10.0; infers double, so the identical += keeps the fraction and prints 12.75.
Line 3picked mixes an int branch and a double branch, so both are promoted to double and the variable holds 1.0, not 1.
Line 4The commented line fails because the inferred type is the declared type and never loosens later.
A type you cannot write by hand
Shows that var can hold the type of an anonymous class, which has no name to spell out, and what is lost when you name a type instead.
public class VarAndUnnameableTypes {
public static void main(String[] args) {
var box = new Object() {
int width = 3;
int height = 4;
int area() { return width * height; }
};
System.out.println(box.area());
Object same = box;
System.out.println(same.getClass().getSuperclass().getSimpleName());
// System.out.println(same.width); // Object has no member width
}
}Example explained
Line 1var box keeps the anonymous class as the variable's type, so width, height and area() are all reachable through box.
Line 2That type has no source-level name, so this declaration is impossible to write without var, and box cannot leave the method as anything better than Object.
Line 3Assigning the same object to an Object variable throws that view away at compile time, so same.width would not compile.
Line 4getSuperclass() shows the runtime class is an unnamed subclass of Object, which is why the printed name is Object.
Important notes
var requires Java 10 or later, and Java 11 for lambda parameters such as (var a, var b) -> a + b; earlier compilers treat var purely as an identifier.
Nothing about var reaches runtime: the class file stores the inferred type as if you had typed it, so var never changes performance and never turns a value into a different primitive than the initialiser implied.
Common mistakes
Writing var b = 100; where byte b = 100; was meant: the constant narrowing only happens with a declared byte target, so b is an int and later storing it into a byte or byte[] fails with 'possible lossy conversion from int to byte'.
Reading var as JavaScript's var and writing var value; or var value = null;: both are compile errors, because there is no initialiser type for the compiler to copy.
Combining var with a diamond, as in var items = new ArrayList<>();: you get ArrayList<Object>, so every element comes back as Object, casts creep in, and add() silently accepts anything.
Try it yourself
Change, predict, then run
In a browser editor, write var a = 100; next to byte b = 100;, then add byte c = a; and read the exact compiler error. Make it compile by changing only a's initialiser, not by casting on the byte c line.
Open the Java workspaceCheck your understanding
byte b = 100; compiles, but var b = 100; produces an int. What does that tell you about how var chooses a type?
- It uses only the static type of the initialiser expression, so there is no declared target type left for the constant to narrow into
- It picks the smallest primitive type the value fits into, then widens it on first use
- It always picks the widest primitive type so that later arithmetic cannot overflow
- It leaves the type open until the variable is first read, and fixes it then
Show answer
byte b = 100; works only because assignment into a declared byte is allowed to narrow a constant that fits. With var there is no declared type on the left, so the variable adopts the initialiser's own type, and the literal 100 is an int. The 'smallest type that fits' answer is tempting because 100 does fit in a byte, but inference never inspects the value: var x = 1; is an int as well.