Skip to main content

Section 12.12 Step 1: Designing the Data

Having thoroughly understood the required operations and conceptual model for our dynamic list in Step 0, we now move to Design Recipe Step 1: Data Definitions. For implementing a class like ArrayList<T>, this step focuses on answering the critical question: What information must an ArrayList object remember internally (as instance variables or fields) to correctly perform all the operations specified by the ListADT<T> interface?
Let’s think iteratively, considering some key operations identified in Step 0 and what data they imply a need for:
  • To implement get(index) or set(index, item): We clearly need access to the underlying sequence of elements themselves to retrieve or modify the one at a specific position. Information Needed: The stored elements in order.
  • To implement size() or isEmpty(): We need to know the current number of elements actively stored in the list, which might be different from the total storage capacity. Information Needed: The current count of elements.
  • To implement add(index, item) or remove(index): We need the current elements (to shift them) and the current size (to know which elements to shift and where the valid indices end). We also need to know if there’s enough underlying storage capacity. Information Needed: The stored elements, the current count, the storage capacity.
  • To handle dynamic resizing (when adding to a full list): We need access to the current elements (to copy them) and the current storage capacity (to know when it’s full and how much larger the new storage should be). Information Needed: The stored elements, the storage capacity.
  • To implement search operations like contains(item) or indexOf(item): We need the current elements and the current size (to know how many elements to check). Information Needed: The stored elements, the current count.
Analyzing these requirements, two fundamental pieces of information emerge as essential state that the ArrayList object must maintain:
  1. The sequence of elements: Since we decided in Step 0 to use an array internally, we need an array field to hold the actual list items.
  2. The current number of elements: We need to track how many items are *actually* in the list, as distinct from the total capacity of the internal array. This logical size determines valid indices and when resizing is needed.
What about the array’s capacity? Do we need a separate field for that? No, because Java arrays provide a built-in way to query their fixed length using .length. So, the capacity information is implicitly available from the array object itself.
Therefore, our data definition for the internal state of ArrayList<T> requires just two instance variables (also called fields):
public class ArrayList<T> implements ListADT<T> {

    // 1. An array to store the list elements.
    //    It holds type T (the generic type parameter).
    private T[] buffer;

    // 2. An integer to track the number of elements currently in the list.
    private int size;

    // Constructor and methods will go here...
}
We declare these fields as private. Why? This enforces encapsulation (See Chapter 3), a core OOP principle. By making the internal array and the size count private, we prevent code outside the ArrayList class from directly manipulating them in ways that might break the list’s rules or invariants (like the "no gaps" rule, or making size inconsistent with the actual number of elements). All interactions must happen through the public methods defined by the ListADT interface, ensuring the list maintains a valid state.

Subsection 12.12.1 The Challenge of Generic Arrays (T[])

Looking at private T[] buffer;, you might recall from the Generics chapter (See Chapter 10, specifically the section on Type Erasure limitations) that creating arrays of a generic type parameter (new T[capacity]) is problematic in Java due to type erasure. At runtime, the JVM doesn’t know what T actually is, so it cannot safely create an array specifically typed as T[].
The standard workaround, which we will use when we implement the constructor (in Step 5), is to create an array of Object and cast it:
// Inside the ArrayList constructor (preview of Step 5)
private static final int DEFAULT_CAPACITY = 10; // Example capacity

public ArrayList() {
    // Cannot do: this.buffer = new T[DEFAULT_CAPACITY]; // Compile Error!
    // Workaround: Create Object array and cast
    this.buffer = (T[]) new Object[DEFAULT_CAPACITY]; // Requires unchecked cast
    this.size = 0;
}
This cast ((T[])) generates an "unchecked cast" warning from the compiler, because the compiler cannot guarantee at runtime that this cast will always be safe (though in the context of how ArrayList uses it, it generally is). To acknowledge this necessary workaround, we typically add the @SuppressWarnings("unchecked") annotation to the constructor or the field declaration. Understanding this issue now helps clarify why the initialization code will look the way it does later. Our data definition remains conceptually T[], but we must remember this implementation detail forced by type erasure.

Subsection 12.12.2 Key Invariants

Based on our chosen fields and the conceptual model from Step 0, we can define crucial invariants – conditions that must always be true for any valid ArrayList object after any public method call completes:
  • 0 <= size: The size can never be negative.
  • size <= buffer.length: The number of elements stored cannot exceed the capacity of the internal array.
  • Elements are contiguous: The list elements occupy the array slots from index 0 to size - 1. All array slots from index size to buffer.length - 1 are considered unused (and should ideally hold null references after a remove or clear). This directly relates to the "no gaps" rule.
These invariants are critical. Our implementation code in Step 5 must ensure that every method maintains these conditions to guarantee the correctness and consistency of our ArrayList.
We have now defined the essential data fields (buffer and size) and their invariants needed for our ArrayList<T>. With this internal state defined, we are ready for Design Recipe Step 2: formally defining the class structure and method signatures based on the ListADT interface.
You have attempted of activities on this page.