FirstHack Learn
Log in Sign up free
Lessons in this course 0/6 All courses Java Fundamentals

CSE

Progress0 / 6 lessons
  1. 1. JVM, JDK and JRE explained properly
  2. 2. Classes, objects and the main method
  3. 3. Primitives, objects and autoboxing
  4. 4. String immutability and why == fails
  5. 5. Collections: ArrayList and HashMap
  6. 6. Exceptions and try-with-resources

Courses › Java Fundamentals

Primitives, objects and autoboxing

The eight primitive types, what a reference really holds, and the traps in Integer.

11 min read · Lesson 3 of 6 · Free

Java has exactly two kinds of value

A primitive holds the value itself. There are exactly eight, and Java fixes their sizes on every platform — unlike C, where int can be 2 or 4 bytes.

Type Bytes Range Default
byte 1 -128 to 127 0
short 2 -32,768 to 32,767 0
int 4 -2,147,483,648 to 2,147,483,647 0
long 8 about -9.2 x 10^18 to 9.2 x 10^18 0L
float 4 about 7 digits of precision 0.0f
double 8 about 15 digits of precision 0.0d
char 2 0 to 65,535, one UTF-16 unit ''
boolean 1 (JVM dependent) true or false false

Two differences from C worth memorising. char is 2 bytes in Java, not 1, because it holds a UTF-16 code unit rather than an ASCII byte. And boolean is a real type — if (1) does not compile in Java, which kills the if (x = 5) bug outright.

Everything else is a reference. A reference holds the address of an object on the heap, not the object.

Java
public class Kinds {
    public static void main(String[] args) {
        int a = 5;                  // the value 5 lives in a
        int[] arr = {1, 2, 3};      // arr holds a reference to a heap object
        String s = "hi";            // s holds a reference

        int b = a;                  // copies the value
        b = 99;
        System.out.println(a);      // 5, unaffected

        int[] other = arr;          // copies the reference, not the array
        other[0] = 99;
        System.out.println(arr[0]); // 99, same array
    }
}

That second case surprises people every time. other = arr does not copy the array. Both names now point at one object, and a change through either is visible through both.

Overflow still happens

Java
public class Overflow {
    public static void main(String[] args) {
        int big = 2147483647;
        System.out.println(big + 1);          // -2147483648

        int seconds = 24 * 60 * 60 * 1000 * 1000;   // overflows
        System.out.println(seconds);                 // 500654080

        long ok = 24L * 60 * 60 * 1000 * 1000;
        System.out.println(ok);                      // 86400000000
    }
}

Java does not throw on integer overflow. It wraps, silently, exactly like C. In the second line the multiplication is done in int because all the literals are int, and it overflows before anything is assigned. Adding L to the first operand makes the whole expression long. This exact trap appears in aptitude tests.

Wrapper classes

Every primitive has an object version: Integer, Long, Double, Character, Boolean, Byte, Short, Float.

They exist because collections can only hold objects. ArrayList<int> does not compile; ArrayList<Integer> does.

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

public class Wrappers {
    public static void main(String[] args) {
        List<Integer> marks = new ArrayList<>();
        marks.add(91);           // autoboxing: int 91 -> Integer.valueOf(91)
        marks.add(88);

        int first = marks.get(0);   // unboxing: Integer -> int

        System.out.println(first + 1);          // 92
        System.out.println(Integer.MAX_VALUE);  // 2147483647
        System.out.println(Integer.parseInt("123") + 1);  // 124
    }
}

Autoboxing is the compiler quietly inserting Integer.valueOf(91). Unboxing is the reverse, intValue(). Convenient, and it hides two real traps.

Trap one: == on Integer

Java
public class BoxTrap {
    public static void main(String[] args) {
        Integer a = 100, b = 100;
        Integer c = 200, d = 200;

        System.out.println(a == b);          // true
        System.out.println(c == d);          // false
        System.out.println(c.equals(d));     // true
    }
}

This is not a bug in Java and it is a favourite exam question.

Integer.valueOf keeps a cache of objects for the values -128 to 127. For 100, both a and b get the same cached object, so == — which compares references — is true. For 200, two separate objects are created, so the references differ and == is false.

⚠️

Never use == on wrapper objects. It compares references, and whether it happens to work depends on a cache boundary at 127 that has nothing to do with your logic. Use .equals() for wrappers, == only for primitives.

Trap two: unboxing null

Java
public class NullBox {
    public static void main(String[] args) {
        Integer count = null;

        try {
            int n = count;          // unboxing calls count.intValue()
            System.out.println(n);
        } catch (NullPointerException e) {
            System.out.println("NullPointerException from unboxing");
        }
    }
}

A primitive int cannot be null. A wrapper Integer can. Assigning a null Integer to an int calls intValue() on null, and you get a NullPointerException on a line that contains no visible method call. This is a common cause of confusing NPEs in code that reads values out of a map.

Trap three: wrappers are slow in a loop

Java
public class BoxCost {
    public static void main(String[] args) {
        long start = System.nanoTime();
        long sumPrim = 0;                    // primitive
        for (int i = 0; i < 10_000_000; i++) sumPrim += i;
        long t1 = System.nanoTime() - start;

        start = System.nanoTime();
        Long sumBox = 0L;                    // wrapper: boxes every iteration
        for (int i = 0; i < 10_000_000; i++) sumBox += i;
        long t2 = System.nanoTime() - start;

        System.out.println("primitive " + t1 / 1_000_000 + " ms");
        System.out.println("wrapper   " + t2 / 1_000_000 + " ms");
        System.out.println(sumPrim + " " + sumBox);
    }
}

The wrapper loop is typically several times slower, because sumBox += i unboxes, adds, and creates a brand new Long object on every single iteration — ten million short-lived objects for the garbage collector to clean.

Underscores in 10_000_000 are legal since Java 7 and purely for readability.

💡

Use primitives for arithmetic and loop counters. Use wrappers only where an object is required: inside collections, or when you need null to mean "no value recorded".

Passing to methods

Java
public class Passing {
    static void changeValue(int x)     { x = 99; }
    static void changeArray(int[] a)   { a[0] = 99; }
    static void replaceArray(int[] a)  { a = new int[]{7, 7, 7}; }

    public static void main(String[] args) {
        int n = 1;
        int[] arr = {1, 2, 3};

        changeValue(n);
        System.out.println(n);          // 1

        changeArray(arr);
        System.out.println(arr[0]);     // 99

        replaceArray(arr);
        System.out.println(arr[0]);     // still 99
    }
}

Java is always pass by value. For a primitive, the value is the number. For an object, the value is the reference — so the method can modify the object it points to (changeArray works) but cannot make your variable point somewhere else (replaceArray does nothing to arr). If someone tells you Java passes objects by reference, this program is the counter-example.