Recently, while chatting with an Expert here at MELI (Mercado Livre), I asked more about his past experiences, curious about the problems he’s tackled. That’s when he shared a particular scenario with me that piqued my interest: the impact of boxing and unboxing in poorly implemented algorithms. I won’t share his real case due to NDA reasons. However, I went off to study this topic myself, and now I’m bringing what I’ve learned in this post for you all.
If you work with Java, you’ve dealt with collections and autoboxing, but are you aware of the hidden cost of these automatic conversions? I hope you enjoy the content, and all feedback is welcome—thanks!
First, What Are Boxing and Unboxing?
In Java, boxing is the automatic conversion of a primitive type (e.g., int
) into its wrapper equivalent (e.g., Integer
). Unboxing is the reverse process. This has been around since Java 5 with autoboxing, making life easier when using APIs like ArrayList
, which only accept objects. Here’s a basic example:
int primitive = 42;
Integer wrapper = primitive; // Autoboxing
int backAgain = wrapper; // Autounboxing
It seems simple, but the problem arises when these conversions happen on a large scale or in poorly designed algorithms.
Some Terms That Came Up
The Heap is the memory area where Java allocates objects and arrays. It’s a global memory space, accessible by different threads in the application.
The Stack is a memory area used to store local variables (primitives or object references) and method execution details (parameters, scope variables, return address).
The Garbage Collector, meanwhile, is the JVM’s automatic janitor. It’s responsible for finding objects in the Heap that no longer have active references (nobody’s pointing to them anymore) and freeing up their memory for reuse.
Why Does This Affect Performance?
Primitive types are stored in the stack and are super lightweight—an int
takes up 4 bytes
. An Integer
, on the other hand, is an object in the heap, with metadata overhead (it can reach 16 bytes
or more, depending on the JVM). Each boxing creates a new object, increasing memory usage and the garbage collector’s workload. In loops or massive operations, this cost piles up fast.
Hands-On
Comparing Boxing vs. Primitives. Let’s test this with real code. The goal here is to sum 10 million integers in two ways:
- Using an
ArrayList
(with boxing) - Using an
int
array.
GitHub Repository: https://github.com/andersonbosa/boxing-performance-test/blob/main/src/BoxingPerformanceTest.java
I kept it in the repo for better readability.
The results on your computer might vary slightly from those below (since computational power differs between machines):
Time with array (primitive): 29 ms
Memory used by primitive array: 0 MB
----------------------------------------
Time with ArrayList (boxing): 171 ms
Memory used by ArrayList: 288 MB
Why? With the ArrayList
, every int
becomes an Integer
(10 million objects!), while the array uses just contiguous memory (TL;DR: fast access, slow insertion) for primitives. The garbage collector also has to clean up those objects afterward, adding to the impact.
Sure, you might be thinking you’re not about to go summing 10 million numbers, right? So let’s move on—I’ll show the impact of boxing/unboxing in poorly thought-out real-world algorithms.
Spoiler
Try setting the size to 500_000_000
to see the consequences!
Beware of Poorly Implemented Algorithms
Nested loops (a loop inside a loop) handling request data, authentication operations, or hash calculations processing large input volumes—what do they have in common? The memory and CPU overhead can easily scale, turning functional code into a bottleneck. Here are some practical examples I picked up from my research and chats with colleagues who’ve faced these issues in real systems:
- Authentication: Batch ID Verification
A REST API validates user IDs from a JWT against an authorized list.
import java.util.List;
import java.util.Set;
import java.util.HashSet;
public class AuthService {
private static final Set<Integer> AUTHORIZED = new HashSet<>(List.of(1001, 1002, 1003));
public boolean validateIds(List<Integer> userIds) {
for (Integer id : userIds) { // Unboxing
if (!AUTHORIZED.contains(id)) { // More unboxing
return false;
}
}
return true;
}
}
For a request with thousands of IDs (e.g., batch validation), each Integer
requires unboxing. The alternative? Use int[]
and IntHashSet
(from the FastUtil library).
- Request Processing: Status Filtering
Imagine an endpoint that filters HTTP status codes (e.g., 200, 404, etc.) from received logs.
import java.util.ArrayList;
import java.util.List;
public class LogProcessor {
public List<Integer> filterStatus(List<Integer> statusCodes) {
List<Integer> filtered = new ArrayList<>();
for (Integer code : statusCodes) { // Unboxing
if (code >= 200 && code < 300) { // More unboxing
filtered.add(code); // Boxing
}
}
return filtered;
}
}
With thousands of requests, each filtering operation generating boxing/unboxing would degrade performance. Using int[]
would solve the issue.
- Encryption/Hashing: Data Integrity Validation
For example, a web service calculates and compares SHA-256 hashes of file chunks sent via request to verify integrity (e.g., in a chunked upload).
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
public class HashValidator {
public List<Integer> calculateHashBytes(byte[] chunk) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(chunk);
List<Integer> hashValues = new ArrayList<>();
for (byte b : hash) {
hashValues.add((int) b); // Boxing to Integer
}
return hashValues; // Returns for comparison
}
}
How could we improve this? Each byte of the hash (32 bytes for SHA-256) is converted into an Integer
, creating 32 objects per call. I simulated this with 10,000 1 KB chunks: boxing increased memory usage by 20% and added overhead to the garbage collector, impacting endpoint latency. Alternative? Stick with byte[]
or use int[]
if conversion is needed.
Takeaways
-
Prefer primitives: If you don’t need objects, use
int
,double
, etc. Arrays likeint[]
are your friends in intensive operations. - Avoid boxing in loops: Each iteration with autoboxing means one more object in the heap.
- Know your alternatives! Libraries like Trove or Eclipse Collections offer optimized collections for primitives.
- Profile your code: Tools like VisualVM or JProfiler reveal where boxing is costing you.
My Conclusion
Boxing and unboxing are handy mechanisms, but their indiscriminate use can severely compromise performance, especially in critical or poorly tested code sections. As developers and engineers, our role goes beyond just making code work. We should aim for solutions that not only solve the problem but are also efficient, resource-sustainable, and resilient to failures.
Test, compare, and optimize—your code, your users, and your career will thank you (lol).
Top comments (4)
Good job! :)
In my opinion there's a lot of ... what I call
collections abuse
- the tendency to solve everything with aList
and aMap
... which hurts in so many ways (including as you say - with boxing/unboxing)... Anyway, I'm just saying that ... if you're looking to write more on such topics - there's a lot! :)I appreciate your words, thank you @zethix ;)
Could you suggest me some subject of study? I'm interested in performance and troubleshooting with Java or Golang but I'm without a north for now...
Pfeww... there's a lot... I don't know, just of the top of my head (and because it also seems related to your post):
If you 'stick to arrays' - you can take a look at the JIT compiler, and in particular - how it turns java code into native... and how when you're using arrays - it can even 'vectorize' it - that is - will optimize it to use SIMD instructions - if available on the CPU
the
stack vs heap
(and how that affects GC) topic is also a really good one I think, quite a lot of things can be done specifically by choosing when to use the stack...I'll think of some more, but, until then - take a look at the above, let me know if they look interesting to you... Or if you need some more info...
Awesome! I will start with how
stack vs heap
can affect the GC, but I was very interested in the termSIMD instructions
, that one is new to me. I'll send you updates when I have something new to share ;) Thanks for guiding me!