---
title: 'Primitive Types and Wrappers'
source: 'https://academia.sh/en/courses/java-fundamentals/primitive-types-and-wrappers'
course: 'Java Fundamentals'
language: en
updated: '2026-08-17T18:09:40+00:00'
license: 'CC BY-SA 4.0'
---

# Primitive Types and Wrappers

A one-letter source difference is a method call in the class file: the line Integer count = 5 adds Integer.valueOf, the reverse direction adds Integer.intValue; the same 200 boxings produce between 100 and 200 objects depending on the warehouse, and where the cache ends, identity and value come apart.

The previous lesson measured that name resolution finishes at compile time: an import line
leaves no trace in the class file, because the compiler resolves the name and puts its fully
qualified counterpart in its place. One question is left. If name resolution finishes at
compile time, when and where does the **value itself** the name carries get decided?

Java does not give a single answer to this question. When `int count = 5` is written, the
value sits in the method's own local slot, as the number itself. When `Integer count = 5` is
written, an object is found on the heap and the local slot holds only that object's
reference. The difference in the source is one letter; the difference in the class file is a
**method call.** The Programming Fundamentals course established basic data types and
implicit conversion as concepts — **that is not repeated here.** There, what a value's type
determines was explained; here, what is measured is how many **steps** the compiler puts into
the class file for this crossing.

## Two Type Families

Java's types split in two. **Primitive types** number eight and carry nothing but a number;
**reference types** carry an object's address. Every primitive type has a **wrapper** class:
a reference type that holds the same value inside an object.

| Primitive type | Width (bits) | Wrapper |
|---|---|---|
| `boolean` | unspecified | `Boolean` |
| `byte` | 8 | `Byte` |
| `short` | 16 | `Short` |
| `char` | 16 | `Character` |
| `int` | 32 | `Integer` |
| `long` | 64 | `Long` |
| `float` | 32 | `Float` |
| `double` | 64 | `Double` |

The distinction's visibility in the source is low, because the crossing between the two
families is never written. An `int` value enters a spot expecting an `Integer` on its own
(**autoboxing**), and an `Integer` enters a spot expecting an `int` on its own
(**unboxing**). In both directions, only an equals sign appears in the source.

This is also where the distinction's reason to exist lies. A primitive type is not an object:
it has no methods, it cannot be `null`, and it **cannot enter any spot expecting a
reference.** A list, a map, or any structure taking a type variable stores only references;
to put an `int` value in a list, an object to wrap it is needed first. The wrapper fills this
gap, and autoboxing is a shorthand that **erases** that filling from the source. The result is
this: a source that writes `List<Integer>` does not do the same work as one that writes
`int` — there is an object layer in between, and that layer's cost sits in the class file.

A single data carrier is used across the course: `Item`, a warehouse record with three
fields — `String name`, `int count`, `long weight` (grams). This lesson measures boxing on
the **`count` field.**

## The Measurement Core

The measurement compiles a given source text by calling the compiler from within the program,
reads the class file it produces, and returns two things for every method: how many
instructions were produced and which calls that method contains. The compiler and the class
file reader are part of the standard library.

- **BS1** — The oracle is the setup itself: since we wrote the measured source ourselves, we
  know what we wrote. The right column lists **all** calls in the class file; the reader
  drops what was written in the source, and what is left is **what the compiler added.**
- **BS2** — The measured source text is fixed and is given to the same compiler on every run;
  the numbers come from a single compiler run and are deterministic within that run.
- **BS3** — The constructor (`<init>`) is not taken into the table; members not written in
  the source are a separate matter.

```java
// Gauge.java — reads what the compiler adds to the class file
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Gauge {
    record Method(String name, int instructions, List<String> added) {}

    static List<Method> read(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("gauge");
        Files.writeString(d.resolve(className + ".java"), source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow().run(sink, sink, "-d", d.toString(),
                d.resolve(className + ".java").toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        List<Method> methods = new ArrayList<>();
        for (MethodModel m : ClassFile.of().parse(d.resolve(className + ".class")).methods()) {
            if (m.code().isEmpty()) continue;
            List<String> added = new ArrayList<>();
            int instructions = 0;
            for (CodeElement e : m.code().get()) {
                if (e instanceof Instruction) instructions++;
                if (e instanceof InvokeInstruction iv)
                    added.add(shortName(iv.owner().asInternalName()) + "." + iv.name().stringValue());
                else if (e instanceof TypeCheckInstruction t && t.opcode() == Opcode.CHECKCAST)
                    added.add("type check " + shortName(t.type().asInternalName()));
            }
            methods.add(new Method(m.methodName().stringValue(), instructions, added));
        }
        return methods;
    }

    static String shortName(String internal) { return internal.substring(internal.lastIndexOf('/') + 1); }
}
```

## The Call Assignment Adds

The measured source carries five methods. `box` binds an `int` value to an `Integer` name,
`unbox` does the reverse. `arraySum` and `listSum` write **the same loop**; the only
difference is whether what is walked is an `int[]` or a `List<Integer>`. `tally` updates a
counter with a `count` value.

```java
// Boxing.java — the trace the primitive-to-wrapper crossing leaves in the class file
public class Boxing {
    static final String SOURCE = """
        import java.util.List;
        import java.util.Map;
        class Warehouse {
            static Object box(int count) { Integer k = count; return k; }
            static int unbox(Integer k) { int count = k; return count; }
            static int arraySum(int[] counts) { int t = 0; for (int x : counts) t += x; return t; }
            static int listSum(List<Integer> counts) { int t = 0; for (int x : counts) t += x; return t; }
            static void tally(Map<String, Integer> counter, String name, int count) { counter.put(name, count); }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.printf("%-14s %6s  %s%n", "method", "instr", "call in class file");
        int total = 0;
        for (Gauge.Method y : Gauge.read(SOURCE, "Warehouse")) {
            if (y.name().equals("<init>")) continue;
            System.out.printf("%-14s %6d  %s%n", y.name(), y.instructions(),
                    y.added().isEmpty() ? "-" : String.join(", ", y.added()));
            total += y.added().size();
        }
        System.out.println("total: " + total);
    }
}
```

```
method          instr  call in class file
box                 5  Integer.valueOf
unbox               5  Integer.intValue
arraySum           24  -
listSum            20  List.iterator, Iterator.hasNext, Iterator.next, type check Integer, Integer.intValue
tally               7  Integer.valueOf, Map.put
total: 9
```

`box`'s source writes no call at all; the class file has **`Integer.valueOf`.** `unbox`
writes no call either; the class file has **`Integer.intValue`.** Autoboxing is not a
language convenience, it is a **method call** invisible in the source, and the reverse holds
too. The `tally` row shows how BS1 gets read: `Map.put` is written in the source and is
dropped; `Integer.valueOf` is not written and stays.

The bottom two rows pay off the course's second thesis. `arraySum` produces **24**
instructions and carries **no** step unwritten in the source: the enhanced `for` turns into a
counted loop over an array, and the summation is done directly on numbers. `listSum` produces
**20** instructions — fewer — but carries **five** steps not written in the source: an
iterator is taken, queried twice, the returned object is checked against `Integer`, and
opened into a number with `intValue`. **Fewer instructions, more added steps.** Instruction
count and added-step count do not move in the same direction; a measurement cannot be
compared without saying which one is being counted.

Four of these five steps sit inside the loop body, meaning they are paid per element. The
class file does not show this; what it shows is the step's **existence.** Seeing how many
times it is paid requires running it.

## Same Number of Boxings, Two Distinct Object Counts

`Integer.valueOf` does not produce a new object on every call. The specification requires
that for `int` values, boxing of **every value between -128 and 127** return **the same
object**; outside this range is left to the implementation. This has a measurable
consequence: the same number of boxings produces a distinct number of objects depending on
where the values fall.

- **BS4** — All three warehouses carry **100** records, and each record's `count` value is
  separate; the only difference is the base the values start from. In each warehouse every
  `count` is boxed **twice**, meaning all three measurements make **200** boxing calls.
- **BS5** — Distinct object count is found by gathering the produced wrappers in a map keyed
  by their identity; no object's identity value is printed, only **how many distinct** objects
  there are is counted.

```java
// Production.java — same number of boxings, two separate object counts
import java.util.IdentityHashMap;
import java.util.Map;

public class Production {
    record Item(String name, int count, long weight) {}

    static Item[] warehouse(int n, int base) {
        Item[] d = new Item[n];
        for (int i = 0; i < n; i++) d[i] = new Item("K" + i, base + i, 1000L + i);
        return d;
    }

    static int distinctObjects(Item[] warehouse) {
        Map<Integer, Boolean> identity = new IdentityHashMap<>();
        for (int round = 0; round < 2; round++)
            for (Item k : warehouse) identity.put(k.count(), Boolean.TRUE);   // every put is one valueOf
        return identity.size();
    }

    public static void main(String[] args) {
        System.out.printf("%-22s %8s %8s %10s%n", "warehouse", "boxings", "values", "distinct obj");
        for (int base : new int[] {1, 100, 1000}) {
            Item[] d = warehouse(100, base);
            System.out.printf("count %4d..%-4d %8d %8d %10d%n",
                    base, base + 99, 2 * d.length, d.length, distinctObjects(d));
        }
        Item[] big = warehouse(100, 1000);
        long t = 0;
        for (Item k : big) t += k.count();
        System.out.println("primitive sum: " + t + " | wrappers produced: 0");
    }
}
```

```
warehouse               boxings   values distinct obj
count    1..100       200      100        100
count  100..199       200      100        172
count 1000..1099      200      100        200
primitive sum: 104950 | wrappers produced: 0
```

The same code runs in all three rows, the same number of boxings is done, and the same number
of distinct values is boxed. The number of objects produced is **100**, **172**, and **200**.
Nothing in the source produces this difference; the difference is entirely a decision made by
the added step, that is, by `Integer.valueOf`.

The middle row shows the boundary directly. When `count` values spread between 100 and 199,
the 28 values below 128 come back from the cache and **one** object forms for each; the
remaining 72 values produce **two distinct** objects across the two rounds. Total 28 plus
144, that is **172.** The number itself marks a threshold.

The last row is the measure's other end. When the same warehouse is summed over `int`, no
wrapper is produced at all: the line `t += k.count()` has no boxing, because `long` and `int`
are both primitive types. The cost of using a wrapper in a loop can be that one object is
produced at every step of that loop — and this is written nowhere in the source.

## Where Identity and Value Come Apart

This difference in object production does not stay a number only; it has a **visible
consequence.** When two reference types are compared with `==`, what is tested is not their
values but **whether they are the same object.** In the cache range, two boxings give the
same object so `==` seems to give the correct result; outside the range the same code gives
the wrong result.

```java
// Cache.java — where a wrapper's identity and value come apart
public class Cache {
    record Item(String name, int count, long weight) {}

    static Item searchByIdentity(Item[] warehouse, Integer wanted) {
        for (Item k : warehouse) {
            Integer count = k.count();          // boxing: Integer.valueOf
            if (count == wanted) return k;       // two wrappers: identity comparison
        }
        return null;
    }

    static Item searchByValue(Item[] warehouse, Integer wanted) {
        for (Item k : warehouse) {
            Integer count = k.count();
            if (count.equals(wanted)) return k;  // value comparison
        }
        return null;
    }

    public static void main(String[] args) {
        Item[] warehouse = { new Item("bolt", 127, 40L), new Item("screw", 128, 90L) };
        System.out.printf("%-10s %-12s %-12s%n", "wanted", "by identity", "by value");
        for (Integer wanted : new Integer[] {127, 128}) {
            Item a = searchByIdentity(warehouse, wanted), b = searchByValue(warehouse, wanted);
            System.out.printf("count %-5d %-12s %-12s%n", wanted,
                    a == null ? "not found" : a.name(), b == null ? "not found" : b.name());
        }
        System.out.println();
        Integer a = 127, b = 127, c = 128, d = 128;
        System.out.println("Integer 127 == 127 : " + (a == b) + " | equals: " + a.equals(b));
        System.out.println("Integer 128 == 128 : " + (c == d) + " | equals: " + c.equals(d));
        Long e = 127L, f = 127L;
        Double g = 1.0, h = 1.0;
        System.out.println("Long    127 == 127 : " + (e == f) + " | equals: " + e.equals(f));
        System.out.println("Double  1.0 == 1.0 : " + (g == h) + " | equals: " + g.equals(h));
    }
}
```

```
wanted     by identity  by value    
count 127   bolt         bolt        
count 128   not found    screw       

Integer 127 == 127 : true | equals: true
Integer 128 == 128 : false | equals: true
Long    127 == 127 : true | equals: true
Double  1.0 == 1.0 : false | equals: true
```

`searchByIdentity` finds the record with count 127, **fails to find** the one with count 128.
There is no difference at all between the two searches in the source; the result changed
because the sought value went up by one. This is the clearest example of the course's fourth
reading: **the decision was given not by the programmer, but by the added step.** The same
code works correctly on small warehouses, incorrectly on large ones, and the compiler
produces no warning.

The bottom table draws the boundary. For `Integer` and `Long`, 127 is equal by identity, 128
is not; for `Double`, even 1.0 is not equal, because fractional wrappers carry no cache. The
`equals` column is **correct in all four rows**: value comparison is not affected by which
object boxing returns. The rule follows from here — wrappers are compared with `equals`, not
`==`; `==` is a value comparison only for primitive types.

The same observation also gives the bounding measurement: there is a range where this
lesson's measured added step has **no** effect on identity at all. Between -128 and 127,
boxing produces no new object, `==` behaves as expected, and object count equals value count.
The added step's visibility depends on which range the value falls into.

## Summary

- The crossing between a primitive type and its wrapper is an equals sign in the source, a
  call in the class file: boxing adds **`Integer.valueOf`**, unboxing adds
  **`Integer.intValue`.**
- The same loop produces **24** instructions and **0** added steps over an array, **20**
  instructions and **5** added steps over `List<Integer>`; fewer instructions, more added
  steps.
- `Integer.valueOf` does not produce a new object on every call: the same 200 boxings leave
  **100**, **172**, or **200** distinct objects depending on the values' range.
- The specification requires the same object to be returned between -128 and 127; `==` looks
  correct in this range, gives the wrong result outside it, and the compiler produces no
  warning.
- Wrappers are compared with `equals`; `equals` gave the correct result in all four
  measurements, `==` is a value comparison only for primitive types.

## Next Step

This lesson measured which form a value is held in: the number itself, or an object wrapping
the number. What is left is the name itself. The next lesson writes the same number of names
into three separate places — inside a method, on an instance, and on the class itself — and
counts where these three names land in the class file: which one is written into a **slot**,
which one into a **field**, why a name opened inside a block never shows up as a name at all
in the class file, and why redeclaring the same name in an inner block gives a compile error.
