Concepts / Partially Full Arrays

Partially Full Arrays

A dynamic array lets a collection grow and shrink while the program runs, and the class design hides the internal array behind operations such as add, remove, get, set, and size.

  • Programming

A Roster That Changes During the Match

Imagine a game that keeps a roster of the players currently taking part. Players join partway through a match, leave when they are eliminated, and the roster has to reflect that at every moment. A plain array can hold the players, but its length is fixed when it is created. Choose a length that is too small and you run out of room. Choose one that is too large and you waste space while still having to keep track of which slots are actually in use.

This article builds a growable collection from the ground up. The design is a Java class called DynamicArrayOfInt. It stores its items in a plain array and wraps that array in a small set of operations, so a caller can add, remove, read, and change items without ever touching the internal array directly. Along the way we will answer three concrete questions: what operations the collection must support, how it keeps track of its contents, and what should happen when someone asks for a position that holds no value.

Why a Fixed Length Is Not Enough

Many beginners look at an array and assume that every position inside it means something. If you declare an array of 100 integers, it seems natural to think that the program is storing 100 numbers. That assumption works when you know ahead of time exactly how many values you will store. It breaks down the moment the number of values changes while the program runs.

Consider a program that reads positive integers from the user, stores each one in an array, and stops when it receives a zero. Suppose the array was created with room for 100 values, but the user enters only two numbers before typing zero. The array's length is still 100, and the array itself cannot change size. Something else has to record where the real data stops.

What do you think happens?

The array has 100 positions. The user enters 2 numbers and then 0. How many positions hold valid data, and what tells the program where the data stops?

Reveal answer
Answer:

Two positions hold valid data, indexes 0 and 1. The length of 100 does not describe the contents. A separate integer counter holding 2 tells the program where the valid region ends.

The array's length describes how many slots exist, not how many of them are in use. The program needs a separate variable that records the number of occupied positions.

The fix is to stop relying on the array's length to describe its contents. The program keeps a separate integer variable, usually called a counter, that records how many positions are in use. The counter does not hold any of the numbers. It only keeps a tally of how many of the array's positions contain valid items.

Capacity and Size Are Different Numbers

Definition

Capacity is the length of the internal array, which is the number of slots that exist. In the class, the array is stored in a field named items, so capacity is items.length. Size is the item count, which records how many of those slots currently hold meaningful values. In the class, the count is stored in a field named itemCt.

Because the array is only partially full most of the time, capacity and size are usually different numbers. The class is written so that these two values are never confused. Every operation that deals with the contents of the collection is written in terms of the count, while the array length is used only when the class needs to know whether there is physical room left for a new value.

itemCt3 itemsitems[0]7items[1]3items[2]9items[3]no itemitems[4]no itemitems[5]no itemitems[6]no itemitems[7]no itemlast valid slot
If the internal array has length 8 but only 3 slots hold items, which slots are valid, and how does the class keep track of that difference?

The diagram shows the situation the class has to manage. The first three slots hold items, and the count of 3 is the only thing that marks them as valid. The remaining five slots exist in the array and are reserved, but the class never reads them as data. The starting capacity of 8 is an arbitrary choice used for this example. Any positive integer would make the class work. A very large starting array would waste memory on programs that only ever hold a few items, which is why the design relies on growth rather than a big initial array.

The Counter Doubles as the Next Free Index

The counter has a second job that is easy to miss. If the count is three, then three items occupy indexes 0, 1, and 2. That same value, three, is also the index of the next open spot. The counter therefore answers two questions at once: how many items are stored, and where the next one should go. Whenever you see the counter in a program, it is worth reading its value both ways.

The storing step follows a simple rhythm. The program places the new value at the position given by the counter, and then increments the counter by one so that the next value goes to the next free position. The counter is never used to decide whether a stored value is valid. It is updated only when a value is actually saved.

Key point

The counter does double duty. Its value is the number of stored items and also the index of the next free position. Positions zero through counter minus one hold items, and the position at the counter value is free.

The Valid Index Range

Every operation that names a position has to decide what to do with a position that does not hold a value. The class decides to throw an ArrayIndexOutOfBoundsException. The check is the same in get, set, and remove: the index is rejected if it is negative or if it is greater than or equal to itemCt.

size4index 0validindex 1validindex 2validindex 3validindex 4rejectedindex 5allocated, not validindex 6allocated, not validindex 7allocated, not validlast valid index is size minus 1
For a dynamic array with size 4 and internal length 8, which indexes are valid, which are unused but allocated, and where does an index of 4 land?

The valid positions run from 0 through itemCt minus 1. With a size of 4, that means indexes 0 to 3. The slot at position 4 exists in the internal array, because the array has length 8, but it holds no item. The class treats it as illegal anyway. This is the detail that catches most readers: the upper limit is the size, not the length.

Which Operations Change the Count

A useful dynamic array needs a small, fixed set of operations. Each one is an instance method of the class, so a caller writes something like roster.add(7) rather than manipulating the internal array directly. The five essential operations are add, remove, get, set, and size. Only add and remove change how many items the collection holds. get, set, and size leave the count alone. That split is useful when reading the class: any method that touches itemCt is a method whose effect on the collection's shape needs careful thought.

OperationChanges the count?What it does to the contentsCan it throw an exception?
add(value)Yes, increases by oneStores value at position itemCt, growing the storage first if it is fullNot for a bad position; the caller supplies no index
remove(index)Yes, decreases by oneSlides every later value one position toward the frontYes, if index is negative or at or beyond itemCt
get(index)NoReturns the value at index without changing anythingYes, if index is negative or at or beyond itemCt
set(index, value)NoOverwrites the value at indexYes, if index is negative or at or beyond itemCt
size()NoReports itemCt, the number of meaningful itemsNo

The five essential operations of the DynamicArrayOfInt class and their effect on the item count.

Tracing a Call Sequence Step by Step

Worked example

Tracing a roster through add, set, remove, and get

Start with a new DynamicArrayOfInt whose internal array has length 8 and whose itemCt is 0. Apply add(7), add(3), add(9), set(1, 5), remove(0), get(1), and get(2). Track the count and the valid contents after each step, and decide what the final get returns.

  1. 1
    Step 1: add(7)

    itemCt is 0, which is less than the length of 8, so no growth is needed. The value 7 is stored at position 0 and itemCt becomes 1. Valid contents: [7].

  2. 2
    Step 2: add(3)

    The value 3 goes to position 1, the current itemCt. itemCt becomes 2. Valid contents: [7, 3].

  3. 3
    Step 3: add(9)

    The value 9 goes to position 2, and itemCt becomes 3. Valid contents: [7, 3, 9].

  4. 4
    Step 4: set(1, 5)

    Index 1 is valid because it is less than itemCt of 3. The value at position 1 is overwritten with 5. The count stays at 3. Valid contents: [7, 5, 9].

  5. 5
    Step 5: remove(0)

    Index 0 is valid. Every later value slides one position toward the front: position 0 takes 5 and position 1 takes 9. itemCt drops to 2. Valid contents: [5, 9]. Position 2 still physically holds 9 from before, but it lies outside the valid range, so the class never reads it.

  6. 6
    Step 6: get(1)

    Index 1 is less than itemCt of 2, so the check passes. The value 9 is returned.

  7. 7
    Step 7: get(2)

    Index 2 is greater than or equal to itemCt of 2, so the check fails. The class throws an ArrayIndexOutOfBoundsException. The valid indexes are only 0 and 1.

Result

The final get(2) throws an ArrayIndexOutOfBoundsException. Before that, get(1) returned 9. Throughout, the internal array kept length 8, while the count moved 0, 1, 2, 3, 3, 2, 2.

StartitemCt 0After add(7)7; itemCt 1After add(3), add(9)7, 3, 9; itemCt 3After set(1, 5)7, 5, 9; itemCt 3After remove(0)5, 9; itemCt 2get(2)throws exceptionadd(7)add(3), add(9)set(1, 5)remove(0)get(2)
After each successive add, set, or remove call, what do the valid contents and the size counter look like?

Notice that set and get never move the count. Only add and remove do, and remove does so by shifting values rather than by shrinking storage. When you trace a sequence, the count is the number you should check first, because it tells you which indexes are legal at the next call.

How Add Grows the Storage

The class behaves as if it can hold any number of items, but the internal array has a fixed length. The trick happens in add. Before writing a new value, add checks whether itemCt has reached items.length. If it has, the array is full. The method then asks Arrays.copyOf for a new array with twice the length, copies the existing values into it, and makes items refer to the new array. Only after that does add store the new value at position itemCt and increase itemCt by one.

  1. 1
    Compare itemCt with items.length before writing.
  2. 2
    If itemCt equals items.length, the array is full.
  3. 3
    Create a new array with twice the length using Arrays.copyOf, which copies the existing values.
  4. 4
    Make items refer to the new, larger array.
  5. 5
    Store the new value at position itemCt.
  6. 6
    Increase itemCt by one.

Because every add that finds a full array doubles the capacity, the storage grows as needed and the caller never has to resize anything. In the example from earlier, starting at capacity 8, the first eight adds need no growth. A ninth add would find the array full, double it to 16, copy the eight old values across, and then place the new value in the first free position.

What Remove Leaves Behind

Removal works in the opposite direction, and it is worth noticing what it does not do. The remove method slides every later value one position toward the front, then decreases itemCt by one. It never shrinks the internal array. The capacity stays what it was, and only the count changes.

Walking the Valid Items in Reverse

When it is time to output the stored values, the counter tells the program where the valid region ends. The last occupied position is always the counter minus one, because the counter points one past the final item. A loop that begins there and walks downward to zero visits every valid item exactly once, from last to first.

  1. 1
    Start the index at itemCt minus 1, the last valid position.
  2. 2
    Keep looping while the index is zero or greater.
  3. 3
    Process the value at the current index.
  4. 4
    Decrement the index by one and repeat.
  5. 5
    Stop once the index drops below zero, having visited every valid item once.
Worked example

Reverse traversal of the roster [5, 9]

After the sequence in the earlier example, itemCt is 2 and the valid contents are 5 and 9. Walk the valid items from last to first and state which positions the loop visits.

  1. 1
    Starting index

    The index starts at itemCt minus 1, which is 2 minus 1, so 1. Position 1 holds 9.

  2. 2
    First pass

    The loop processes items[1], which is 9. The index drops to 0.

  3. 3
    Second pass

    The index is still 0, which is zero or greater, so the loop continues. It processes items[0], which is 5. The index drops to minus 1.

  4. 4
    Stop

    The index is now below zero, so the loop ends. Position 2 was never visited, because it lies outside the valid range.

Result

The loop visits position 1 (9) and then position 0 (5), printing the valid items in reverse order: 9, then 5.

When a Sequence Overflows the Array

Counting protects a program only if the count stays inside the array's bounds. A plain array does not resize. Suppose it was created with a fixed number of positions and the program keeps storing values. Once the count reaches the array's length, the next value has no valid place to go. The program would attempt to store it in a position that does not exist, and Java raises an ArrayIndexOutOfBoundsException at run time.

Worked example

A fixed backing array of length 3

A plain array of length 3 is used with a counter. The input sequence is store a, store b, store c, store d. At which step does the program fail?

  1. 1
    Store a

    The counter is 0, so a goes to index 0. The counter becomes 1.

  2. 2
    Store b

    The counter is 1, so b goes to index 1. The counter becomes 2.

  3. 3
    Store c

    The counter is 2, so c goes to index 2. The counter becomes 3. The array is now full, and the last valid index is 2.

  4. 4
    Store d

    The counter is 3, which would be index 3. That index does not exist in an array of length 3, so the store raises an ArrayIndexOutOfBoundsException.

Result

The fourth store fails. The last valid index is always one less than the length, so the attempt to use index 3 is out of bounds. A dynamic array avoids this by growing before the write, which is why its add method checks for a full array first.

Mistakes to Avoid

  • Common mistake

    Treating the array length as the number of items

    What it looks like

    An array of length 100 holds only 2 user-entered numbers, but the loop prints all 100 positions.

    Why it fails

    The length describes how many slots exist, not how many are in use. Positions beyond the counter hold no data the program should read.

    Do this instead

    Keep a separate counter and loop only from 0 to counter minus 1.

  • Common mistake

    Using the array length as the upper bound for an index check

    What it looks like

    A check that accepts index 4 in a collection of size 4 because the internal array has length 8.

    Why it fails

    The valid range runs from 0 through size minus 1. Index 4 holds no item even though the slot physically exists.

    Do this instead

    Compare the index against itemCt, rejecting it when it is negative or greater than or equal to itemCt.

  • Common mistake

    Starting the reverse loop at the counter

    What it looks like

    With itemCt equal to 2, the loop starts at index 2 and reads a slot that holds no item.

    Why it fails

    The counter points one past the last item, so it is the index of the next free spot, not a stored value.

    Do this instead

    Start at itemCt minus 1.

  • Common mistake

    Expecting remove to shrink the internal array

    What it looks like

    After removing most items, the program assumes memory has been returned to the system.

    Why it fails

    The class only lowers the count and shifts values. The internal array keeps its capacity.

  • Common mistake

    Assuming a leftover value after remove is still part of the collection

    What it looks like

    After remove(0) on [7, 5, 9] the slot at position 2 still holds 9, and a get(2) is assumed to return it.

    Why it fails

    Position 2 lies outside the valid range once itemCt drops to 2, so get throws an exception.

    Do this instead

    Trust the count, not the leftover contents of the array.

Practice Tracing the Roster

MEDIUM

Start with a new DynamicArrayOfInt. Apply add(4), add(8), add(6), remove(1), add(2), and then get(2). After each call, record the value of itemCt and the valid contents. Predict whether the final get(2) returns a value or throws an ArrayIndexOutOfBoundsException before you check your trace.

Hints
  • After add(4), add(8), and add(6), itemCt is 3 and the valid contents are 4, 8, 6.
  • remove(1) slides the later value 6 into position 1 and lowers itemCt to 2.
  • add(2) stores at the current itemCt, which is position 2, and raises itemCt to 3.
  • A get is valid only when the index is less than itemCt at the moment of the call.
EASY

Explain in your own words why the valid index range is 0 through size minus 1 rather than 0 through the array length minus 1. Then state which index in a size-4 collection with internal length 8 would be the first rejected index.

Hints
  • The count, not the array length, decides which positions hold meaningful items.
  • Think about the slot at position equal to the size: it exists physically but holds no item.

Key Takeaways

  • A dynamic array tracks two numbers: the capacity, which is the internal array length, and the size, which is the count of meaningful items stored in itemCt.
  • The counter does double duty as the number of stored items and as the index of the next free position, and valid indexes run from 0 through size minus 1.
  • add grows the storage by doubling when the array is full and then stores the value at itemCt, while remove shifts later values forward and lowers the count without shrinking storage.
  • Reverse traversal starts at itemCt minus 1 and runs down to 0, visiting every valid item once and never touching unused slots.
  • A plain array does not resize, so storing past its last valid index raises an ArrayIndexOutOfBoundsException at run time.