<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Java Collections &amp; Data Structures: From Scratch | unkletayo.dev</title>
    <link>https://unkletayo.dev</link>
    <description>Build the data structures behind Java&apos;s collections, from arrays and ArrayList toward B+ trees and lock-free queues, and see how the JVM memory model shapes them.</description>
    <language>en</language>
    <managingEditor>contact@unkletayo.dev (Adetayo Akinsanya)</managingEditor>
    <lastBuildDate>Tue, 13 Oct 2026 07:00:00 GMT</lastBuildDate>
    <atom:link href="https://unkletayo.dev/engineering/java-collections/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>How Java ArrayList Works Internally: Resizing, Growth Factor, and Memory</title>
      <link>https://unkletayo.dev/engineering/java-collections/03-building-my-arraylist/</link>
      <guid isPermaLink="true">https://unkletayo.dev/engineering/java-collections/03-building-my-arraylist/</guid>
      <description>Build an ArrayList from scratch and see how resizing, System.arraycopy and garbage collection shape its performance.</description>
      <pubDate>Tue, 13 Oct 2026 07:00:00 GMT</pubDate>
      <category>java</category>
      <category>datastructures</category>
      <category>arraylist</category>
      <category>algorithms</category>
      <enclosure url="https://unkletayo.dev/cover/engineering/java-collections/03-building-my-arraylist.png" length="0" type="image/png"/>
      <content:encoded><![CDATA[<p>Before you start: this article assumes you can read a simple Java class. It briefly explains arrays (and counting positions from 0), references, generics and casting, exceptions, the binary shift, and Big-O as they come up.</p>
<p>A list is an ordered collection that grows as you add items, like a shopping list you keep adding to. An array is a fixed-size row of numbered slots, and Java's <code>ArrayList</code> is a list built on top of one: you call <code>add()</code> to append an item and <code>get(i)</code> to read the item at position <code>i</code>. The reason to want a list is that you often don't know in advance how many items you will have, and a plain array can't grow. Behind the scenes, an <code>ArrayList</code> swaps in a bigger array when it runs out of room. "Pre-sizing" means telling it up front how many slots to reserve.</p>
<p>The usual advice is to pre-size your lists. Before building one, I measured what that advice buys.</p>
<p>The test adds 500,000 references to a list, once with <code>new ArrayList&lt;&gt;()</code> and once with <code>new ArrayList&lt;&gt;(500_000)</code>. Setup: OpenJDK 25.0.2, a 512 MB heap, an Intel Core i9-9880H, 200 warm-up rounds, then the median of 301 runs:</p>
<pre><code class="language-text">default   median 3.95 ms   p90 5.35 ms
presized  median 4.04 ms   p90 4.81 ms
</code></pre>
<p>A few terms from that setup. The heap is the memory area where Java objects live. Warm-up rounds run the code first so Java's optimizer has settled before timing starts. The median is the middle result when all the runs are sorted from fastest to slowest. The p90 is the value that 90 percent of the runs were at or below, so it shows the slower runs.</p>
<p>Same speed. The presized list came out a hair slower, which is within the run-to-run noise. In time, the resizing costs almost nothing. Time here means how long the loop took to run. The next measurement looks at memory instead.</p>
<p>It costs memory. I measured allocated bytes for the same two loops:</p>
<pre><code class="language-text">default   6.48 MB
presized  2.00 MB
</code></pre>
<p>That's 3.2 times the allocation for an identical list. Every array except the last one is dead the moment its contents are copied, and the garbage collector has to reclaim it. The garbage collector is the part of Java that automatically frees memory held by objects nothing refers to any more. I measured allocated bytes here, not GC time, so I can't tell you what that costs in your service.</p>
<p>The rest of this article builds the thing that produces those numbers.</p>
<hr />
<h2>The problem a plain array has</h2>
<p>Think of an array as a row of numbered lockers (0, 1, 2, ...). You choose how many when you buy the row, and you can't bolt more on later.</p>
<p>Create an array of 10 and add an 11th element, and Java throws <code>ArrayIndexOutOfBoundsException</code>. Throwing an exception means Java stops the program with an error message unless the code handles it. The length is locked at creation.</p>
<p>You could allocate a million slots up front and never worry. With compressed references that's about 4 MB per list, whether it holds a million elements or three. A reference is a slot's pointer to an object, and compressed references let the JVM (the program that runs Java code) store each one in 4 bytes instead of 8.</p>
<p><code>ArrayList</code> is the compromise: a normal array inside, replaced by a bigger one when it fills. Replacing means making a brand-new, larger array, copying every item from the old one into it, and using the new one from then on. The old array is abandoned.</p>
<h2>What's inside</h2>
<p>Think of a row of numbered mailboxes with a sticky note saying how many have mail. The row is the array and the note is the count. Two fields do all the work:</p>
<ol>
<li><code>Object[] elementData</code> holds the elements. <code>Object[]</code> is an array whose slots can each hold a reference, meaning a pointer to an object that lives elsewhere in memory, to any kind of Java object.</li>
<li><code>int size</code> counts how many you've added.</li>
</ol>
<p>In the picture below, <code>null</code> means "this slot points at nothing". Capacity is the number of slots in the array. Size is how many of them hold real items.</p>
<pre><code class="language-text">After adding "A" and "B" to a list with capacity 4:

elementData:  [ "A" | "B" | null | null ]
size:         2
capacity:     4   (elementData.length)
</code></pre>
<p><code>size</code> is what the caller sees. <code>elementData.length</code> is how much room exists before the next resize. They are different numbers, and the code below keeps them apart.</p>
<h2>Growing the array</h2>
<p>Picture moving house when your boxes no longer fit in the van: you hire a bigger van, move everything across, and give the old one back. The list does the same with arrays.</p>
<p>When <code>size == elementData.length</code> the buffer (the internal array) is full, and the next <code>add()</code> does three things: allocate a bigger array, copy the old elements across with <code>System.arraycopy()</code>, and point <code>elementData</code> at the new one. <code>System.arraycopy()</code> is a built-in Java method that copies a range of slots from one array to another, or within the same array, in one call.</p>
<pre><code class="language-text">BEFORE (full, capacity 4):
[ 10 | 20 | 30 | 40 ]

AFTER (capacity 6 = 4 + 4/2), then 50 goes in the first free slot:
[ 10 | 20 | 30 | 40 | 50 | null ]
</code></pre>
<p>The JDK grows by 1.5 times:</p>
<p>$$\text{newCapacity} = \text{oldCapacity} + (\text{oldCapacity} \gg 1)$$</p>
<p>Computers store whole numbers in binary (base 2). Shifting right by one bit moves every binary digit one place to the right, which halves the number without a division and drops any remainder. For example, <code>10 &gt;&gt; 1</code> is 5 (binary 1010 becomes 101), and <code>5 &gt;&gt; 1</code> is 2. So with an old capacity of 10, the new capacity is 10 + 5 = 15.</p>
<p>A bigger factor means fewer copies but more empty slots after each jump. At 1.5, a list that has just resized is about a third empty, and that is the worst case. For example, a full list of 1,000 slots grows to 1,500. Once the 1,001st item is in, 499 slots are empty, and 499 of 1,500 is about a third.</p>
<p>One difference from the real class: <code>new ArrayList&lt;&gt;()</code> doesn't allocate 10 slots. In the OpenJDK source it starts with a shared empty array and only grows to 10 on the first <code>add()</code>. The version below allocates 10 eagerly because that's easier to read.</p>
<h2>The implementation</h2>
<p>A few pieces of Java syntax show up below. <code>MyArrayList&lt;T&gt;</code> uses <code>&lt;T&gt;</code> as a placeholder for the element type, so <code>MyArrayList&lt;String&gt;</code> holds strings and <code>MyArrayList&lt;Integer&gt;</code> holds integers. <code>private</code> means only code inside this class can touch a field, and <code>static final</code> makes a constant shared by the whole class. <code>this</code> refers to the object being built or used. The array stores plain <code>Object</code>s, so <code>(T)</code> is a cast that tells the compiler to treat a value as type <code>T</code>, and <code>@SuppressWarnings("unchecked")</code> silences the compiler's warning that it cannot verify that cast. <code>Arrays.copyOf(a, n)</code> returns a new array of length <code>n</code> holding a's items, padded with <code>null</code> if <code>n</code> is larger. An index is an item's position number, starting at 0.</p>
<pre><code class="language-java">import java.util.Arrays;

public class MyArrayList&lt;T&gt; {
    private static final int DEFAULT_CAPACITY = 10;
    private Object[] elementData;
    private int size;

    public MyArrayList() {
        this.elementData = new Object[DEFAULT_CAPACITY];
        this.size = 0;
    }

    public MyArrayList(int initialCapacity) {
        if (initialCapacity &lt; 0) {
            throw new IllegalArgumentException("Capacity cannot be negative: " + initialCapacity);
        }
        this.elementData = new Object[initialCapacity];
        this.size = 0;
    }

    public void add(T element) {
        ensureCapacity(size + 1);
        elementData[size++] = element;
    }

    @SuppressWarnings("unchecked")
    public T get(int index) {
        checkBounds(index);
        return (T) elementData[index];
    }

    @SuppressWarnings("unchecked")
    public T remove(int index) {
        checkBounds(index);
        T oldValue = (T) elementData[index];

        int numMoved = size - index - 1;
        if (numMoved &gt; 0) {
            System.arraycopy(elementData, index + 1, elementData, index, numMoved);
        }

        elementData[--size] = null; // clear the vacated slot
        return oldValue;
    }

    public int size() {
        return size;
    }

    private void ensureCapacity(int minCapacity) {
        if (minCapacity &gt; elementData.length) {
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity &gt;&gt; 1); // 1.5x growth
            if (newCapacity &lt; minCapacity) {
                newCapacity = minCapacity;
            }
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }

    private void checkBounds(int index) {
        if (index &lt; 0 || index &gt;= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
    }
}
</code></pre>
<h3>Walking through it</h3>
<p>Start with <code>list.add("A")</code> on a new list. <code>ensureCapacity(1)</code> sees that 1 is not more than the 10 slots, so it does nothing. Then <code>elementData[size++] = element</code> stores "A" in slot 0 and bumps <code>size</code> from 0 to 1. The <code>++</code> after <code>size</code> means "use the current value as the index, then add 1", so storing the element and updating the count happen in one line.</p>
<p>Now suppose the list holds 10 items and you add an 11th. <code>ensureCapacity(11)</code> finds that 11 is more than 10, so <code>newCapacity</code> becomes 10 + (10 &gt;&gt; 1) = 15. <code>Arrays.copyOf</code> builds a new 15-slot array holding the old 10 items, with <code>null</code> in the rest, and <code>elementData</code> now points at it. The old array has nothing pointing at it any more. Then the 11th item goes into slot 10.</p>
<p>Now <code>remove(1)</code> on <code>[A, B, C, D]</code>, where <code>size</code> is 4. <code>checkBounds(1)</code> passes because 1 is at least 0 and less than 4. <code>oldValue</code> is "B". Then <code>numMoved = 4 - 1 - 1 = 2</code>, the two items after the gap:</p>
<pre><code class="language-text">before:  [ A | B | C | D ]
copy C, D one slot left (from index 2 into index 1):
after:   [ A | C | D | D ]
then --size makes size 3 and nulls slot 3:
final:   [ A | C | D | null ]
</code></pre>
<p>If you ask for <code>get(3)</code> now, <code>checkBounds</code> throws <code>IndexOutOfBoundsException</code>, because 3 is not less than <code>size</code>.</p>
<h2>The line that stops a leak</h2>
<p>Imagine you take a book off a library shelf but leave its card in the catalogue. The library still believes the book is in use, so it never clears the space. A leftover slot in the array does the same thing to the garbage collector.</p>
<p>Look at the last statement in <code>remove()</code>:</p>
<pre><code class="language-java">elementData[--size] = null;
</code></pre>
<p>Here <code>--size</code> subtracts 1 from <code>size</code> first and uses the new value as the index. After removing "B" from <code>[A, B, C, D]</code> and shifting, the array is <code>[A, C, D, D]</code> and <code>size</code> is 4. The statement makes <code>size</code> 3 and sets slot 3 (the stale second "D") to <code>null</code>, giving <code>[A, C, D, null]</code>.</p>
<p>Drop the <code>= null</code> and keep only the <code>--size</code>, and the array still holds a reference to the removed object. No public method can reach it. The garbage collector can't know that; it sees a live reference and keeps the object alive. Objects kept alive this way are called loitering objects.</p>
<h2>Why <code>add()</code> is still O(1)</h2>
<p>Big-O notation describes how the work grows as the number of items grows. $O(1)$ means the work stays about the same however many items there are. $O(N)$ means it grows in proportion to $N$, the number of items. Think of paying a small bill every day versus a huge bill once a year: the yearly bill is big, but spread over 365 days it is a small daily cost.</p>
<p>One resize copies the whole array, which is $O(N)$. So how can <code>add()</code> be constant time?</p>
<p>Because resizes get rarer as the list grows. I simulated the JDK's growth rule from 10 slots up to 500,000 elements and counted: 27 resizes, 1,080,430 element copies in total. That's 2.16 copies per insert, spread over half a million inserts.</p>
<p>It's no accident that the number sits near 2. Each resize copies the old size, and with a 1.5 factor those old sizes form a geometric series:</p>
<p>$$\frac{N}{1.5} + \frac{N}{1.5^2} + \frac{N}{1.5^3} + \dots \approx 2N$$</p>
<p>Each term is the previous one divided by 1.5, so the terms shrink quickly. A series like that adds up to its first term divided by $(1 - 1/1.5)$. The first term is $N/1.5$ and $1 - 1/1.5$ is $1/3$, so the total is $(N/1.5) \div (1/3) = 2N$. For example, with $N = 600$ the copies are 400, then 267, then 178, and so on, and the running total creeps toward 1,200.</p>
<p>Total copying is bounded by about $2N$ for $N$ inserts. The cost per insert is a constant, which is what amortized $O(1)$ means. Amortized means averaged over a long run of operations: a few <code>add()</code> calls are expensive, but the average cost per call stays constant. Individual calls can still be slow; the average can't.</p>
<h2>What I'd do with this</h2>
<p>If you know the final size, pre-size. In my benchmark it didn't speed up the loop, but it cut allocation from 6.48 MB to 2.00 MB. If you don't know the size, leave it alone; the growth rule handles it.</p>
<p>And if you write your own list, put the <code>null</code> in <code>remove()</code>.</p>
<h2>Quick summary</h2>
<ul>
<li>A <code>size</code> field and a flat array make a list. Everything else is bookkeeping.</li>
<li>Full means resize by 1.5 times with <code>Arrays.copyOf()</code>. 27 resizes take it from 10 slots to 500,000 elements.</li>
<li>Resizing barely moved the timing; it tripled the memory allocated.</li>
<li><code>remove()</code> shifts elements left, then nulls the last slot so nothing loiters.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Why You Must Always Override equals() and hashCode() Together in Java</title>
      <link>https://unkletayo.dev/engineering/java-collections/02-equals-and-hashcode/</link>
      <guid isPermaLink="true">https://unkletayo.dev/engineering/java-collections/02-equals-and-hashcode/</guid>
      <description>Why equals() and hashCode() have to agree, and how breaking that contract causes silent HashMap lookup bugs and memory leaks.</description>
      <pubDate>Thu, 01 Oct 2026 07:00:00 GMT</pubDate>
      <category>java</category>
      <category>datastructures</category>
      <category>hashmap</category>
      <category>jvm</category>
      <enclosure url="https://unkletayo.dev/cover/engineering/java-collections/02-equals-and-hashcode.png" length="0" type="image/png"/>
      <content:encoded><![CDATA[<p>Before you start: this article assumes you know what a class, an object, a field and a method are in Java, and how to create an object with <code>new</code>. Everything else is explained as it comes up.</p>
<p>Suppose it's 2:00 AM on a Friday. Your phone alerts you to a production incident: logged-in users cannot access their subscription accounts. ("Production" means the live system that real users are using right now.)</p>
<p>Your authentication service uses a custom <code>UserSession</code> object as a key inside a <code>HashMap</code> to fetch user roles.</p>
<p>A <code>HashMap</code> is a Java lookup table that stores pairs: you hand it a key and it gives back the value saved for that key, like looking up a name in a phone book to get a number. A key is the item you look something up by (here, a session), and a value is the thing stored under it (here, the user's role). Putting a pair in is called <code>put</code>, and looking a value up by its key is called <code>get</code>. In plain terms, when a user logs in the program stores their role in the map, and later requests ask the map for it.</p>
<p>The logs show that when a user logs in, the service creates a <code>UserSession(userId=981)</code> key and stores their role. One millisecond later, when the user requests their profile page, the server constructs another <code>UserSession(userId=981)</code> key and queries the map.</p>
<p>The result? <code>map.get(session)</code> returns <code>null</code>. The user gets logged out immediately.</p>
<p>Why does <code>HashMap</code> claim the session key does not exist when both key objects contain identical data?</p>
<p>The answer lies in the contract between <code>equals()</code> and <code>hashCode()</code>.</p>
<hr />
<h2>Identity (<code>==</code>) vs Value Equality (<code>equals</code>)</h2>
<p>Picture two printed copies of the same form. They say the same thing, but they are still two separate sheets of paper. Asking "is this the very same sheet?" is what <code>==</code> does. Asking "do these say the same thing?" is what <code>equals</code> does.</p>
<p>In Java, a variable that holds an object really holds a reference, which is the address of where the object lives in memory. The heap is the area of memory where Java stores objects. The <code>==</code> operator compares memory addresses when applied to object references. It returns <code>true</code> only if both variables point to the exact same heap allocation (the same block of memory).</p>
<pre><code class="language-java">User userA = new User(101, "Alex");
User userB = new User(101, "Alex");

boolean sameAddress = (userA == userB); // false! Separate heap allocations.
</code></pre>
<pre><code class="language-text">HEAP
+-----------------------------------+
| User Object 0x1A8F                |
| id: 101, name: "Alex"             |  &lt;--- userA
+-----------------------------------+

+-----------------------------------+
| User Object 0x2B90                |
| id: 101, name: "Alex"             |  &lt;--- userB
+-----------------------------------+
</code></pre>
<p>To compare the fields you care about (here the user ID and name) rather than memory locations, Java provides the <code>equals(Object obj)</code> method on <code>java.lang.Object</code>.</p>
<p>Every Java class automatically inherits from the built-in class <code>java.lang.Object</code>. Inheriting means your class gets the methods <code>Object</code> already has, including <code>equals</code>, without you writing them. To override a method means to write your own version of it in your class, which replaces the inherited one.</p>
<p>By default, <code>Object.equals()</code> simply performs <code>this == obj</code>. You must override it to enforce value-based equality, meaning two objects count as equal when their fields match.</p>
<hr />
<h2>The Rules <code>equals()</code> Must Follow</h2>
<p>Think of "equals" the way you think of "is the same age as": it has to behave sensibly. If Ana is the same age as Ben, then Ben is the same age as Ana.</p>
<p>The Java standard library requires any <code>equals()</code> override to fulfill five mathematical properties. A contract here means a rule that the Java libraries rely on and trust you to follow. The compiler does not check it.</p>
<ol>
<li><strong>Reflexive</strong>: <code>x.equals(x)</code> must return <code>true</code>. (Everything equals itself.)</li>
<li><strong>Symmetric</strong>: <code>x.equals(y)</code> must return <code>true</code> if and only if <code>y.equals(x)</code> returns <code>true</code>. (If A says it equals B, B must say it equals A.)</li>
<li><strong>Transitive</strong>: If <code>x.equals(y)</code> is <code>true</code> and <code>y.equals(z)</code> is <code>true</code>, then <code>x.equals(z)</code> must return <code>true</code>. (If A equals B and B equals C, then A equals C.)</li>
<li><strong>Consistent</strong>: Multiple invocations return identical results unless fields mutated. (Mutated means changed after creation. The answer may change only if a field used in the comparison changed.)</li>
<li><strong>Null comparison</strong>: <code>x.equals(null)</code> must always return <code>false</code>. (<code>null</code> means "no object", and a real object is never equal to it.)</li>
</ol>
<hr />
<h2>The Hash Code Integer</h2>
<p>Think of a coat check. You hand over your coat and get a numbered ticket, and the attendant hangs the coat on the rack that matches the number. When you come back, the ticket tells the attendant which rack to look at, so nobody searches every rack. A hash code works like that ticket: it is a number that acts as a short label for an object, and it tells the map which shelf to look on.</p>
<p>Formally, a hash code is a 32-bit signed integer (<code>int</code>, a whole number that can be negative) computed from an object's internal fields.</p>
<p>Hash-based collections use this integer to pick a spot in an array. <code>HashMap</code> stores key and value pairs, <code>HashSet</code> stores a set of unique items, and <code>LinkedHashMap</code> is a <code>HashMap</code> variant that also remembers the order items were added. An array here is a numbered row of slots, and each slot is called a bucket. Because the map jumps straight to one bucket instead of checking every entry, a lookup takes about the same time no matter how many entries the map holds. This is written $O(1)$ and read as "constant time".</p>
<p>The real <code>HashMap</code> source code (OpenJDK, <code>java.util.HashMap</code>) computes the bucket index in two steps. First it mixes the high bits of the hash code into the low bits, then it keeps only the low bits that fit the array:</p>
<pre><code class="language-java">int h = key.hashCode();
int hash = h ^ (h &gt;&gt;&gt; 16);          // spread: mix the top 16 bits into the bottom 16
int bucketIndex = (n - 1) &amp; hash;   // n is the array length, always a power of two
</code></pre>
<p>Here <code>&gt;&gt;&gt;</code> shifts the bits of a number to the right, <code>^</code> combines two numbers bit by bit (XOR), and <code>&amp;</code> keeps only the bits set in both numbers. Because <code>n</code> is a power of two (the default is 16), <code>(n - 1) &amp; hash</code> gives a number from 0 up to <code>n - 1</code>, so it always lands inside the array.</p>
<p>If two objects have different hash codes, a hash collection immediately knows they cannot be equal. It skips the expensive field-by-field <code>equals()</code> check. This works because of the contract described below: equal objects must have equal hash codes, so different hash codes prove the objects are not equal.</p>
<h3>How <code>put</code> and <code>get</code> use the hash code, step by step</h3>
<p>Putting an entry in with <code>put(key, value)</code>:</p>
<ol>
<li>The map calls <code>key.hashCode()</code> to get a number.</li>
<li>It spreads that number with <code>h ^ (h &gt;&gt;&gt; 16)</code> and keeps the low bits with <code>(n - 1) &amp; hash</code> to get a bucket index.</li>
<li>If that bucket is empty, the map stores the entry there and is done.</li>
<li>If the bucket already holds entries, the map compares the new key with each stored key using <code>equals()</code>. If one is equal, it replaces that entry's value. If none is equal, it adds the new entry to the bucket.</li>
</ol>
<p>Looking a value up with <code>get(key)</code>:</p>
<ol>
<li>The map calls <code>key.hashCode()</code> and computes the bucket index the same way.</li>
<li>It goes straight to that bucket. Nothing else is searched.</li>
<li>Inside the bucket, it checks the stored keys with <code>equals()</code> (the real code also compares the stored hash first as a quick filter). When one matches, it returns that entry's value. If none matches, it returns <code>null</code>.</li>
</ol>
<p>A tiny worked example with made-up numbers, using a map with 16 buckets:</p>
<ul>
<li>Suppose a key has hash code 1012. Since <code>1012 &gt;&gt;&gt; 16</code> is 0, spreading changes nothing: <code>1012 ^ 0</code> is 1012. In binary, 1012 ends in <code>0100</code>, so <code>15 &amp; 1012</code> is 4. The entry goes in bucket 4.</li>
<li>Later, <code>get</code> is called with a second key that <code>equals()</code> the first and so, following the rule, also has hash code 1012. It computes bucket 4, finds the stored key there, <code>equals()</code> says yes, and the value comes back.</li>
<li>Now suppose the second key broke the rule and had hash code 100000. Here <code>100000 &gt;&gt;&gt; 16</code> is 1, spreading gives <code>100000 ^ 1</code>, which is 100001, and <code>15 &amp; 100001</code> is 1. The map looks in bucket 1, finds nothing, and returns <code>null</code>. It never gets to call <code>equals()</code>, because it never looked in bucket 4.</li>
</ul>
<p>That last case is exactly the bug in this article.</p>
<hr />
<h2>The Unbreakable Contract Relationship</h2>
<p>Besides the five rules above, <code>equals()</code> and <code>hashCode()</code> share one more rule. This is the contract that <code>HashMap</code> depends on:</p>
<blockquote>
<p><strong>If two objects are equal according to <code>equals(Object)</code>, they MUST produce the exact same integer result from <code>hashCode()</code>.</strong></p>
</blockquote>
<p>The reverse is not required: two unequal objects CAN produce identical hash codes. That scenario is called a <strong>hash collision</strong>. Two unequal objects that collide end up in the same bucket, and the map then uses <code>equals()</code> to tell them apart.</p>
<pre><code class="language-text">               +----------------------------------+
               |         Equal Objects            |
               |     (userA.equals(userB) == true)|
               +----------------------------------+
                                |
                                | MUST HAVE
                                v
               +----------------------------------+
               |       Identical Hash Codes       |
               | (userA.hashCode() == userB.hashCode())
               +----------------------------------+
</code></pre>
<hr />
<h2>Production Horror Story: The Missing <code>hashCode()</code> Bug</h2>
<p>A bug like this can slip through because the code compiles, runs without errors, and only returns the wrong answer. Here is code that reproduces the problem from the opening scenario. Read the notes after the listing if any line is unfamiliar:</p>
<pre><code class="language-java">import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

class UserSession {
    private final int userId;

    public UserSession(int userId) {
        this.userId = userId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserSession session = (UserSession) o;
        return userId == session.userId;
    }

    // MISSING HASHCODE OVERRIDE!
}

public class IncidentDemo {
    public static void main(String[] args) {
        Map&lt;UserSession, String&gt; roles = new HashMap&lt;&gt;();
        
        UserSession loginSession = new UserSession(981);
        roles.put(loginSession, "ADMIN");

        UserSession requestSession = new UserSession(981);
        
        // loginSession.equals(requestSession) is TRUE, but hashCodes almost certainly differ!
        System.out.println("Role found? " + roles.get(requestSession)); // Prints NULL!
    }
}
</code></pre>
<p>How to read the listing:</p>
<ul>
<li><code>private final int userId</code> declares a field that only this class can see (<code>private</code>) and that cannot be reassigned after the constructor sets it (<code>final</code>).</li>
<li><code>@Override</code> is a note to the compiler that the method below replaces one inherited from <code>Object</code>. If the method name or parameters do not match an inherited method, the compiler reports an error.</li>
<li><code>getClass() != o.getClass()</code> compares the exact types of the two objects. <code>getClass()</code> returns the class an object was created from, so the check returns <code>false</code> for an object of a different type.</li>
<li><code>(UserSession) o</code> is a cast. It tells Java to treat <code>o</code>, which is declared as a plain <code>Object</code>, as a <code>UserSession</code>, so its <code>userId</code> can be read. The type check on the line above makes that safe.</li>
<li><code>Map&lt;UserSession, String&gt;</code> uses generics, the angle-bracket notation that says what a collection holds. Here the keys are <code>UserSession</code> objects and the values are <code>String</code>s. The <code>new HashMap&lt;&gt;()</code> on the right lets Java fill in the same types.</li>
</ul>
<p>What happens when <code>main</code> runs:</p>
<ol>
<li><code>roles.put(loginSession, "ADMIN")</code> hashes <code>loginSession</code> and stores "ADMIN" in the bucket that hash code picks.</li>
<li><code>requestSession</code> is a second, separate object with the same <code>userId</code>. <code>equals()</code> would say it matches <code>loginSession</code>.</li>
<li><code>roles.get(requestSession)</code> hashes <code>requestSession</code>. Without a <code>hashCode()</code> override, that number is almost certainly different, so the map looks in a different bucket, finds nothing, and returns <code>null</code>.</li>
</ol>
<p>Because <code>hashCode()</code> was omitted, <code>loginSession</code> and <code>requestSession</code> inherited the default identity hash code from <code>java.lang.Object</code>. Identity means it is tied to the object itself, not to its field values. The Java documentation says only that <code>Object.hashCode()</code> returns distinct integers for distinct objects as far as is reasonably practical. It does not promise that the number is the memory address, so do not rely on that. The result is that two separate objects get different hash codes in practice.</p>
<p><code>roles.get(requestSession)</code> hashed <code>requestSession</code>, computed a completely different bucket index (very likely, though not guaranteed), inspected an empty array slot, and returned <code>null</code>. It never reached <code>equals()</code>.</p>
<hr />
<h2>Correct Implementation Pattern</h2>
<p>Always override both methods together using the exact same fields. If <code>equals()</code> compares <code>userId</code>, then <code>hashCode()</code> must be built from <code>userId</code> too, so any two objects that <code>equals()</code> calls the same get the same number. <code>Objects.hash(...)</code> is a ready-made helper that combines the fields you give it into one hash code:</p>
<pre><code class="language-java">@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    UserSession session = (UserSession) o;
    return userId == session.userId;
}

@Override
public int hashCode() {
    return Objects.hash(userId);
}
</code></pre>
<hr />
<h2>Quick Summary</h2>
<ul>
<li><code>==</code> checks reference addresses. <code>equals()</code> checks business value equality (equal in the sense your program cares about, such as the same user ID).</li>
<li>Equal objects must return identical <code>hashCode()</code> values.</li>
<li>Omitting <code>hashCode()</code> causes hash collections to inspect wrong bucket indices, triggering silent lookup failures in production.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>How the JVM Actually Lays Out Arrays in Memory</title>
      <link>https://unkletayo.dev/engineering/java-collections/01-java-memory-and-arrays/</link>
      <guid isPermaLink="true">https://unkletayo.dev/engineering/java-collections/01-java-memory-and-arrays/</guid>
      <description>How HotSpot lays out primitive arrays compared with arrays of object references, across stack frames, object headers, and CPU caches.</description>
      <pubDate>Thu, 24 Sep 2026 07:00:00 GMT</pubDate>
      <category>java</category>
      <category>datastructures</category>
      <category>memory</category>
      <category>jvm</category>
      <enclosure url="https://unkletayo.dev/cover/engineering/java-collections/01-java-memory-and-arrays.png" length="0" type="image/png"/>
      <content:encoded><![CDATA[<p>Before you start: you should know that arrays are numbered from 0 (so <code>numbers[3]</code> is the fourth item), that <code>new</code> creates an object, and that a byte is 8 bits. Everything else is explained below.</p>
<p>Java hides memory addresses by default. Most of the time, that's a blessing: you instantiate an array (create it), append items, pass it to a method, and never think about pointers (variables that hold the memory address of something, like a street address instead of the house) or memory offsets (how far past the start of something a piece of data sits).</p>
<p>Until you scale throughput (how much work your program finishes per second) or inspect a heap dump (a snapshot file listing every object in memory).</p>
<p>Then you notice something baffling: an array holding one million <code>Integer</code> wrappers takes about 20 megabytes of heap, while an array holding one million primitive <code>int</code> values takes 4 (measured on OpenJDK 25 with default settings).</p>
<p>Two terms there need a plain definition. A primitive such as <code>int</code> stores just the number itself. An <code>Integer</code> is an object that wraps one number inside it, and Java converts between the two automatically, which is called boxing. The heap is the large area of memory where Java keeps every object you create with <code>new</code>. That is a five-to-one gap, and the section called The Numbers in Practice, further down, shows where every byte goes.</p>
<p>To understand why custom data structures perform well or fail under memory pressure, you have to look past the Java syntax and see how the HotSpot JVM actually organizes bytes in RAM. The JVM (Java Virtual Machine) is the program that runs your compiled Java code and manages its memory, HotSpot is the most common JVM implementation, and RAM is the computer's working memory.</p>
<hr />
<h2>Stack Frames vs The JVM Heap</h2>
<p>Think of the stack as a sticky note on your desk: you jot down what you need for the current task and throw the note away when the task ends. The heap is a warehouse: bigger items are stored there and found later by a shelf address.</p>
<p>In Java terms, a thread is one line of execution in a running program. Each time a thread calls a method, the JVM sets aside a small block of memory for that call, called a stack frame, and the frames pile up in order on the thread's call stack. When a thread executes a Java method, the JVM divides working memory between two distinct regions, the stack (the frames) and the heap (the shared warehouse of objects):</p>
<pre><code class="language-text">  THREAD STACK (Local execution)             JVM HEAP (Shared Objects)
+-----------------------------+          +----------------------------------+
| process() frame             |          | int[] Object Allocation          |
|                             |          |                                  |
| primitiveVal = 42           |          | Header (Mark Word + Klass Word)  |
| numbersRef -----[0x7A4F00]----------&gt;  | Length: 4                        |
|                             |          | [ 10 | 20 | 30 | 40 ]            |
+-----------------------------+          +----------------------------------+
</code></pre>
<p>Local primitive variables (<code>int</code>, <code>double</code>, <code>boolean</code>) live directly inside the stack frame slot. When the method returns, the frame pops off the call stack (is removed from the top of the pile) and that memory is immediately reclaimed without any garbage collection involvement. Garbage collection is the JVM's automatic cleanup that finds heap objects nothing uses any more and frees their memory.</p>
<p>Reference variables (<code>int[]</code>, <code>String</code>, custom objects) hold a memory address. A reference is Java's word for a pointer. An address is normally 8 bytes (64 bits), but by default the JVM shrinks it to 4 bytes (32 bits) using a feature called Compressed OOPs (compressed ordinary object pointers), which works while the heap stays below roughly 32 gigabytes. This article assumes that default, so every reference below is 4 bytes. That address points to a block of memory allocated inside the shared JVM heap.</p>
<p>Reading the diagram above: <code>primitiveVal</code> is a local variable, so the number 42 sits right in the frame. <code>numbersRef</code> is also a local variable, but it holds only the address <code>0x7A4F00</code> (the <code>0x</code> prefix means the number is written in hexadecimal, base 16). The array itself, including its length, sits in the heap at that address. When the method returns, both locals vanish from the stack, but the array stays in the heap until the garbage collector finds that nothing points to it.</p>
<hr />
<h2>Contiguous Memory: The Primitive Array Layout</h2>
<p>A primitive array is like a row of numbered lockers, all the same size, built side by side. Contiguous means exactly that: back to back with no gaps, like seats in one row of a theater.</p>
<p>When you run:</p>
<pre><code class="language-java">int[] numbers = new int[4];
</code></pre>
<p>The HotSpot JVM allocates a single contiguous slice of bytes on the heap (a byte is the unit memory is counted in, and an <code>int</code> takes 4 of them):</p>
<pre><code class="language-text">Memory Offset   | Field / Content
----------------|----------------------------------------------
0x00 - 0x07     | Mark Word (8 bytes: GC metadata, locks, hash)
0x08 - 0x0B     | Klass Word (4 bytes under Compressed OOPs)
0x0C - 0x0F     | Array Length = 4 (4 bytes)
0x10 - 0x13     | numbers[0] (4 bytes)
0x14 - 0x17     | numbers[1] (4 bytes)
0x18 - 0x1B     | numbers[2] (4 bytes)
0x1C - 0x1F     | numbers[3] (4 bytes)
</code></pre>
<p>How to read the table: the first column is the offset, the number of bytes past the start of the object, written in hexadecimal (<code>0x10</code> is 16). Every object begins with a header, a fixed block the JVM uses for its own bookkeeping. You never write to it, but it costs space. The Mark Word holds things like garbage collection information, locks, and the object's hash code. The Klass Word records which class the object belongs to. For an array the header is 8 + 4 + 4 = 16 bytes (mark word, class word, length), and the elements start right after it, at offset <code>0x10</code>.</p>
<p>Because 32-bit integers sit right next to each other in physical RAM, reading <code>numbers[3]</code> doesn't require scanning through previous elements. Every element is the same size, so the address of element 3 is computed directly from the start of the array rather than found by counting up from element 0.</p>
<p>The CPU calculates the memory address in hardware:</p>
<p>$$\text{Address}(\text{numbers}[i]) = \text{BaseAddress} + 16 + (i \times 4)$$</p>
<p>Here BaseAddress is where the array begins, 16 is the header size from the table above, and 4 is the byte size of each <code>int</code>. Multiplying by the index $i$ jumps straight to element $i$. Worked example: for $i = 3$, the offset is $16 + 3 \times 4 = 28$, which is <code>0x1C</code> in hexadecimal, exactly where <code>numbers[3]</code> sits in the table.</p>
<p>This is why random indexing in an array is strictly $O(1)$. O(1), or constant time, means the work does not grow as the array gets bigger: getting item 3 and item 3,000,000 takes the same number of steps.</p>
<p>There is a second benefit. The CPU keeps recently used memory in a small, very fast storage area called a cache, and it loads memory from RAM in fixed-size chunks called cache lines (commonly 64 bytes). When the CPU reads <code>numbers[0]</code>, its hardware prefetcher pulls the entire 64-byte cache line into the L1 cache (the smallest and fastest level), so <code>numbers[1]</code>, <code>numbers[2]</code> and the rest of that chunk are already waiting. Items stored next to each other usually arrive together, which makes sequential reads very fast.</p>
<hr />
<h2>The Indirection Trap: Object Reference Arrays</h2>
<p>Indirection means reaching a value through a middleman, like a contact list that holds addresses instead of the people. An object array is that list: each locker holds a note saying where to find the real item, and each real item lives somewhere else.</p>
<p>Now contrast primitive arrays with an array of objects:</p>
<pre><code class="language-java">String[] names = new String[3];
names[0] = "Alice";
names[1] = "Bob";
</code></pre>
<p>It's tempting to think this allocates strings contiguously in RAM. It doesn't.</p>
<p><code>String[]</code> is an array of pointers:</p>
<pre><code class="language-text">  STACK                                     HEAP
+----------+          +-----------------------------------------------+
| names    |---------&gt;| String[] Array (Container)                    |
+----------+          | Length: 3                                     |
                      | [0]: 0x9B10 --------------------+             |
                      | [1]: 0x9C40 ------------------+ |             |
                      | [2]: null                     | |             |
                      +-------------------------------|-|-------------+
                                                      | |
                                                      | +--&gt; [ String: "Alice" ]
                                                      +----&gt; [ String: "Bob" ]
</code></pre>
<p>Each index in <code>names</code> stores only a 4-byte pointer (a reference, using the Compressed OOPs size described earlier). The actual <code>String</code> instances live somewhere else in heap memory, often scattered in unrelated places. <code>null</code> in slot <code>[2]</code> means that slot holds no reference yet: it points at nothing.</p>
<p>Walkthrough: <code>new String[3]</code> creates three empty slots, all <code>null</code>. The two assignment lines create the strings <code>"Alice"</code> and <code>"Bob"</code> elsewhere in the heap and store their addresses in slots 0 and 1. The array holds addresses only, never the text itself.</p>
<p>When you loop through <code>names</code>, the CPU has to follow each pointer to a new address, jumping across cache lines and causing CPU cache misses. A cache miss happens when the CPU needs data that is not in its cache and has to fetch it from slower main memory.</p>
<hr />
<h2>The Numbers in Practice</h2>
<p>Start with one element. In a primitive array, each element is a 4-byte number sitting in its locker. In a boxed array, each element is a 4-byte note in the locker (the pointer) plus a separate 16-byte <code>Integer</code> object somewhere else. That is 4 bytes versus 4 + 16 = 20 bytes per element, a ratio of 5 to 1. Multiply by one million and you get the figures in the code below.</p>
<p>Here is what one million elements look like in JVM heap memory:</p>
<pre><code class="language-java">public class MemoryFootprint {
    public static void main(String[] args) {
        int size = 1_000_000;

        // Primitive int array:
        // 16 bytes header + (1,000,000 * 4 bytes) ≈ 4.0 MB total.
        int[] rawNumbers = new int[size];

        // Boxed Integer array:
        // 16 bytes array header + (1,000,000 * 4 bytes pointers) = 4.0 MB
        // PLUS 1,000,000 separate Integer objects (16 bytes each) = 16.0 MB
        // Total footprint: ~20.0 MB, measured with allocated-bytes counters.
        Integer[] boxedNumbers = new Integer[size];
        for (int i = 0; i &lt; size; i++) {
            boxedNumbers[i] = i;
        }
    }
}
</code></pre>
<p>Walkthrough: <code>new Integer[size]</code> creates only the million pointer slots, all <code>null</code> at first. Each <code>boxedNumbers[i] = i;</code> line makes Java box the <code>int</code> (automatically convert it to an <code>Integer</code>), which creates a new <code>Integer</code> object on the heap and stores its address in slot <code>i</code>. Every <code>Integer</code> is an object, so it carries its own header (12 bytes: the 8-byte mark word and a 4-byte class word) plus the 4 bytes of the number, which makes 16 bytes. The array itself costs 16 bytes of header plus 4 million bytes of pointers, about 4 MB. The million objects add 16 million bytes, about 16 MB. Together that is about 20 MB, against about 4 MB for the <code>int</code> array.</p>
<p>That 5x memory bloat isn't just wasted RAM. With compressed references off, an <code>Integer</code> grows to 24 bytes and the ratio to 6x.</p>
<p>It also means the Garbage Collector has to track, mark, and sweep one million individual objects during a collection cycle instead of inspecting a single contiguous byte buffer. Marking means finding which objects are still in use, and sweeping means freeing the ones that are not.</p>
<hr />
<h2>What Comes Next</h2>
<p>Understanding contiguous memory allocation and reference pointer indirection is the foundation for everything we will build in this series.</p>
<p>In Part 2, we dissect how the JVM calculates <code>equals()</code> and <code>hashCode()</code> contract rules, why hash collisions (terms we define in Part 2) degrade memory lookups, and how that shapes custom collection implementations.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
