Skip to main content

Section 12.22 Step 5.4: Implementing Remaining Methods

We’re in the main phase of Design Recipe Step 5: Implementation. We’ve successfully implemented the core indexed add/remove methods and the essential resizing logic. Many of the remaining methods defined in the ListADT<T> interface can now be implemented more easily by leveraging the core logic we’ve already built and tested. This demonstrates a key benefit of good design: building reusable components.

Subsection 12.22.2 Implementing End Accessors (first, last)

These methods provide access to the first and last elements, primarily involving an empty-list check before delegating to get.
// Inside the ArrayList<T> class...

@Override
public T first() {
    // --- Implementation based on Step 4 Skeleton ---
    // 1. Check if empty
    if (isEmpty()) { // Reuse isEmpty() method
        throw new NoSuchElementException("List is empty.");
    }
    // 2. If not empty, delegate to get(0)
    return get(0); // Reuse get() method
}

@Override
public T last() {
    // --- Implementation based on Step 4 Skeleton ---
    // 1. Check if empty
    if (isEmpty()) { // Reuse isEmpty() method
        throw new NoSuchElementException("List is empty.");
    }
    // 2. If not empty, delegate to get(size - 1)
    return get(this.size - 1); // Reuse get() and size field/method
}
These implementations clearly show how convenience methods are built upon core methods like get, isEmpty, and the size field, after handling their specific preconditions (the empty check).

Subsection 12.22.3 Implementing Convenience Add/Remove Methods

We previously implemented addFirst using its direct logic sequence. Now we implement the remaining convenience methods: removeFirst, removeLast (also using their direct logic sequences), and addAfter (which demonstrates composition by using other methods).
// Inside the ArrayList<T> class...

@Override
public T removeFirst() {
    // --- Implementation based on Step 4 Skeleton ---
    // 1. Check if empty
    if (isEmpty()) {
        throw new NoSuchElementException("List is empty.");
    }
    // 2. Store element at index 0
    T removedItem = this.buffer[0];
    // 3. Shift elements 1..size-1 left (if any exist)
    if (this.size > 1) { // Check if shifting is needed
        shiftLeft(0); // Call helper
    }
    // 4. Decrement size
    this.size--;
    // 5. Null out the now-unused slot at the new end
    this.buffer[this.size] = null; // Help GC
    // 6. Return the stored element
    return removedItem;
}

@Override
public T removeLast() {
    // --- Implementation based on Step 4 Skeleton ---
    // 1. Check if empty
    if (isEmpty()) {
        throw new NoSuchElementException("List is empty.");
    }
    // 2. Calculate last index
    int lastIndex = this.size - 1;
    // 3. Store element at last index
    T removedItem = this.buffer[lastIndex];
    // 4. Decrement size *before* nulling out
    this.size--;
    // 5. Null out the now-unused slot (at the new size, which was the old lastIndex)
    this.buffer[this.size] = null; // Help GC - No shifting needed!
    // 6. Return the stored element
    return removedItem;
}

@Override
public boolean addAfter(T existing, T item) {
    // --- Implementation based on Step 4 Skeleton ---
    // 1. Check null arguments
    if (existing == null || item == null) {
        throw new IllegalArgumentException("Existing item and new item cannot be null.");
    }
    // 2. Find index of existing item
    int foundIndex = indexOf(existing); // Reuse indexOf()

    // 3. If found...
    if (foundIndex >= 0) {
        int insertionIndex = foundIndex + 1;
        // Index bounds check (0 <= insertionIndex <= size)
        // Note: If foundIndex is valid (0 to size-1), then 
        // insertionIndex (1 to size) is always <= size.
        // We only need to ensure it's not > size, which it won't be.
        // The lower bound (>=0) is also guaranteed.

        // Ensure capacity (Call helper)
        growIfNeeded(); 

        // Shift right to make space (Call helper - handles no-shift case)
        shiftRight(insertionIndex); 

        // Place item
        this.buffer[insertionIndex] = item;

        // Increment size
        this.size++;
        return true;
    } else {
        // 5. If not found...
        return false;
    }
}
The implementations for removeFirst and removeLast show the explicit sequence of checks and helper calls needed for removing from the ends. The addAfter method illustrates building functionality by combining previously implemented and tested methods (indexOf and add).

Subsection 12.22.4 Implementing clear()

Finally, the clear method needs to reset the list to an empty state. As planned in Step 4, this involves resetting the size and optionally nulling out array slots to help the garbage collector.
// Inside the ArrayList<T> class...

@Override
public void clear() {
    // --- Implementation based on Step 4 Skeleton ---

    // 1. Optional (Helps GC): Null out references in the array slots
    //    that were actually used (0 to size-1).
    for (int i = 0; i < this.size; i++) {
        this.buffer[i] = null;
    }

    // 2. Reset the size to 0.
    this.size = 0;

    // Note: We could also resize buffer back to DEFAULT_CAPACITY here
    //       if memory usage is a major concern, but it's not strictly required
    //       by the ADT contract. We'll omit that for simplicity now.
}

Subsection 12.22.5 Run All Tests!

Congratulations! You have now implemented skeletons for all methods defined in the ListADT<T> interface. This is the moment of truth.
Go to the VSCode Test Explorer and run the entire test suite. If your implementation correctly follows the logic planned in the skeletons and accurately reflects the ListADT contract (including all edge cases and error handling), all tests should now pass (✅✅✅)!
If you still have failing tests (❌), carefully examine the failure messages:
  • Which test method failed? What scenario was it testing?
  • Was it an AssertionError (wrong result)? What was expected vs. actual?
  • Was it an unexpected exception (like NullPointerException)? Where did it occur in your code (check the stack trace)?
  • Was it a missing expected exception (like IndexOutOfBoundsException)? Did your checks fail?
Use the detailed examples from Step 3 and the logic from your Step 4 skeletons to debug your implementation in ArrayList.java until all tests pass. Getting a completely "green" test bar is the goal.

Note 12.22.2. Code Snapshot.

You should now have a fully working implementation passing all tests! The state of the code after completing this section can be found on the dr-step-5.4-remaining-impl branch.
Once all tests pass, you have a working, robust implementation of a generic ArrayList based on the provided specification. The final part of Step 5 involves reflecting on the implementation and considering potential refinements or improvements, which we will do in the next section.
You have attempted of activities on this page.