Using the Agrona library (Part 3)
Primitive Collections: Maps Without Boxing
"Every
Integer.valueOf()is a tiny betrayal of performance. Agrona's primitive collections commit zero betrayals."
Java generics require objects. Agrona provides primitive-specialized collections that store int, long, and double values directly in arrays — no boxing, no wrapper objects, no GC pressure.
The Boxing Problem Revisited
// JDK: 3 objects per map entry
HashMap<Integer, Integer> jdkMap = new HashMap<>();
jdkMap.put(42, 100);
// Creates: Integer(42) + Integer(100) + HashMap.Node
// Agrona: 0 objects per map entry
Int2IntHashMap agronaMap = new Int2IntHashMap(Integer.MIN_VALUE);
agronaMap.put(42, 100);
// Creates: nothing. Stores 42 and 100 directly in int[] arrays.
Int2IntHashMap — int→int Mapping
The most commonly used Agrona collection. Stores int keys mapped to int values with zero object allocation:
import org.agrona.collections.Int2IntHashMap;
// The "missing value" — returned when a key doesn't exist (instead of null)
int MISSING = Integer.MIN_VALUE;
Int2IntHashMap map = new Int2IntHashMap(MISSING);
// Basic operations
map.put(1001, 15025); // instrumentId → price
map.put(1002, 22050);
map.put(1003, 8775);
int price = map.get(1001); // 15025 (primitive int, no boxing!)
int missing = map.get(9999); // Integer.MIN_VALUE (missing)
boolean has = map.containsKey(1001); // true
int old = map.remove(1003); // 8775
// Size and emptiness
map.size(); // 2
map.isEmpty(); // false
The Missing Value Pattern
Since primitives can't be null, Agrona uses a missing value sentinel:
// ❌ JDK: null means "not found"
Integer val = jdkMap.get(unknownKey); // null
if (val == null) { /* not found */ }
// ✅ Agrona: a specific int value means "not found"
int val = agronaMap.get(unknownKey); // returns MISSING_VALUE
if (val == MISSING) { /* not found */ }
// IMPORTANT: You cannot store the missing value as a real value!
// Int2IntHashMap map = new Int2IntHashMap(0);
// map.put(42, 0); // ⚠ Cannot distinguish "value is 0" from "key not found"!
// Choose a missing value that your data will never use.
How Open Addressing Works
Unlike JDK's HashMap (which uses chaining — linked lists for collisions), Agrona uses open addressing with linear probing:
Linear Probing in Detail
// Conceptual Int2IntHashMap.put():
void put(int key, int value) {
int slot = hash(key) & mask; // Start slot
while (true) {
if (keys[slot] == MISSING_VALUE) {
// Empty slot — insert here
keys[slot] = key;
values[slot] = value;
size++;
return;
}
if (keys[slot] == key) {
// Key exists — update value
values[slot] = value;
return;
}
// Collision — try next slot (linear probe)
slot = (slot + 1) & mask;
}
}
// Conceptual Int2IntHashMap.get():
int get(int key) {
int slot = hash(key) & mask;
while (true) {
if (keys[slot] == MISSING_VALUE) {
return missingValue; // Key not found
}
if (keys[slot] == key) {
return values[slot]; // Found!
}
slot = (slot + 1) & mask; // Linear probe
}
}
Why Linear Probing is Cache-Friendly
Adjacent array elements are in the same cache line (~64 bytes = 16 ints). Probing the next slot is often a cache hit. Following a Node.next pointer to a random heap location is almost always a cache miss.
Load Factor
Agrona maps resize when the load factor (default 0.55) is exceeded:
// Custom initial capacity and load factor
Int2IntHashMap map = new Int2IntHashMap(
1024, // initial capacity
0.67, // load factor (max 67% full before resize)
Integer.MIN_VALUE // missing value
);
Lower load factor → fewer collisions → faster lookups → more memory. Higher load factor → more collisions → slower lookups → less memory.
Long2ObjectHashMap — long→Object Mapping
When keys are long primitives but values are objects:
import org.agrona.collections.Long2ObjectHashMap;
Long2ObjectHashMap<String> sessionMap = new Long2ObjectHashMap<>();
sessionMap.put(1234567890L, "session-abc");
sessionMap.put(9876543210L, "session-def");
String session = sessionMap.get(1234567890L); // "session-abc"
// Key is stored as primitive long — no Long boxing!
// Value is still an object reference (unavoidable for non-primitives)
Available Primitive-Key Map Variants
Int2ObjectHashMap — int→Object Mapping
import org.agrona.collections.Int2ObjectHashMap;
Int2ObjectHashMap<Order> orderBook = new Int2ObjectHashMap<>();
orderBook.put(1001, new Order("AAPL", 100, 150.25));
orderBook.put(1002, new Order("MSFT", 200, 380.50));
Order order = orderBook.get(1001); // No Integer boxing for the key!
IntArrayList & LongArrayList — Primitive Lists
Avoid boxing for list operations:
import org.agrona.collections.IntArrayList;
// JDK: boxes every int
List<Integer> jdkList = new ArrayList<>();
jdkList.add(42); // Integer.valueOf(42) — allocation!
int val = jdkList.get(0); // Integer.intValue() — unboxing
// Agrona: stores primitive ints directly
IntArrayList agronaList = new IntArrayList();
agronaList.addInt(42); // No boxing — stored in int[]
int val2 = agronaList.getInt(0); // No unboxing — returns primitive
// Bulk operations
agronaList.addInt(10);
agronaList.addInt(30);
agronaList.addInt(20);
agronaList.sort(); // Sorts using Arrays.sort(int[])
// Also available:
// LongArrayList — for long primitives
IntHashSet & ObjectHashSet
import org.agrona.collections.IntHashSet;
import org.agrona.collections.ObjectHashSet;
// Primitive int set — no boxing
IntHashSet instrumentIds = new IntHashSet();
instrumentIds.add(1001);
instrumentIds.add(1002);
instrumentIds.add(1003);
instrumentIds.contains(1002); // true — no Integer boxing!
// Object set — open addressing (unlike JDK HashSet which wraps HashMap)
ObjectHashSet<String> symbols = new ObjectHashSet<>();
symbols.add("AAPL");
symbols.add("MSFT");
symbols.contains("AAPL"); // true
Key difference from JDK: ObjectHashSet uses open addressing directly, while JDK's HashSet wraps a HashMap with dummy values (extra overhead).
Int2ObjectCache — Set-Associative Cache
A specialized cache with LRU-like eviction using a set-associative design (similar to CPU caches):
import org.agrona.collections.Int2ObjectCache;
// Capacity MUST be a power of 2
Int2ObjectCache<String> cache = new Int2ObjectCache<>(
1024, // number of sets
8, // associativity (entries per set)
(key, value) -> {
// Eviction callback — called when an entry is evicted
System.out.println("Evicted: " + key + " → " + value);
}
);
cache.put(42, "Hello");
String val = cache.get(42); // "Hello"
// When a set is full, the oldest entry is evicted
Iteration Without Allocation
JDK iterators allocate Iterator objects. Agrona provides allocation-free iteration:
Int2IntHashMap map = new Int2IntHashMap(Integer.MIN_VALUE);
map.put(1, 100);
map.put(2, 200);
map.put(3, 300);
// ✅ Allocation-free iteration with forEach
map.forEach((key, value) -> {
System.out.println(key + " → " + value);
});
// ✅ Key-only iteration
map.forEachKey((key) -> {
process(key);
});
// ⚠️ JDK-compatible iteration (allocates Iterator, but works with for-each)
for (Int2IntHashMap.Entry entry : map.entrySet()) {
// entry is a flyweight — reused across iterations!
int key = entry.getIntKey();
int value = entry.getIntValue();
}
Complete Comparison Table
| Agrona Collection | JDK Equivalent | Boxing | Memory/Entry | Objects/Entry |
|---|---|---|---|---|
Int2IntHashMap |
HashMap<Integer,Integer> |
None | ~8 B | 0 |
Long2LongHashMap |
HashMap<Long,Long> |
None | ~16 B | 0 |
Int2ObjectHashMap<V> |
HashMap<Integer,V> |
Key only avoided | ~12 B | 0 (key) |
Long2ObjectHashMap<V> |
HashMap<Long,V> |
Key only avoided | ~16 B | 0 (key) |
Object2IntHashMap<K> |
HashMap<K,Integer> |
Value only avoided | ~12 B | 0 (value) |
IntArrayList |
ArrayList<Integer> |
None | 4 B | 0 |
LongArrayList |
ArrayList<Long> |
None | 8 B | 0 |
IntHashSet |
HashSet<Integer> |
None | ~4 B | 0 |
ObjectHashSet<E> |
HashSet<E> |
N/A | ~8 B | 0 (no Map wrapper) |
Summary
The one-liner: Agrona's primitive collections store int/long values directly in arrays using open-addressing hash maps — eliminating boxing, reducing memory 8x, and achieving zero GC pressure in the hot path.