Skip to main content

Section 12.19 Step 5.1: Implementing Core Methods

We have reached the pivotal Design Recipe Step 5: Implementation, Testing & Refinement. This is where our careful planning from Steps 0-4 pays off. Our task now is to translate the logical skeletons we developed into working Java code within the ArrayList<T> class (src/DataStructures/ArrayList.java).
A key part of Step 5 is the iterative cycle of implementing a small piece of functionality (based on a skeleton), running the provided unit tests (using the techniques from Section 11) to verify it, and debugging any failures until the tests pass. We will implement the methods incrementally, starting with the foundational ones.

Subsection 12.19.1 Initial Class Structure and Constructor

Let’s start with the basic class structure, including the fields from Step 1 and the constructor needed to initialize them.
package DataStructures;

import ADTs.ListADT;
import java.util.NoSuchElementException; // Needed for first()/last() etc.
// We might need IndexOutOfBoundsException, IllegalArgumentException too,
// but they are in java.lang (implicitly imported).

public class ArrayList<T> implements ListADT<T> {

    private static final int DEFAULT_CAPACITY = 10;

    private T[] buffer;
    private int size;

    /**
     * Constructs an empty list with an initial capacity of DEFAULT_CAPACITY.
     */
    @SuppressWarnings("unchecked") // Suppress warning for the generic array cast
    public ArrayList() {
        // --- Implementation based on Step 1 Data Definition ---

        // As discussed in Step 1, we cannot directly do "new T[DEFAULT_CAPACITY]"
        // due to type erasure. We use the standard workaround:
        this.buffer = (T[]) new Object[DEFAULT_CAPACITY];

        // Initially, the list contains no elements.
        this.size = 0;
    }

    // --- Methods will be added below ---

}
Explanation:
  • We declare the DEFAULT_CAPACITY (e.g., 10) as a constant.
  • The constructor initializes the buffer array. It uses the new Object[...] approach and casts the result to T[], as planned in Step 1 to work around Java’s type erasure limitations (See Chapter 10). The @SuppressWarnings("unchecked") annotation tells the compiler we are aware of and accept the necessary cast here.
  • It initializes size to 0, fulfilling the invariant for an empty list.

Subsection 12.19.2 Implementing size() and isEmpty()

These are the simplest methods, directly reflecting the state planned in Step 1 and skeletonized in Step 4.
// Inside the ArrayList<T> class...

@Override
public int size() {
    // --- Implementation based on Step 4 Skeleton ---
    // 1. Return the current value of the 'size' instance variable.
    return this.size;
}

@Override
public boolean isEmpty() {
    // --- Implementation based on Step 4 Skeleton ---
    // 1. Return true if size == 0, false otherwise.
    return this.size == 0;
}
Explanation: These methods simply return the value of the size field or check if it’s zero, directly implementing the logic outlined in their skeletons.

Subsection 12.19.3 Implementing get() and set()

These methods involve accessing or modifying an element at a specific index, requiring bounds checks first, as planned in Step 4.
// Inside the ArrayList<T> class...

@Override
public T get(int index) {
    // --- Implementation based on Step 4 Skeleton ---

    // 1. Check index bounds (0 <= index < size)
    if (index < 0 || index >= this.size) {
        throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + this.size);
    }

    // 2. Return buffer[index]
    return this.buffer[index];
}

@Override
public T set(int index, T item) {
    // --- Implementation based on Step 4 Skeleton ---

    // 1. Check for null item (as per Javadoc contract)
    if (item == null) {
        throw new IllegalArgumentException("Item cannot be null.");
    }

    // 2. Check index bounds (0 <= index < size)
    if (index < 0 || index >= this.size) {
        throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + this.size);
    }

    // 3. Store current buffer[index]
    T oldItem = this.buffer[index];

    // 4. Update the array
    this.buffer[index] = item;

    // 5. Return 'oldItem'
    return oldItem;
}
Explanation:
  • Both methods first perform the necessary checks identified in the skeletons (index bounds for both, null item check for set). Note the slightly different bound for get/set (index < size) versus add (index <= size).
  • If the checks pass, get simply returns the element at the requested index from the internal array.
  • set saves the original element, updates the array at the index with the new item, and returns the saved original element, fulfilling its contract.
  • We include informative messages when throwing exceptions, which helps in debugging.

Subsection 12.19.4 Run Tests Now! (Verifying Core Methods)

You have just implemented the constructor and the fundamental methods size(), isEmpty(), get(index), and set(index, item). This is the perfect time to practice the implement-test-debug cycle.
Go to the VSCode Test Explorer (Section 11). Run the tests. What should you expect? Since most list operations depend on being able to add elements first, and we haven’t implemented any add methods yet, most tests will fail. This is entirely normal!
However, based on the code written so far, these specific tests (or parts of tests) should now pass:
  • BasicOperationsTests > testNewListIsEmpty: This test directly checks the state created by the constructor using isEmpty() and size(). If your constructor correctly initializes size to 0, and your isEmpty() and size() methods are correct, this test should pass.
  • ✅ Parts of ExceptionTests > testAccessModify_InvalidIndex_ThrowsIndexOutOfBounds: Specifically, the parts that test get(0), set(0, "A"), and potentially remove(0) *on an empty list*. Your implemented get and set methods should correctly check if the index (0) is out of bounds when size is 0 and throw an IndexOutOfBoundsException. (The remove(0) check will likely still fail as remove isn’t implemented).
What to Watch Out For (Why Other Tests Fail):
  • Dependency on Add: Many tests (like testGet_ValidIndex, testSet_ValidIndex, or the second half of testAccessModify_InvalidIndex_ThrowsIndexOutOfBounds) first try to add elements to set up a specific list state before checking get or set. Since add isn’t working yet, these tests will fail, even if your get or set logic is perfect. Don’t be confused by this – focus on the tests that isolate the methods you’ve just written.
  • NullPointerExceptions or Incorrect Failures: If you have stub methods (methods with empty bodies or just return null;) for unimplemented operations like add, tests might fail in unexpected ways or with generic errors instead of specific assertion failures.
  • Incorrect Exception Type/Message: If an exception test fails, check if you threw the *exact* exception type required by the contract (e.g., IndexOutOfBoundsException) and if your exception message (if any) is helpful.
Your Goal Right Now: Focus on getting testNewListIsEmpty to pass (✅) and ensuring the empty list checks within testAccessModify_InvalidIndex_ThrowsIndexOutOfBounds for get and set also pass (likely by correctly throwing IndexOutOfBoundsException). Use the failure details for these specific tests to debug your constructor, size, isEmpty, get, and set implementations. Once these are solid, we can confidently move on.

Note 12.19.1. Code Snapshot.

If you\’d like to see the complete code as it should look at the end of this section (with core methods implemented), you can view the dr-step-5.1-core-impl branch in the project repository using Git checkout.
Having implemented and tested the constructor and basic accessors/modifiers, we have built a solid foundation. In the next section, we\’ll tackle the crucial logic for dynamic resizing and implementing the basic addLast method.
You have attempted of activities on this page.