← Fundamentals
Guided lesson 7 questions · Java only

Java final, primitives, and boxing

What an int, an Integer, and final each guarantee.

A Java variable holds a primitive value or a reference to an object. Java converts an int to an Integer object and back without being asked; after that conversion, == compares objects, and a null can reach an int. final stops a variable from being assigned again. It does not make an object unchangeable.

Written for Java.

Before you begin

Why this matters

Code that mixes int and Integer can pass every test with small numbers and fail with large ones, or crash on a value that is missing. If you know where Java converts one into the other, you can find these bugs by reading the code.

What this helps you reason about

  • Predict whether == on two Integer values prints true or false, and use equals instead.
  • Find the line that throws NullPointerException when a missing value becomes an int, and read the message Java 17 prints.
  • Say what final stops for a local, a parameter, and a field, and what it leaves changeable.

In Python and JavaScript, two numbers that look equal are equal. Kotlin has one Int type. Java has two kinds of number variable: an int holds the number, and an Integer holds a reference to an object that holds the number. Java converts between the two without being asked. Most of the time that is invisible. This lesson shows where it becomes visible: == on two Integers, a missing value that becomes an int, and a List of Integers that removes the wrong element. It ends with what final protects.

Already know this? The short version
  • Java has eight primitive types (byte, short, int, long, float, double, char, boolean); every other type, including the wrapper classes Integer, Long, Double, and the rest, is a reference type.
  • javac inserts the conversions: it compiles Integer x = n to Integer.valueOf(n), and int n = x to x.intValue(), which throws NullPointerException when x is null; Java 17’s message names the expression that was null.
  • == on two Integers compares identity, and Integer.valueOf reuses one object per number from -128 to 127, so == looks correct in small tests and fails at 128; compare with equals, or unbox one side, because == between an int and an Integer compares numbers.
  • The compiler prefers an overload that needs no boxing, so list.remove(2) on a List<Integer> removes index 2, and list.remove(Integer.valueOf(2)) removes the value 2.
  • final forbids a second assignment to a local, a parameter, or a field, and a final field must be assigned exactly once by the end of every constructor; final never makes the object itself unchangeable. The section “What goes wrong with boxing and final in real programs?” covers Long ids compared with ==, an int passed to a Long collection, a counter map that crashes on a new key, and a list shared through a final field.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
primitive
A value of one of Java’s eight primitive types: byte, short, int, long, float, double, char, or boolean. A primitive variable holds the value itself, not a reference.
reference
A value that leads to an object, or null. A variable of a class or array type holds a reference, never the object itself, and copying the reference does not copy the object.
wrapper class
A class whose objects each hold one primitive value: Integer for int, and Byte, Short, Long, Float, Double, Character, and Boolean for the others. A wrapper object never changes its value.
boxing
Java’s automatic conversion of a primitive into its wrapper object, such as int into Integer. Unboxing is the conversion back.
== and equals
For two references, == is true when both lead to the same object. equals is a method. A class that overrides it, such as String, compares contents; Object’s own equals compares references, like ==. Two Strings are equal when they contain the same characters.
null
A reference that refers to no object. Map.get and System.getenv return null for a name that is not there.
overload
One of several methods in a class with the same name and different parameter types. The compiler chooses one from the types of the arguments in the call.
final
A modifier that stops a variable from being assigned again. It does not stop the object the variable refers to from being changed.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / int and Integer

What is the difference between int and Integer?

Variables showed that assignment copies whatever a variable holds: a primitive value or a reference. Java has exactly eight primitive types: byte, short, int, and long for whole numbers, float and double for decimals, char for one character, and boolean. Every other type, including String and every array, is a reference type.

Each primitive type has a wrapper class, whose objects each hold one value of that type. Integer wraps int; the others are Byte, Short, Long, Float, Double, Character, and Boolean. A collection holds objects, never primitives. The type in angle brackets says what a collection holds, so a list of whole numbers is a List<Integer>, never a List<int>.

Java converts between int and Integer for you. The conversion from int to Integer is called boxing, and the conversion back is unboxing. javac compiles boxing as a call to Integer.valueOf, which returns an Integer object that holds the number, and unboxing as a call to intValue on the object. Both calls are in the compiled program; they do not appear in your code.

An Integer object never changes. boxed = boxed + 1 unboxes boxed, adds 1, and boxes the result into a different Integer. A variable that still refers to the first object still sees the old number.

Main.java
int floor = 7;
// compiled as Integer.valueOf(floor)
Integer boxed = floor;
// compiled as boxed.intValue()
int back = boxed;

int holds a number; Integer holds a reference to an object, and javac converts between them with method calls you do not write.

Coming from Kotlin: Kotlin has one Int type and boxes it when needed, for example for Int?; in Java you choose int or Integer yourself, and the choice changes what == and null do.

Precisely: what the compiler writes

The Java Language Specification (5.1.7 and 5.1.8) defines boxing and unboxing by their results: boxing an int gives an Integer that holds the same number, and boxing the same int from -128 to 127 twice gives the same object. It does not name a method. Calling Integer.valueOf and intValue is how javac implements the conversions; javap -c Main lists the compiled instructions, and both calls appear there. The constructor new Integer(7) always makes a new object, but it has been deprecated since Java 9; valueOf may return an object it made earlier, as the next section shows. Sizes are fixed by the language: byte 8 bits, short 16, int 32, long 64, float 32, double 64, and char 16 (one UTF-16 code unit). The size of a boolean is not specified.

Experiment

Add 1 to an Integer and watch its id

javac compiles Integer boxed = floor as a call to Integer.valueOf(7). Integer saved = boxed copies the reference, so both variables lead to one object. javac compiles boxed + 1 as boxed.intValue() + 1 and boxes the 8 with Integer.valueOf, so boxed receives a reference to another object. The first Integer is never changed.

Why it matters Changing an Integer variable makes it refer to another object. Another variable that shares the old Integer keeps the old number.

Predict the three printed numbers. Does saved change when boxed does? Then change boxed = boxed + 1; to boxed++; and predict again.

class Main {
    public static void main(String[] args) {
        int floor = 7;
        Integer boxed = floor;
        Integer saved = boxed;
        boxed = boxed + 1;
        System.out.println(floor + " " + boxed + " " + saved);
    }
}
What to look for

floor shows 7, a plain number. boxed shows java.lang.Integer@ followed by a number, then {value: 7}: an object whose field value holds 7. The number after @ is the debugger’s id for one object, not a memory address. saved shows the same id as boxed. After boxed = boxed + 1, boxed shows a different id with {value: 8}, and saved keeps the first id and 7. The run prints 7 8 7. After the edit, the stops and the output are the same: boxed++ also makes boxed refer to a different Integer.

Expected output

7 8 7

02 / == and equals

What does == compare?

== asks a different question for each kind of value. For two primitives, == compares the values: 128 == 128 is true. For two references, == is true when both lead to the same object. So for two Integer objects, == compares objects, not numbers.

The trap is that small numbers seem to work. Integer.valueOf keeps one Integer object for each number from -128 to 127, and returns that same object every time. The Java Language Specification requires this range. Two boxed 127s are therefore one object, and == says true. Two boxed 128s are two objects unless Java was started with a larger cache (the note below shows how), and == says false. A test with small numbers passes, and the same code fails with larger ones.

To compare the numbers, use equals: large.equals(largeAgain) is true when both hold the same number. When one side of == is an int, Java unboxes the other side and compares numbers, so an int compared with an Integer is safe unless the Integer is null.

Main.java
Integer small = 127;
Integer smallAgain = 127;
Integer large = 128;
Integer largeAgain = 128;
System.out.println(small == smallAgain); // true
System.out.println(large == largeAgain); // false
System.out.println(large.equals(largeAgain)); // true

Compare Integer objects with equals; == on two Integers compares objects.

Coming from Python and JavaScript: In Python and JavaScript, 128 == 128 is true however the two numbers were made; in Java it is false when both sides are Integer objects, because == asks the question Python’s is asks.

Precisely: the cache

The Java Language Specification (5.1.7) requires one shared object per value from -128 to 127. OpenJDK can keep more: started with -XX:AutoBoxCacheMax=1000, or with the system property java.lang.Integer.IntegerCache.high=1000, it returns one object for each number up to 1000, 128 included. So == on two Integers can give different answers on different machines, which is one more reason never to rely on it. Long, Short, and Byte share the same range, Character shares 0 to 127, Boolean has two objects, and Double and Float share none. equals also checks the class: Long.valueOf(5).equals(5) is false, because 5 is boxed to an Integer. < and > unbox both sides, so for two Integers holding 1000, a < b, a > b, and a == b are all false.

Experiment

Compare two boxed 127s, then two boxed 128s

first == second compares two references. Integer.valueOf returns the same cached object for every number from -128 to 127, and a new object for 128 in this run. equals compares the numbers inside the objects, so it prints true for both.

Why it matters Code that compares Integer objects with == can pass every test with small numbers and fail with larger ones. Use equals.

Predict both lines. Then change 127 to 128 and predict again.

class Main {
    public static void main(String[] args) {
        int laps = 127;
        Integer first = laps;
        Integer second = laps;
        System.out.println(first == second);
        System.out.println(first.equals(second));
    }
}
What to look for

Step past the two Integer lines and compare the ids after @ for first and second. With 127, both show the same id: Integer.valueOf returned one cached object, and == prints true. After the edit, first and second show two different ids, both with {value: 128}, and the run prints false, then true.

Expected output

true
true

03 / Unboxing null

What happens when null becomes an int?

An Integer variable can hold null, because it holds a reference. An int cannot. When Java must unbox null, the hidden intValue call has no object to run on, and the program throws a NullPointerException: an exception, an object that stops the normal flow of the program when an operation fails.

The most common source is a map: on a Map<String, Integer>, get returns null for a key the map does not contain. Assigning that result to an int unboxes it at once, and Java 17 says what was null:

Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

The message names intValue, a method your code never calls. That name is the sign of unboxing.

There are two fixes. getOrDefault(key, 0) returns 0 for a missing key, which is right when a missing entry means none. Keeping the result in an Integer and testing it against null lets the code treat a missing entry differently from an entry of 0.

The same choice comes back when you write a method that sometimes has no number to return. An int with a special value, such as -1, works when -1 can never be a real answer. An Integer can be null, and every caller must test it before unboxing. OptionalInt, from java.util, is an object that holds one int or nothing, and the caller must ask which before using the number.

Main.java
Map<String, Integer> seeds = new HashMap<>();
seeds.put("basil", 12);
// null: the map has no "dill" key
Integer found = seeds.get("dill");
// 0 for a missing key
int packets = seeds.getOrDefault("dill", 0);
// throws NullPointerException
int crash = seeds.get("dill");

Unboxing null throws NullPointerException; test for null or use a default before a value becomes an int.

Coming from C#: C# will not assign an int? to an int without a cast; Java converts an Integer to an int with no cast, so the missing value is found when the program runs, not when it compiles.

Precisely: the message, and other places that unbox

These detailed messages (JEP 358) are on by default since Java 15. They name a local variable only when the class was compiled with debug information (javac -g), as the stepper compiles it; without it, the message says "<local1>" where the stepper says "boxed". Unboxing also happens in arithmetic (boxed + 1), in comparisons with < or with an int, and in the conditional operator: ready ? boxed : 0 has type int, so it throws when boxed is null.

Experiment

Unbox a missing map entry

seeds.get("dill") returns null, because the map has no "dill" key. Storing null in an Integer is allowed. Storing it in an int runs the intValue() call that javac compiled into that line, and calling a method on null throws NullPointerException. getOrDefault returns 0 for a missing key, so nothing null is unboxed.

Why it matters When a NullPointerException message names intValue, look for an Integer becoming an int on that line.

The catch block runs when a line in the try block throws a NullPointerException; e.getMessage() returns the exception’s message. Predict which println lines run and what each prints. Then change seeds.get("dill") inside the try block to seeds.getOrDefault("dill", 0) and predict again.

import java.util.HashMap;
import java.util.Map;

class Main {
    public static void main(String[] args) {
        Map<String, Integer> seeds = new HashMap<>();
        seeds.put("basil", 12);
        Integer found = seeds.get("dill");
        System.out.println("found " + found);
        try {
            int packets = seeds.get("dill");
            System.out.println("packets " + packets);
        } catch (NullPointerException e) {
            System.out.println(e.getMessage());
        }
    }
}
What to look for

seeds shows its one entry, "basil" with an Integer holding 12. found shows null. Step from int packets = seeds.get("dill"): the next stop is the catch line, so println("packets " + packets) never runs. The panel then shows e as a long NullPointerException object whose extendedMessage field is null; Java builds the message when getMessage is called, and the output shows it: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null. After the edit, the step after the packets line is the println in the try block, packets shows 0, and the run prints found null, then packets 0.

Expected output

found null
Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

04 / Overloads

Which method runs when an int could be boxed?

A class can have several methods with the same name and different parameter types. Each one is an overload, and the compiler chooses one from the types of the arguments. List has two remove methods. remove(int index) removes the element at a position, counting from 0. remove(Object o) removes the first element equal to o.

For a List<Integer>, rooms.remove(2) could mean either one. The compiler first looks for a method that fits without boxing. remove(int) fits, so the call removes the element at index 2. To remove the value 2, pass an object: rooms.remove(Integer.valueOf(2)). The choice depends on the declared type of the argument, so a variable declared Integer also selects remove(Object).

In the code below, List.of(5, 2, 9) makes a list that cannot change, and new ArrayList<>(...) copies it into a list that can.

Main.java
List<Integer> rooms = new ArrayList<>(List.of(5, 2, 9));
// remove(int): removes index 2, the 9
rooms.remove(2);
System.out.println(rooms); // [5, 2]
// remove(Object): removes the value 2
rooms.remove(Integer.valueOf(2));
System.out.println(rooms); // [5]

On a List<Integer>, remove(2) removes a position; remove(Integer.valueOf(2)) removes a value.

Precisely: how the compiler chooses

Overload resolution runs in phases (JLS 15.12.2): first without boxing or unboxing, then with them, then with variable-length argument lists. The first phase that finds a method wins. A Set<Integer> has only remove(Object), so set.remove(2) removes the value.

Experiment

Remove 2 from a List of Integers

List has remove(int index) and remove(Object o). The argument 2 is an int, and remove(int) accepts it without boxing, so the compiler chooses it and the list loses the element at index 2, the 9. Integer.valueOf(2) is an object, so the edited call matches remove(Object), which removes the first element equal to 2.

Why it matters On a List<Integer>, remove with an int argument removes by position. Pass an Integer to remove a value.

Predict what prints. Then change rooms.remove(2); to rooms.remove(Integer.valueOf(2)); and predict again.

import java.util.ArrayList;
import java.util.List;

class Main {
    public static void main(String[] args) {
        List<Integer> rooms = new ArrayList<>(List.of(5, 2, 9));
        rooms.remove(2);
        System.out.println(rooms);
    }
}
What to look for

Look at rooms before and after the remove line. The panel lists the ArrayList’s elements as three Integers, each with its own id: {value: 5}, {value: 2}, {value: 9}. After rooms.remove(2), the list shows two Integers, 5 and 2: the element at index 2 is gone. The run prints [5, 2]. After the edit, the Integer holding 2 leaves the list, the Integer holding 9 keeps its id and moves into its place, and the run prints [5, 9].

Expected output

[5, 2]

05 / final

What does final prevent?

final can be written on three kinds of variable, and for each one it stops a second assignment. It does not stop the object that the variable refers to from changing; the experiment below changes a field of an object through a final variable.

A final local can be assigned once, in its declaration or on a later line. javac rejects a second assignment and points at that line. After final int limit = 3;, the line limit = 4; gives:

error: cannot assign a value to final variable limit

If the declaration has no value and two later lines assign limit, the second one gives: error: variable limit might already have been assigned.

A final parameter receives its value from the call, so any assignment in the method is rejected:

error: final parameter n may not be assigned

A final field must be assigned exactly once. A final instance field (one copy per object) can be assigned in its declaration, in an instance initializer block (a { } block written directly in the class body, which runs for every new object), or in a constructor. Whichever place is used, the field must be assigned exactly once by the end of every constructor. A static final field (one copy for the class) is assigned in its declaration or in a static initializer block, static { }. If a constructor can finish without assigning the field, javac points at the closing brace of that constructor:

error: variable litres might not have been initialized

For a class with no constructor of its own, the message is: error: variable litres not initialized in the default constructor. An assignment to the field in any other method gives the “cannot assign a value to final variable” error.

final on an Integer variable stops reassignment, and the Integer object could never change anyway. Integer keeps its number in a field declared private final int value, the field the stepper shows as {value: 7}. A class whose fields are all final, and hold primitives or objects that cannot change, makes objects that cannot change. Integer and String are built this way.

Main.java
class Tank {
    final int litres;

    Tank(int litres) {
        // the one assignment this field gets
        this.litres = litres;
    }
}

final int limit = 3;
// limit = 4; would not compile

final stops a second assignment to a local, a parameter, or a field; it never makes the object unchangeable.

Precisely: other meanings of final

JLS 8.3.1.2 states the field rule, and chapter 16 (Definite Assignment) defines the analysis javac runs to check it: a final instance field must be definitely assigned at the end of every constructor and must not be assigned twice, and a static final field must be definitely assigned at the end of the static initializers. A local that is never assigned after its first assignment is effectively final, and a lambda can use it, as Stack and heap shows. A static final field holding a primitive or a String literal is a constant, which the compiler copies into the code that uses it. Final fields also have a promise about threads: another thread that sees the object sees the values the constructor gave them (JLS 17.5). On a method or a class, final means something else: the method cannot be overridden, and the class cannot be extended.

Experiment

Change an object through a final variable

final applies to the variable bucket: it keeps one reference for its whole life. bucket.litres = 10 does not assign bucket; it assigns the litres field of the object that bucket refers to, and that field is not final. The edited line assigns bucket itself, which final forbids, so javac rejects the program before it runs.

Why it matters final on a reference fixes which object the variable refers to. To stop the object from changing, its class must declare its fields final.

bucket is final. Predict what prints. Then change bucket.litres = 10; to bucket = new Bucket(10); and predict again.

class Bucket {
    int litres;

    Bucket(int litres) {
        this.litres = litres;
    }
}

class Main {
    public static void main(String[] args) {
        final Bucket bucket = new Bucket(40);
        Bucket same = bucket;
        bucket.litres = 10;
        System.out.println(same.litres + " " + (same == bucket));
    }
}
What to look for

The first stops are inside the Bucket constructor, where this shows {litres: 0} (a new object’s int fields start at 0) until the assignment runs. Back in main, bucket shows Bucket@ and an id, then {litres: 40}, and same shows the same id. After bucket.litres = 10, bucket and same still show that id, now with {litres: 10}. The run prints 10 true. After the edit, the program does not compile, and javac reports: Main.java:13: error: cannot assign a value to final variable bucket.

Expected output

10 true

06 / In production

What goes wrong with boxing and final in real programs?

Each bug below follows from a rule in this lesson, and each one can pass every test with small data.

Database ids are often Long objects, and Long.valueOf shares objects only from -128 to 127, as Integer.valueOf does. So order.ownerId() == user.id() is true for the first 127 users and false after that. Compare ids with equals, or with Objects.equals(a, b) from java.util when either id can be null.

A Set<Long> never contains an int. ids.contains(5) boxes 5 to an Integer, and an Integer is never equal to a Long, so the result is false even when the set holds 5L. On a Map<Long, String>, owners.get(42) returns null for the same reason. Both calls compile, because contains and get accept any Object. Write 5L, or pass a long variable.

A counter map crashes on the first occurrence of each key: counts.put(colour, counts.get(colour) + 1) unboxes null for a colour not yet in the map. counts.merge(colour, 1, Integer::sum) stores 1 for a new key. For a key that is already there, merge calls the function given as its third argument with the old count and 1, and stores the result. Integer::sum is a method reference: it passes the existing method Integer.sum, which adds two ints, as that function. The lambda (old, one) -> old + one, a small function written in place, would do the same.

A final field that holds a list does not stop the list from changing. If a constructor stores the list it receives, the object shares that list with the code that called the constructor, and a later change to the list appears in the object. Store List.copyOf(list), a copy that cannot be changed. Collections.unmodifiableList(list) is not a copy: it refuses changes made through it, but it shows changes made to the original list.

Main.java
// false once ids pass 127: two Long objects
order.ownerId() == user.id()
// false: 5 is boxed to an Integer
ids.contains(5)
// NullPointerException for a new colour
counts.put(colour, counts.get(colour) + 1);
// 1 for a new colour, otherwise the old count plus 1
counts.merge(colour, 1, Integer::sum);

Compare Long ids with equals, pass a long to a Long collection, count with merge, and copy a list before a final field stores it.

Experiment

Change a list after a final field stores it

final on the field campers stops a second assignment to the field. The constructor assigns the reference it received, so names and tent.campers lead to one ArrayList, and names.add changes that list. List.copyOf makes a new list with the same elements, and that list throws UnsupportedOperationException on any change.

Why it matters When an object must keep the list it was given, copy the list in the constructor. final alone does not protect the elements.

campers is a final field. Predict what prints. Then change this.campers = campers; to this.campers = List.copyOf(campers); and predict again.

import java.util.ArrayList;
import java.util.List;

class Tent {
    final List<String> campers;

    Tent(List<String> campers) {
        this.campers = campers;
    }
}

class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Lior", "Mei"));
        Tent tent = new Tent(names);
        names.add("Ravi");
        System.out.println(tent.campers);
    }
}
What to look for

In main, names shows java.util.ArrayList@ and an id, then the elements ["Lior", "Mei"]. Inside the Tent constructor, the parameter campers shows the same id. this shows {campers: null} until the assignment runs, then the campers field shows that id too. Back in main, tent shows Tent@ and an id, with the campers field inside it. After names.add("Ravi"), names shows ["Lior", "Mei", "Ravi"], and the campers field of tent shows the same three names. The run prints [Lior, Mei, Ravi]. After the edit, the campers field shows a different id and the class java.util.ImmutableCollections$List12, a class Java uses for small lists that cannot change. names.add changes only names, the campers field keeps ["Lior", "Mei"], and the run prints [Lior, Mei].

Expected output

[Lior, Mei, Ravi]

07 / Try it

Can you predict each comparison?

Each comparison below compares either two objects or two numbers. Decide which before predicting the output.

Main.java
import java.util.HashMap;
import java.util.Map;

class Main {
    public static void main(String[] args) {
        Map<String, Integer> votes = new HashMap<>();
        votes.put("oak", 200);
        votes.put("elm", 200);
        Integer oak = votes.get("oak");
        Integer elm = votes.get("elm");
        int ash = votes.getOrDefault("ash", 0);
        System.out.println(oak == elm);
        System.out.println(oak.equals(elm) + " " + ash);
        final Integer total = oak + elm;
        System.out.println(total == 400);
    }
}

What are the three printed lines?

Reveal the reasoning

false, then true 0, then true. Each put boxed 200 with its own call to Integer.valueOf, and 200 is outside the range -128 to 127, so oak and elm refer to two objects and == is false. equals compares the numbers. ash uses getOrDefault, so the missing "ash" key gives 0 instead of a NullPointerException. total == 400 compares an Integer with an int, so Java unboxes total and compares numbers. final on total changes nothing here.

Practice tickets

  1. JAVA-101 · High-scoring drawn rounds go uncounted in darts → Free
  2. JAVA-102 · One unlisted drink loses a whole order’s stamps → Sign in · Pro
  3. JAVA-103 · A sent race entry changes its crew → Sign in · Pro

Keep exploring

Stack and heap →

Primary references