Senior 6 min · March 06, 2026
Autoboxing and Unboxing in Java

Java Integer == Fails at 128 — The Cache Boundary Bug

Integer comparison returns false for 200 but true for 50.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Autoboxing = compiler automatically converts primitive to wrapper (int → Integer) via Integer.valueOf()
  • Unboxing = compiler automatically converts wrapper to primitive (Integer → int) via intValue()
  • Integer cache: values -128 to 127 return cached objects — == works for 100, fails for 200
  • Performance: Using Long in a loop instead of long creates heap objects each iteration = 5-10x slower + GC pressure
  • Production trap: Unboxing null Integer → NullPointerException on hidden intValue() call, stack trace points to innocent-looking arithmetic
  • Biggest mistake: Using == to compare Integer objects — it works for small numbers (cache) and fails for large numbers (new objects), making the bug intermittent
✦ Definition~90s read
What is Autoboxing and Unboxing in Java?

Autoboxing and unboxing are Java's automatic conversions between primitive types (int, double, boolean) and their wrapper classes (Integer, Double, Boolean). Introduced in Java 5, this feature exists to bridge the gap between Java's object-oriented collections (which only store objects) and its performance-critical primitives.

Imagine you have a coin (a primitive int) and a coin purse (an Integer object).

When you write Integer x = 42, the compiler silently inserts a call to Integer.valueOf(42) — and that's where the trouble starts. The Integer.valueOf() method caches values from -128 to 127 by default, meaning Integer a = 100; Integer b = 100; a == b returns true, but Integer c = 128; Integer d = 128; c == d returns false.

This cache boundary bug has burned countless developers who assume == compares values rather than object references. The fix is simple: always use .equals() for wrapper comparisons, or better yet, stick to primitives where possible. The performance implications are equally brutal — autoboxing in tight loops creates unnecessary object allocations that can tank throughput by orders of magnitude, and unboxing a null reference throws a NullPointerException that's notoriously hard to trace.

Understanding this mechanism is essential for anyone writing Java that touches collections, generics, or any code where primitives and objects mix.

Plain-English First

Imagine you have a coin (a primitive int) and a coin purse (an Integer object). Sometimes a shop only accepts purses, not loose coins. Autoboxing is Java automatically dropping your coin into a purse before handing it over. Unboxing is Java taking the coin back out when you need raw math. You never see it happen — Java just does it quietly behind the scenes. The catch? Opening and closing purses costs a tiny bit of effort, and if the purse is empty (null), handing it over causes a crash.

Every Java developer writes code that mixes primitive types and their object counterparts constantly — passing an int into an ArrayList, returning an Integer from a method, or comparing values with ==. For years before Java 5, you had to manually convert between them, writing verbose boilerplate that cluttered your logic and made bugs easier to hide. Autoboxing changed that, and understanding it deeply separates developers who write clean, performant code from those who wonder why their app slows down or throws a NullPointerException out of nowhere.

The problem autoboxing solves is the fundamental tension at the heart of Java: primitives (int, double, boolean, etc.) are fast and live on the stack, but the Collections framework and generics only work with objects. You can't put an int into a List<Integer> directly — the JVM needs a full-fledged object with a memory address. Autoboxing bridges that gap automatically, letting the compiler handle the conversion so your code stays readable without you thinking about it every single time.

After reading this article you'll understand exactly what autoboxing and unboxing are, why they exist, how the JVM handles the conversion under the hood (including the integer cache you've probably never heard of), and the three real mistakes that trip up even experienced developers. You'll also walk away with solid answers to the interview questions that actually get asked about this topic.

What Autoboxing and Unboxing Actually Are (Under the Hood)

Java has eight primitive types: byte, short, int, long, float, double, char, and boolean. Each one has a corresponding wrapper class in java.lang — Integer, Double, Boolean, etc. These wrapper classes turn a primitive into a full Java object, which means it can be stored in collections, used with generics, passed where Object is expected, and set to null.

Autoboxing is the compiler automatically calling Integer.valueOf(int) for you when a primitive is used where an object is expected. Unboxing is the compiler automatically calling intValue() (or the equivalent) when an object is used where a primitive is expected. This all happens at compile time — the compiler inserts the conversion calls into the bytecode so the JVM never sees the raw conversion gap.

This isn't magic and it isn't free. Every autoboxed value allocates a new object on the heap (with one important exception we'll cover). Knowing that the compiler is secretly inserting method calls lets you predict performance, understand NullPointerExceptions, and reason about == comparisons that behave in surprising ways.

io/thecodeforge/java/AutoboxingBasics.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.thecodeforge.java;

public class AutoboxingBasics {

    public static void main(String[] args) {

        // --- AUTOBOXING ---
        // The compiler rewrites this line as: Integer boxedScore = Integer.valueOf(42);
        // You write a primitive literal, Java wraps it into an Integer object.
        Integer boxedScore = 42;

        // --- UNBOXING ---
        // The compiler rewrites this as: int rawScore = boxedScore.intValue();
        // You use an Integer where a primitive is needed, Java unwraps it.
        int rawScore = boxedScore;

        // --- AUTOBOXING IN A COLLECTION ---
        // ArrayList only holds objects, not primitives.
        // Each add() call autoboxes the int literal into an Integer object.
        java.util.List<Integer> playerScores = new java.util.ArrayList<>();
        playerScores.add(100);  // compiler inserts: Integer.valueOf(100)
        playerScores.add(250);  // compiler inserts: Integer.valueOf(250)
        playerScores.add(75);   // compiler inserts: Integer.valueOf(75)

        // --- UNBOXING IN ARITHMETIC ---
        // get() returns an Integer object, but + operator needs primitives.
        // The compiler inserts .intValue() calls on both operands automatically.
        int totalScore = playerScores.get(0) + playerScores.get(1);

        System.out.println("Boxed score (Integer object): " + boxedScore);
        System.out.println("Unboxed score (int primitive): " + rawScore);
        System.out.println("Total of first two scores: " + totalScore);

        // --- WHAT THE COMPILER ACTUALLY GENERATES ---
        // You can verify this by compiling and running: javap -c AutoboxingBasics.class
        // You'll see invokevirtual calls to Integer.valueOf and Integer.intValue
        System.out.println("\nAll scores in list: " + playerScores);
    }
}
Verify It Yourself:
Compile any class that uses autoboxing, then run 'javap -c YourClass.class' in the terminal. You'll literally see the Integer.valueOf() and intValue() calls the compiler inserted. This makes the hidden mechanism completely visible and kills any doubt about what's happening.
Production Insight
Integer.valueOf() uses the integer cache for values -128 to 127. For values outside this range, it creates a new object every call.
This is a flyweight pattern optimisation. The cache size can be increased with -XX:AutoBoxCacheMax=<size> but doing so is rare.
Rule: Never rely on identity (==) for wrapper objects, even if you think they're from the cache. The JVM flag can change cache size, breaking assumptions.
Key Takeaway
Autoboxing is the compiler inserting Integer.valueOf() calls; unboxing is the compiler inserting .intValue() calls — it's not runtime magic, it's compile-time code generation you can verify with javap.
Every autoboxed value (outside the cache) allocates a new object on the heap. This is not free.
Rule: Use primitives for local math and accumulators. Use wrappers only when the API requires an object or null is meaningful.
Primitive vs Wrapper Selection
IfLocal variable, loop counter, or accumulator
UseUse primitive. Faster, no heap allocation, no GC pressure. Example: int sum = 0; not Integer sum = 0;
IfCollection element (List<Integer>, Map<String, Integer>)
UseMust use wrapper. Collections require objects. Autoboxing handles conversion automatically at insertion/extraction.
IfReturn value may be null (meaning 'not found')
UseUse wrapper (Integer, Long, etc.). Primitives cannot represent absence. Example: public Integer findScore(String name) returning null if not found.
IfJSON/ORM mapping field (database column could be NULL)
UseUse wrapper. Database null maps to Java null. Primitive would map to 0, losing the distinction between 'zero' and 'null'.
IfMethod called millions of times, performance critical
UseUse primitive for parameters and return types. Avoid boxed types in hot paths. But beware of null requirements.
Java Integer Cache Boundary Bug THECODEFORGE.IO Java Integer Cache Boundary Bug Autoboxing, caching, and == comparison pitfalls Autoboxing & Unboxing Primitive ↔ wrapper conversion Integer Cache (-128 to 127) Cached objects for small values == Comparison at 128 Fails due to different objects Null Unboxing NPE Null wrapper → NullPointerException Generic Collections Autoboxing Hidden object creation in loops Use .equals() or int Safe comparison or primitive ⚠ == on Integer outside cache range gives false Always use .equals() for wrapper comparison THECODEFORGE.IO
thecodeforge.io
Java Integer Cache Boundary Bug
Autoboxing Unboxing Java

The Integer Cache — Why == Comparisons Will Lie to Your Face

Here's the thing that trips up even senior developers: Java caches Integer objects for values between -128 and 127. This means Integer.valueOf(100) returns the exact same object every time — not a new one. It's a JVM optimization built into the spec because small integers are used so frequently that creating millions of identical tiny objects would be wasteful.

The consequence is bizarre. Comparing two autoboxed Integer values with == works correctly for small numbers but silently fails for larger ones, because == on objects compares memory addresses, not values. Two Integer objects holding 200 are different objects at different addresses, so == returns false even though they hold the same number.

This is one of the most famous Java interview questions for good reason — it looks like a bug in the language but it's actually documented, intentional behavior. The fix is simple: always use .equals() to compare Integer objects. Never use == unless you specifically want to check object identity. The same cache behavior applies to Short, Byte, Character (0–127), and Boolean (both cached).

io/thecodeforge/java/IntegerCacheDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package io.thecodeforge.java;

public class IntegerCacheDemo {

    public static void main(String[] args) {

        // --- VALUES WITHIN THE CACHE RANGE: -128 to 127 ---
        // Integer.valueOf(100) returns the SAME cached object both times.
        // So == compares the same memory address and returns true.
        Integer firstSmallNumber = 100;   // autoboxed via Integer.valueOf(100)
        Integer secondSmallNumber = 100;  // returns the SAME cached object

        System.out.println("=== Within cache range (100) ===");
        System.out.println("firstSmallNumber == secondSmallNumber : "
                + (firstSmallNumber == secondSmallNumber));   // true — SAME object
        System.out.println("firstSmallNumber.equals(secondSmallNumber): "
                + firstSmallNumber.equals(secondSmallNumber)); // true — same value

        // --- VALUES OUTSIDE THE CACHE RANGE ---
        // Integer.valueOf(200) creates a NEW object every time.
        // So == compares DIFFERENT memory addresses and returns false.
        Integer firstLargeNumber = 200;   // new Integer object created
        Integer secondLargeNumber = 200;  // ANOTHER new Integer object created

        System.out.println("\n=== Outside cache range (200) ===");
        System.out.println("firstLargeNumber == secondLargeNumber : "
                + (firstLargeNumber == secondLargeNumber));   // false — DIFFERENT objects!
        System.out.println("firstLargeNumber.equals(secondLargeNumber): "
                + firstLargeNumber.equals(secondLargeNumber)); // true — same value

        // --- THE SAFE APPROACH: always use .equals() for wrapper comparisons ---
        Integer playerLevel = 250;
        Integer targetLevel = 250;

        // WRONG way — will fail silently for values outside the cache
        if (playerLevel == targetLevel) {
            System.out.println("\n[WRONG CHECK] Levels match (== used — unreliable!)");
        } else {
            System.out.println("\n[WRONG CHECK] Levels do NOT match (== used — lied to us!)");
        }

        // RIGHT way — always use .equals() to compare wrapper object values
        if (playerLevel.equals(targetLevel)) {
            System.out.println("[CORRECT CHECK] Levels match (.equals() used — reliable)");
        }
    }
}
Watch Out:
The integer cache range (-128 to 127) can actually be extended with the JVM flag -XX:AutoBoxCacheMax=<size>. This means code that 'works' in your environment might silently break in a deployment with different JVM settings if you're using == to compare Integers. Don't rely on == for Integer comparisons. Ever.
Production Insight
The integer cache is a performance optimisation that becomes a correctness trap when developers use == for equality.
The cache range is -128 to 127 by default, but can be changed with -XX:AutoBoxCacheMax. Some frameworks (e.g., some application servers) increase this value, making the bug even more subtle.
Rule: SpotBugs rule Dm: Use of == to compare Integer objects should be treated as an error in CI, not a warning.
Key Takeaway
Never use == to compare Integer, Long, Short, Byte, Character, or Boolean objects — the integer cache makes it return true for values between -128 and 127 but false for larger values, creating a bug that only appears with certain inputs.
The safe approach: always use .equals() for wrapper comparisons.
Rule: If performance is critical, unbox to primitive after null check and compare with == on primitives.

Performance Pitfalls — When Autoboxing Quietly Kills Your Loop

Autoboxing feels invisible, but it isn't free. Each conversion creates a heap object (except cached values), which means more garbage for the GC to collect. In a tight loop that runs thousands or millions of times, unnecessary autoboxing can turn a fast operation into a slow one without a single obvious line of code to blame.

The classic trap is accidentally using a wrapper type as an accumulator in a loop. If you declare Long totalRevenue instead of long totalRevenue, every single addition operation unboxes the Long, adds the primitive, then autoboxes the result back into a new Long object. A loop running a million times creates a million short-lived objects the GC must track and collect.

The rule of thumb is simple: use primitives for local variables and computation. Use wrapper types only when the API requires it — for collections, generics, method signatures that return null as a 'no value' signal, or database/JSON mapping where null is meaningful. This distinction is what modern Java engineers mean when they talk about being intentional with types.

io/thecodeforge/java/AutoboxingPerformance.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package io.thecodeforge.java;

public class AutoboxingPerformance {

    private static final int TRANSACTION_COUNT = 1_000_000;

    public static void main(String[] args) {

        // --- SLOW VERSION: wrapper type used as accumulator ---
        // Every += operation does THREE things:
        //   1. Unbox: totalRevenueSlow.longValue()
        //   2. Add:   result = longValue + nextAmount
        //   3. Rebox: totalRevenueSlow = Long.valueOf(result)
        // This creates 1,000,000 temporary Long objects on the heap.
        Long totalRevenueSlow = 0L;
        long startSlow = System.nanoTime();

        for (int transactionIndex = 0; transactionIndex < TRANSACTION_COUNT; transactionIndex++) {
            totalRevenueSlow += 1L; // hidden autobox/unbox on EVERY iteration
        }

        long durationSlowMs = (System.nanoTime() - startSlow) / 1_000_000;

        // --- FAST VERSION: primitive used as accumulator ---
        // No objects created. Pure stack arithmetic. GC never involved.
        long totalRevenueFast = 0L;
        long startFast = System.nanoTime();

        for (int transactionIndex = 0; transactionIndex < TRANSACTION_COUNT; transactionIndex++) {
            totalRevenueFast += 1L; // simple primitive addition — no boxing
        }

        long durationFastMs = (System.nanoTime() - startFast) / 1_000_000;

        // --- RESULTS ---
        System.out.println("=== Performance Comparison ===");
        System.out.println("Transactions processed: " + TRANSACTION_COUNT);
        System.out.println("Slow (Long accumulator):  " + durationSlowMs + " ms");
        System.out.println("Fast (long accumulator):  " + durationFastMs + " ms");
        System.out.println("Both totals match: " + totalRevenueSlow.equals(totalRevenueFast));
        System.out.println();
        System.out.println("Tip: The difference grows as TRANSACTION_COUNT grows.");
        System.out.println("In a financial system processing millions of records,");
        System.out.println("this single type choice has real consequences.");
    }
}
Real-World Context:
This exact performance issue has been found in production Java systems — it shows up in profilers as excessive GC pressure with thousands of short-lived Integer/Long allocations. Tools like JProfiler or async-profiler will point directly at the boxing site. If your app's GC logs show high allocation rates, a misused wrapper type in a loop is one of the first things to check.
Production Insight
The slow version with Long totalRevenueSlow = 0L creates 1,000,000 Long objects for 1M iterations. The fast version with long totalRevenueFast creates zero objects.
The memory difference: 1M * 24 bytes (Integer object overhead + int) = 24MB allocated, then GC'd. The CPU difference: boxing/unboxing overhead + GC pauses.
Rule: Profile before optimising, but default to primitives for any variable that participates in arithmetic or loops. Wrappers only for collections, nullability, or generics.
Key Takeaway
Using a wrapper type as a loop accumulator (Long instead of long) triggers autoboxing on every iteration and can make a tight loop 5–10x slower due to heap allocations and GC pressure — always use primitives for local math.
The hidden cost: each autoboxing operation allocates memory, which eventually requires GC. In high-throughput systems, this is a performance killer.
Rule: For any variable that is updated in a loop or used in arithmetic, use primitive types. Wrappers are for collections and nullability.

The NullPointerException Nobody Expects — Null Unboxing in Practice

Here's the sneakiest autoboxing bug: unboxing a null wrapper causes a NullPointerException that looks like it came from nowhere. When Java tries to call .intValue() on a null Integer reference, it throws. The stack trace points at a line that looks like plain arithmetic or a simple variable assignment — no obvious null check, no object method call in your code. That's exactly why it's so confusing.

This pattern appears constantly in real code. A method returns Integer (with null meaning 'no data found'). The caller assigns it to an int variable. Boom. Or a Map lookup returns null for a missing key, and the result is immediately used in a calculation.

The fix isn't to avoid nullable wrappers — they're genuinely useful for signalling absence of a value. The fix is to always validate before unboxing when the wrapper could be null. Use null checks, Optional<Integer>, or provide a default with Objects.requireNonNullElse(). Understanding that unboxing is a hidden method call is the mental model that makes these bugs obvious before they bite you.

io/thecodeforge/java/NullUnboxingDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package io.thecodeforge.java;

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

public class NullUnboxingDemo {

    // Simulates a database lookup — returns null when the player doesn't exist
    static Integer getPlayerHighScore(String playerName) {
        Map<String, Integer> scoreDatabase = new HashMap<>();
        scoreDatabase.put("alice", 4500);
        scoreDatabase.put("bob", 3200);
        // Note: "charlie" is not in the database — lookup returns null
        return scoreDatabase.get(playerName);
    }

    public static void main(String[] args) {

        // --- THE HIDDEN NPE TRAP ---
        // getPlayerHighScore returns Integer (nullable).
        // Assigning to int triggers unboxing: null.intValue() — NPE!
        try {
            int charlieScore = getPlayerHighScore("charlie"); // null gets unboxed here
            System.out.println("Charlie's score: " + charlieScore);
        } catch (NullPointerException npe) {
            System.out.println("[CAUGHT] NPE from unboxing null Integer!");
            System.out.println("The line looked safe but null.intValue() was called.");
        }

        // --- FIX 1: null check before unboxing ---
        Integer charlieRawScore = getPlayerHighScore("charlie");
        if (charlieRawScore != null) {
            int safeScore = charlieRawScore; // safe to unbox — we know it's not null
            System.out.println("\n[FIX 1] Charlie's score: " + safeScore);
        } else {
            System.out.println("\n[FIX 1] Charlie has no recorded score yet.");
        }

        // --- FIX 2: provide a default value using Objects.requireNonNullElse ---
        Integer aliceRawScore = getPlayerHighScore("alice");
        int aliceScore = Objects.requireNonNullElse(aliceRawScore, 0); // 0 if null
        System.out.println("[FIX 2] Alice's score (default 0 if absent): " + aliceScore);

        int unknownPlayerScore = Objects.requireNonNullElse(
                getPlayerHighScore("dave"), 0);
        System.out.println("[FIX 2] Dave's score (default 0 if absent): " + unknownPlayerScore);
    }
}
Watch Out:
In Java 14+, the enhanced NullPointerException message will tell you exactly which variable was null ('Cannot unbox a null value'). But in older codebases running Java 8 or 11, you just get 'NullPointerException' with no detail. Knowing that unboxing is the culprit saves you from 20 minutes of confused staring at code that looks correct.
Production Insight
Null unboxing is responsible for many NPEs that seem to come from 'impossible' lines (e.g., int total = calculatePrice() + calculateTax() where both methods return Integer).
The stack trace points to the line, but the actual source of null is the wrapper returned from a method.
Rule: Always trace null to its source: any method returning a wrapper type (Integer, Long, etc.) could return null. Validate before unboxing.
Key Takeaway
Unboxing null always throws NullPointerException at the hidden .intValue() call — the stack trace points at your arithmetic line, not at the source of the null, so the fix is to validate before unboxing any wrapper that could be null.
Prevention: Use Objects.requireNonNullElse(wrapper, defaultValue) before unboxing, or use Optional to make nullability explicit.
Rule: If a method returns a wrapper type, treat it as nullable. Check before unboxing, or use primitive type as return value if null is not meaningful.

Java Primitive Types and Their Wrapper Classes — The Cheat Sheet You'll Memorise

Every autoboxing and unboxing disaster traces back to one thing: not knowing which primitive maps to which wrapper. The compiler hides this, but the JVM doesn't forgive ignorance.

There are exactly eight primitive types in Java, each with a corresponding wrapper class in java.lang. Boolean wraps boolean, Byte wraps byte, Short wraps short, Character wraps char, Integer wraps int, Long wraps long, Float wraps float, Double wraps double. That's it. No more. No less.

Notice the pattern: primitive names are lowercase, wrappers start with a capital. Except for char → Character and int → Integer. Those two are the ones that burn juniors who try to guess the naming convention.

When the compiler inserts autoboxing or unboxing, it's calling valueOf() and xxxValue() behind the scenes. Knowing these pairs by heart means you catch implicit conversions before they hit production. Every performance issue with autoboxing starts with someone treating an int like an Integer in a tight loop. Map the types, avoid the trap.

WrapperMapping.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — java tutorial

public class WrapperMapping {
    public static void main(String[] args) {
        // Autoboxing: primitive -> wrapper via valueOf()
        Integer wrapped = 42;               // int → Integer
        Boolean boolWrapped = true;          // boolean → Boolean
        Character charWrapped = 'X';         // char → Character (not Char)

        // Unboxing: wrapper -> primitive via xxxValue()
        int unwrapped = wrapped;             // Integer → int
        boolean boolUnwrapped = boolWrapped;  // Boolean → boolean

        // The two that break naming conventions
        System.out.println(int.class.getName());      // int
        System.out.println(Integer.class.getName());  // java.lang.Integer
        System.out.println(char.class.getName());     // char
        System.out.println(Character.class.getName()); // java.lang.Character
    }
}
Output
int
java.lang.Integer
char
java.lang.Character
Senior Shortcut:
Memorise the eight pairs: boolean/Boolean, byte/Byte, short/Short, char/Character, int/Integer, long/Long, float/Float, double/Double. The two capitalisation exceptions (int→Integer, char→Character) cause more bugs than anything else.
Key Takeaway
Always know which primitive and wrapper pair you're working with—autoboxing hides the conversion, not the cost.

The Generic Generational Trap — Autoboxing in Collections You Never Wrote

Here's where autoboxing slaps you when you least expect it: generics. The Java compiler forces collections to use wrapper types. You write List<Integer>, you push ints into it, and the compiler silently calls Integer.valueOf() for every single element.

This isn't just about your code. It's about the code you never wrote. Third-party libraries that return List<Integer> when you wanted int[]? You're paying autoboxing tax on every access. Sorting? Iterating? Each get() call triggers an implicit unboxing. Each put() triggers an autobox.

The real grief shows up in maps. Try HashMap<Integer, Double> in a data processing pipeline that runs over a million entries. You're allocating an Integer and a Double object—two heap allocations—every time you insert. The GC will remind you why you should have used a primitive collection library or a plain array.

Production lesson: if your collection size exceeds 10,000 elements and you see GC pressure, profile it. Chances are autoboxing is drowning your heap in short-lived wrapper objects. Trove, Eclipse Collections, or plain arrays exist for a reason. Use them.

AutoboxingInCollections.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — java tutorial

import java.util.*;

public class AutoboxingInCollections {
    public static void main(String[] args) {
        // Hidden autoboxing: int to Integer, double to Double
        Map<String, Double> priceMap = new HashMap<>();
        priceMap.put("server", 1499.99);   // boxed twice: String and Double
        
        // Unboxing on every get
        Double rawPrice = priceMap.get("server");  // gets Double object
        double actualPrice = rawPrice;              // unboxing calls doubleValue()
        
        System.out.println("Price: " + actualPrice);
        
        // The expensive part: bulk operations with autoboxing
        List<Integer> ids = new ArrayList<>();
        for (int i = 0; i < 10_000; i++) {
            ids.add(i);   // 10,000 Integer objects created on heap
        }
        long sum = 0;
        for (int id : ids) {  // each iteration unboxes intValue()
            sum += id;
        }
        System.out.println("Sum: " + sum);
    }
}
Output
Price: 1499.99
Sum: 49995000
Production Trap:
List<Integer> + for-each loop + 100,000+ elements = GC nightmare. Each iteration allocates an Integer on add and unboxes it on get. Switch to int[] or a primitive-backed collection library for tight loops.
Key Takeaway
Generics force wrapper types—every collection access may auto-box/unbox thousands of times. Profile before you trust the compiler.
● Production incidentPOST-MORTEMseverity: high

The Intermittent Integer Comparison That Corrupted Financial Reports

Symptom
Transaction reports showed mismatched totals for high-volume accounts. Detailed logging showed that two Integer variables, each containing 200, were being compared with == and returning false. The same code compared small values (e.g., 50) correctly. The team saw the failure only on accounts with >127 transactions. The bug was intermittent by value, not by timing.
Assumption
The team assumed Integer comparison with == worked because they'd tested with small numbers and it passed. They didn't know about the integer cache. They also assumed that since both Integers came from the same source (autoboxing of ints from a database query), they would be the same object. They didn't know that Integer.valueOf(200) creates a new object each time.
Root cause
The code was: if (storedCount == currentCount) { ... } where both were Integer objects from different map lookups. For values between -128 and 127, JVM reuses cached objects, so == works. For values 128 and above, JVM creates new objects each time. The comparison failed because the two Integer objects were different heap objects with the same value. The bug was completely invisible until a day with 200+ transactions occurred. The reconciliation logic used == for equality, leading to incorrect matches and corrupt reports.
Fix
1. Changed all Integer comparisons to .equals(): if (storedCount.equals(currentCount)). 2. Added a project-wide lint rule: -Xlint:unchecked and SpotBugs rule Dm: Use of == to compare Integer objects. 3. For high-performance paths, unboxed to primitive int before comparison: int stored = storedCount; int current = currentCount; if (stored == current) (null-checked first). 4. Updated the team's coding standards: 'Never use == to compare wrapper objects. Always use .equals().'
Key lesson
  • The integer cache causes == to work for -128..127 and fail for other values. This is the most common autoboxing bug in production Java code.
  • Never use == to compare Integer, Long, Short, Byte, Character, or Boolean objects. Always use .equals().
  • SpotBugs and IntelliJ inspection warnings about 'Boxed value comparison' are not noise — they catch real bugs.
  • If performance is critical, unbox to primitive after null check: int a = integerA; int b = integerB; if (a == b)
Production debug guideSymptom → Action mapping for common autoboxing failures in production Java applications.5 entries
Symptom · 01
Integer comparison works sometimes, fails other times — same values, same method
Fix
Integer cache. Values <=127 work, >=128 fail. Check if values cross the 127 boundary. Replace == with .equals().
Symptom · 02
NullPointerException on a line with arithmetic (e.g., int total = price + tax)
Fix
Either price or tax is an Integer that is null. Unboxing called intValue() on null. Add null checks before unboxing: if (price != null && tax != null)
Symptom · 03
High GC pressure in a method with simple arithmetic or loops
Fix
You're using wrapper types as loop accumulators (e.g., Long total = 0L inside loop). Change to primitives: long total = 0L. Profile with JFR to see allocation spikes.
Symptom · 04
Map.get(key) returns null but code assigns to primitive — NPE
Fix
Map.get returns Integer which is null. Assignment to int triggers unboxing. Use Integer result = map.get(key); if (result != null) { int val = result; } or int val = map.getOrDefault(key, 0);
Symptom · 05
Performance regression after migrating from int[] to List<Integer>
Fix
Autoboxing overhead: each element access boxes/unboxes. For large lists, consider using IntArrayList from Eclipse Collections, or stick with int[]. Profile with async-profiler to confirm allocation rate.
★ Java Autoboxing Debug Cheat SheetFast diagnostics for autoboxing issues in production Java applications.
== works sometimes, fails others — integer cache suspicion
Immediate action
Check if values cross the 127 boundary
Commands
javap -c YourClass.class | grep -A10 'if_icmpne\|if_acmpne'
echo 'System.out.println(IntegerCache.high);' | jshell -
Fix now
Replace if (a == b) with if (a.equals(b)). For primitive comparison, unbox first: if (a != null && b != null && a.intValue() == b.intValue())
NullPointerException on arithmetic line — null unboxing+
Immediate action
Check if operands are wrapper types (Integer, Long, Double) that could be null
Commands
javap -c YourClass.class | grep -B5 'invokevirtual.*intValue'
grep -n 'Integer\|Long\|Double' src/ | grep -v '<' | grep -v '>'
Fix now
Add null checks: if (price != null && tax != null) { int total = price + tax; }. For Map.get, use map.getOrDefault(key, 0) or Objects.requireNonNullElse(map.get(key), 0)
Performance slow — high GC from boxing+
Immediate action
Check for wrapper types used as loop accumulators
Commands
grep -n 'for.*Long\|for.*Integer' src/
jcmd <pid> GC.heap_info | grep -A5 'Integer\|Long'
Fix now
Change Long total = 0L to long total = 0L. Change Integer sum = 0 to int sum = 0. Use primitives for loop variables and accumulators.
API returns Integer but caller treats as primitive — NPE+
Immediate action
Check method signature: returns Integer, but caller assigns to int
Commands
grep -n 'public.*Integer' src/ | grep -v 'Integer.*,.*\['
grep -n 'int\s\+\w\+\s*=\s*\w+\.get' src/
Fix now
Change caller to handle null: Integer result = service.getValue(); if (result != null) { int val = result; }. Or change API to return Optional<Integer> or primitive with sentinel (-1).
Large List<Integer> causing memory bloat — 4x expected+
Immediate action
Check memory usage: Integer object overhead vs int primitive
Commands
jmap -histo <pid> | grep -E '\[I\|java.lang.Integer'
echo 'Integer object: 16 bytes header + 4 bytes int = 20 bytes; int[] = 4 bytes per element'
Fix now
Replace List<Integer> with int[] for large collections. Use IntArrayList from Eclipse Collections, IntList from FastUtil, or IntBuffer from Java NIO.
Primitive vs Wrapper (Integer, Long, etc.)
AspectPrimitive (int, long, etc.)Wrapper Class (Integer, Long, etc.)
Memory locationStack (fast access, no GC)Heap (requires GC, object overhead)
Memory per value (int/Integer)4 bytes16 bytes (object header) + 4 bytes int = 20 bytes typical (~16 with compressed oops)
Can be nullNo — always has a valueYes — null means 'no value'
Use in Collections/GenericsNot allowed directly (except via arrays)Required (List<Integer>, Map<String, Integer>)
Comparison operator ==Compares value — always correctCompares object identity — unreliable, use .equals()
Performance in loopsFast — no allocationsSlower — creates heap objects each conversion (except cache)
Default value for fields0, false, 0.0 (type dependent)null (can cause NPE on unbox)
Methods availableNone — it's just a valueparseInt(), valueOf(), compareTo(), toString(), etc.
Autoboxing overheadNoneInteger.valueOf() call (cached for -128..127, new object otherwise)
When to choose itLocal vars, counters, math-heavy code, performance-critical pathsCollections, nullable fields, API return types, ORM/JPA entities

Key takeaways

1
Autoboxing is the compiler inserting Integer.valueOf() calls; unboxing is the compiler inserting .intValue() calls
it's not runtime magic, it's compile-time code generation you can verify with javap.
2
Never use == to compare Integer, Long, or other wrapper objects
the integer cache makes it return true for values between -128 and 127 but false for larger values, creating a bug that only appears with certain inputs.
3
Using a wrapper type as a loop accumulator (Long instead of long) triggers autoboxing on every iteration and can make a tight loop 5–10x slower due to heap allocations and GC pressure
always use primitives for local math.
4
Unboxing null always throws NullPointerException at the hidden .intValue() call
the stack trace points at your arithmetic line, not at the source of the null, so the fix is to validate before unboxing any wrapper that could be null.
5
Primitives live on the stack (fast), wrappers live on the heap (slower, GC cost). Choose primitives unless you need collection storage, nullability, or generics compatibility.

Common mistakes to avoid

5 patterns
×

Using == to compare Integer objects

Symptom
Two Integer variables holding 200 return false with ==, breaking logic for values >127. Works for small numbers (cache range), fails for larger numbers. The bug is intermittent based on value, not timing.
Fix
Always use .equals() when comparing wrapper objects. For performance, unbox first: if (a != null && b != null && a.intValue() == b.intValue()). Add SpotBugs rule to block == on boxed types in CI.
×

Declaring a wrapper type as a loop accumulator (e.g., `Long total = 0L`)

Symptom
Loop that should be fast becomes 5-10x slower. GC logs show high allocation rates. Profiler shows Integer.valueOf() as a hotspot.
Fix
Use primitive type: long total = 0L. The wrapper is not needed. Only use wrapper if null is meaningful or the collection requires it.
×

Unboxing a wrapper returned from a Map or database call without a null check

Symptom
NullPointerException on a line that looks like simple arithmetic or variable assignment. The stack trace doesn't show .intValue() call (hidden by compiler).
Fix
Always check for null before unboxing: Integer result = map.get(key); if (result != null) { int val = result; }. For Map, use map.getOrDefault(key, 0) if default value acceptable. Use Objects.requireNonNullElse(wrapper, defaultValue) for nullable wrappers.
×

Using Integer where primitive would suffice in a hot path

Symptom
High CPU in boxing/unboxing operations, high GC allocation rate. Method called millions of times experiences unnecessary heap churn.
Fix
Change method signatures and local variables to primitive types. Return int not Integer if null is not meaningful. Use int fields in performance-critical classes, not Integer.
×

Assuming Integer cache size is always -128..127

Symptom
Code that uses == for Integer comparison passes all tests (small values) but fails in production when values exceed 127. Also fails in environments where -XX:AutoBoxCacheMax is increased.
Fix
Never rely on identity (==) for wrapper objects regardless of value. Always use .equals(). The cache range is an implementation detail, not a contract.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the integer cache in Java, and how does it affect == comparisons...
Q02SENIOR
What happens at the bytecode level when you write 'int x = someIntegerOb...
Q03SENIOR
You have a loop that runs 10 million times and accumulates a sum. Someon...
Q04SENIOR
Why does Automatic (un)boxing exist in Java? Explain the trade-offs betw...
Q01 of 04SENIOR

What is the integer cache in Java, and how does it affect == comparisons between autoboxed Integer values? Can you show an example where == gives different results for Integer variables holding 100 vs 200?

ANSWER
Java caches Integer objects for values between -128 and 127 (inclusive). This is an optimisation in the JVM's Integer.valueOf() method. When you autobox an int within this range, the same cached object is returned. For values outside this range, a new Integer object is created each time. Example: Integer a = 100; Integer b = 100; a == b returns true because both reference the same cached object. Integer c = 200; Integer d = 200; c == d returns false because they are two different objects on the heap. Using == compares object references (memory addresses), not the numeric values. This is why you should always use .equals() to compare the values of wrapper objects. The cache range can be extended with the JVM flag -XX:AutoBoxCacheMax, but relying on this is fragile.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Why does Java need autoboxing at all? Why not just make collections accept primitives?
02
Is autoboxing slow? Should I avoid it everywhere?
03
What is the difference between Integer.valueOf() and new Integer()? Which does autoboxing use?
04
How do I avoid NullPointerException when unboxing values from a Map?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Java Basics. Mark it forged?

6 min read · try the examples if you haven't

Previous
Java Wrapper Classes
13 / 13 · Java Basics
Next
if-else Statement in Java