Dynamic Array Implementation
PhoneDirectory stores name/number pairs in a private array and tracks the used portion with dataCount.
A Directory That Must Grow
A phone directory stores a list of names, each paired with a phone number. Given a name, you can look up its number. Given a name and a number, you can record that pairing, and if the name is already present, the new number replaces the old one. The Java class PhoneDirectory implements exactly these operations, and it does so with only one array and one counter. Because the state is that small, you can follow every change to it line by line.
The problem this article addresses is how the array can hold an unknown number of entries when Java arrays have a fixed length. The answer is a dynamic array: an array that is replaced by a larger copy when it runs out of room. The same idea appears in the DynamicArrayOfInt class, which applies it to a plain list of int values. Working through both classes gives you two views of one mechanism.
What do you think happens?
A directory holds two entries, Ann and Ben, and dataCount is 2. What does find(Carl) return?
Reveal answer
-1
find compares Carl with the name at index 0 and then at index 1. Both comparisons fail. The index then reaches 2, which is no longer less than dataCount, so the loop ends and find returns -1. The search never looks past dataCount, so it never examines any unused slots.
Backing Array Versus Item Count
A dynamic array in this source is built from two pieces of state. The first is a backing array, a plain int array called items. The second is a count called itemCt. The two are kept separate on purpose, so that the capacity of the array and the number of meaningful values can differ. The array's length is the space that has been reserved. The item count says how much of that space is actually holding values that belong to the list.
PhoneDirectory uses the same split with different names. Its private array is called data, and its private integer dataCount records how many slots are in use. The pair type, PhoneEntry, is a private static nested class with a name field and a number field. Keeping both the array and the entry type private is an example of encapsulation: code outside the class never touches data directly and only calls the public methods getNumber and putNumber.
In this picture the backing array has eight slots, which matches the source's initialization of items to new int[8]. Three values have been added, so itemCt is 3. Slots 0 through 2 are the list. Slots 3 through 7 exist and have memory reserved for them, but they are not yet part of the list. The source calls the items array partially full, and that phrase is the key to the whole design. Two numbers describe the array at any moment: items.length says how many values can be stored before the array must grow, and itemCt says how many values are currently part of the list.
Searching the Used Part with find
Every lookup and every update in PhoneDirectory depends on the private find method. It takes a name and scans the used part of the array, from index 0 up to but not including dataCount, comparing each stored name with the one it was given. If it finds a match, it returns that index. If the loop ends without a match, it returns -1. The loop bound is dataCount rather than data.length, so slots that have not been filled yet are never examined.
An unsuccessful search for Carl
The directory holds Ann at index 0 and Ben at index 1, with dataCount equal to 2 and data.length equal to 2. Trace find(Carl).
- 1Start the loop
Set i to 0. Since 0 is less than
dataCount(2), the loop body runs. - 2Compare index 0
The stored name Ann does not match Carl, so the comparison fails and i becomes 1.
- 3Compare index 1
The stored name Ben does not match Carl, so the comparison fails and i becomes 2.
- 4Check the bound
i is now 2, which is not less than
dataCount(2). The loop ends without a match. - 5Return the sentinel
find returns -1. Because no slot beyond
dataCountwas examined, the result does not depend on the total capacity of the array.
find(Carl) returns -1.
The same loop handles a successful search. If find(Ben) were called on that directory, the comparison at index 0 would fail, the comparison at index 1 would succeed, and find would return 1 immediately without checking the remaining positions. The search is a single left-to-right scan, and its stopping points are the first match or the bound dataCount, whichever comes first.
Looking Up and Updating Numbers
getNumber calls find. If find returns -1, getNumber turns that result into null, which signals that the name is absent. Otherwise it returns the number stored at the index find reported. The update method putNumber is more involved. Its first step is to check both arguments. If either the name or the number is null, it throws an IllegalArgumentException with the message name and number cannot be null. This check happens before find is called, so a null argument never reaches the search and never changes the directory.
After the null check, putNumber calls find. If the name is already present, the method replaces the number stored at that index and adds no new pair. If the name is absent, it adds a new pair in the next free slot and increases dataCount. This is the difference between replacement and insertion, and the directory's contents show which one happened.
In the diagram, Ann was previously stored with 555-0101. Because Ann is already present, putNumber finds index 0 and replaces the number there, so dataCount stays at 2. The two entries are still the only entries. A call to getNumber(Carl) would find no match, find would return -1, and getNumber would return null.
555-0199
null- The second
putNumberreplaces the stored number for Ann instead of adding a second Ann entry. getNumber(Carl)returns null because find returned -1.- The last call throws
IllegalArgumentExceptionbefore find runs, so the directory is unchanged.
Doubling the Array During an Insert
The constructor creates the PhoneDirectory array with a length of 1 and sets dataCount to 0. The resize happens inside putNumber, and only when a name is not already present. The code checks whether dataCount has reached data.length. If it has, the array is full, and the code replaces it with a copy that is twice as long. The doubling is performed with Arrays.copyOf, and it occurs only during an insert of a new name.
Because the constructor starts with length 1, the first insert does not trigger a copy. dataCount is 0 and data.length is 1, so the array has room. The second insert finds dataCount equal to data.length, so the array is doubled to length 2 before the new pair is stored. The existing entry is carried across into the larger array, and the new entry occupies the next free slot.
- The check uses
dataCount, not the name search result, to decide that the array is full. Arrays.copyOfcopies the existing entries into an array of twice the length.- The new pair goes into the next free slot, and
dataCountgrows by one.
The DynamicArrayOfInt class follows the same pattern in its add method. Its first line checks whether itemCt has reached the length of the backing array. If it is full, the method creates a new array twice as large and copies the old values into it, using Arrays.copyOf(items, 2*items.length). Then it stores the new item at position itemCt and increases itemCt by one. The doubling happens only when the backing array is full. On most calls to add, the copy is skipped, and the new item is stored directly into the existing array.
Index Checks in get, set, add, and remove
The get and set methods let a caller read or change an element by its position. Both begin with the same guard. If the index is negative or is not less than itemCt, the method throws an ArrayIndexOutOfBoundsException with a message that names the bad index. Only after the guard passes does the method touch the backing array. The remove method uses the same guard, so an invalid index throws an exception there too.
- The guard compares against
itemCt, notitems.length. - A slot that exists in the backing array but lies beyond
itemCtis still rejected. - Only a passing guard leads to access of items.
The remove method deletes the item at a given index. After the guard passes, a loop moves every later item one position toward the front, overwriting the removed value. The loop runs from index plus one up to the last item in use. Each pass copies items[j] into items[j-1]. After the loop, itemCt decreases by one. The loop stops at itemCt, not at items.length, so slots beyond the list are never shifted. remove does not reallocate or shrink the backing array.
Removing the middle item
A DynamicArrayOfInt has items holding 4, 7, 9 in slots 0 through 2, with itemCt equal to 3 and items.length equal to 8. Trace remove(1).
- 1Check the index
1 is not negative and is less than
itemCt(3), so the guard passes. - 2Shift later items
The loop starts at j = 2 and runs up to
itemCt- 1 = 2. It copies items[2] (9) into items[1], overwriting 7. - 3Decrease the count
itemCtgoes from 3 to 2. The list is now 4 and 9. - 4Observe the leftover slot
items[2] still physically holds 9, but the count no longer includes it, so
get(2)would now throw an exception.
The list reads 4, 9. itemCt is 2, and the backing array length is still 8.
Mistakes When Tracing Dynamic Arrays
Common mistake Treating items.length as the size of the list
What it looks likeAfter adding three values to an array of length 8, the trace says the size is 8 because the array is 8 long.
Why it failsThe size method returns
itemCt, and the backing array length only says how many values can be stored before a resize.Do this insteadRead itemCt for the count of meaningful values and treat items.length as reserved capacity.
Common mistake Expecting find to scan the whole array
What it looks likeA trace of find(Carl) looks at every slot in data, including unfilled ones.
Why it failsThe loop bound is
dataCount, so the loop stops before any unused slot.Do this insteadCompare only indexes 0 up to but not including dataCount.
Common mistake Assuming a null argument changes the directory
What it looks likeA trace of putNumber(null, 555-0300) shows a new entry added with a null name.
Why it failsThe null check runs first and throws
IllegalArgumentExceptionbefore find or any insert.Do this insteadStop the trace at the exception; the data and dataCount values stay as they were.
Common mistake Expecting doubling on every insert
What it looks likeA trace shows the array being copied on each putNumber call.
Why it failsThe copy happens only when
dataCountequalsdata.lengthduring an insert of a new name.Do this insteadCompare dataCount with data.length at each step to decide whether a copy occurs.
Common mistake Expecting remove to shrink the backing array
What it looks likeAfter remove, the trace shows items.length dropping from 8 to 7.
Why it failsremove shifts later items left and decreases
itemCt, but it does not reallocate or shrink the array.Do this insteadExpect items.length to stay the same after remove and only itemCt to change.
Locating Where a Bad Result Starts
When a trace disagrees with your prediction, do not start by rereading the whole class. Compare your predicted state step by step with the recorded state, and find the first call where they differ. That call is the place to examine. When a dynamic array returns something unexpected, the fastest way to locate the problem is to ask three questions in order.
- 1First, what is
itemCt(ordataCount) at the moment of the call? - 2Second, which guard was evaluated, and did it pass or throw?
- 3Third, if the call changed the list, did the change happen in the array, in the count, or in both?
- 4Most surprises trace back to one of these three points, which narrows the search to a single method.
In the PhoneDirectory class, the usual suspects are the loop bound in find, the dataCount comparison that decides whether a resize happens, and the early null check in putNumber. In DynamicArrayOfInt, they are the itemCt guard in get, set, add, and remove, and the shifting loop in remove. A call that fails with an exception has its origin at the guard, because that is the first place the bad index or count is used.
Practice Predicting a Trace
A DynamicArrayOfInt starts empty with items of length 8. Add 4, then 7, then 9. Predict itemCt and the contents of items[0] through items[2] after the three adds. Then predict what remove(0) does to itemCt and to items[0] and items[1]. Before you check, state which guard each call evaluates.
Hints
- After the three adds,
itemCtshould match the number of values added. remove(0)shifts items[1] into items[0] and items[2] into items[1].- Check whether
items.lengthchanged after remove.
Predict the result of putNumber(Ben, 555-0177) on a directory that already holds Ann and Ben, with dataCount 2 and data.length 2. Say whether a copy happens, whether dataCount changes, and what getNumber(Ben) returns afterward.
Hints
- Ben is already present, so find will return an index.
- Replacement changes only the number stored at that index.
Key Takeaways
- A dynamic array keeps a backing array (items or data) for reserved space and a separate count (
itemCtordataCount) for the values that are part of the list. - PhoneDirectory searches with find, which scans only indexes 0 through
dataCountminus 1 and returns -1 when no name matches;getNumberturns -1 into null. putNumberrejects null arguments first, replaces the number for an existing name, and adds a new pair only after doubling a full array withArrays.copyOf.- get, set, add, and remove check indices against
itemCtrather thanitems.length, and remove shifts later items left without shrinking the backing array. - When a trace diverges, find the first call where the predicted and recorded states differ, then check the count, the guard, and whether the change touched the array or the count.