Lecture 8: Enlisted To List! (Part II)

Array Lists - Implementation

Before we start designing our first data structure in class, you should know something…

…these data structures are very popular… so popular, that Java (and most other languages) include packages for every ADT we discuss in this course.

The Java collections framework is a library of implemented collection ADTs, one of which is the java.util.ArrayList class (among others).

That said, since this is a course on data-structure first-principles, we will be largely designing these collections ourselves.

With the disclaimer done, let’s summarize our quest:

Example

Design an implementation of the array list ADT that supports dynamic sizing (arbitrary length) and relative item ordering for ints. Call this the IntArrayList class.

Here’s an interface to support the basic operations, as noted earlier:

public interface IntList {
 
    public int  getAt (int index);
    public void append (int toAdd);
    public void insertAt (int toAdd, int index);
    public void removeAt (int index);
  
}

Step 1: Design Fields

We said that an array is almost what we want for an array list, so we’ll use an array of ints as a basis field. We can then use dynamic allocation to “grow” the array as items are inserted.

This gives us the following scaffold, in which we have 2 fields: int[] items to hold the actual int values stored within, and size to record-keep how many are presently stored.

public class IntArrayList implements IntList {
 
    // Fields
    private int[] items;
    private int   size;
    
    ...
}

Step 2: Design Constructor

OK, pretty simple so far, but now we need to initialize our array and instantiate the size to 0 (assuming it’s empty from the start).

Insight: One of the chief challenges here will be choosing the starting size of the array data member "items"; reserve too much space and we waste memory, but too little and we'll run out of space very quickly.

Since we don’t know how many items the user will necessarily need in the ArrayList at the start, let’s just choose a starting allocation of room for 8 ints.

public class IntArrayList implements IntList {
 
    // Class Constants
    private static final int START_SIZE = 8;
    
    // Fields
    private int[] items;
    private int   size;
    
    // Constructor
    IntArrayList () {
        this.items = new int[START_SIZE];
        this.size  = 0;
    }
    
    ...
}

So far so good! (notice I made the start size into a private static variable so that if I want to change it in the future, I only have to change it in one place!)

Now, let’s start with some easy methods and see the challenges that might arise:

Step 3: Design Methods

Example

Let’s start with the public void append(int toAdd); method that adds the given int toAdd to the end of the ArrayList.

This raises the question of what we consider the “end” of our ArrayList, since this will be the next-available index that should become valid.

Insight: here's where we get some added utility from our size field: it also record-keeps the next available index at the end of the valid indexes!

...
 
    public void append (int toAdd) {
        // [!] Warning: something missing here...
        this.items[this.size] = toAdd;
        this.size++;
    }
 
...

Common Misconception: note here that the size field and items.length properties will only be the same if the items array is full!

In other words, since our starting size is room for 8 ints, then as soon as we want to add a 9th, we have to grow our storage to accommodate more.

“Growing” our list means that we need to reserve more space in our items array, so we’ll be clever and make a private helper that uses dynamic allocation:

...
 
    /**
    * Expands the size of the list whenever it is at
    * capacity
    */
    private void checkAndGrow () {
        // Case: big enough to fit another item, so no
        // need to grow
        if (this.size < this.items.length) {
            return;
        }
        
        // Case: we're at capacity and need to grow
        // Step 1: create new, bigger array; we'll
        // double the size of the old one
        int[] newItems = new int[this.items.length * 2];
        
        // Step 2: copy the items from the old array
        for (int i = 0; i < this.items.length; i++) {
            newItems[i] = this.items[i];
        }
        
        // Step 3: update IntArrayList reference
        this.items = newItems;
    }
 
...

Note the method signature here denotes the method as private - private methods within classes are something we refer to as "helper methods": methods written within a class that are only intended to be used within the class' methods, and is not to be called upon via an object itself.

Now, whenever we want to add an item, we can just call checkAndGrow()!

So, let’s circle back to append:

...
 
    public void append (int toAdd) {
        checkAndGrow(); // [!] Fixed!
        this.items[this.size] = toAdd;
        this.size++;
    }
 
...

How simple! We exploit the fact the the size field will always have the index of the “last” spot in our items array, and so the append operation can easily stash the new entry there (without worrying about moving anything down).

For example, if we call append once after creating a new IntArrayList, we will have room for 7 more before we need to grow again.

Example

Let’s try to implement the public int getAt(int index) method next — this one is pretty simple since we’re using arrays… but with one catch!

As such, we’ll need to throw an exception whenever it isn’t!

...
 
    public int getAt (int index) throws IllegalArgumentException {
        if (index < 0 || index >= this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        return this.items[index];
    }
 
...

Note the added throws IllegalArgumentException syntax in the method signature above: this alerts the compiler that there is some control path that leads to a known exception, which is useful for some testing purposes later.

Example

We can test our class briefly with some basic empirical tests if we’d like! (just to make sure we’re on track before we’d circle back with lots of unit tests)

public class IntArrayListTests {
 
    public static void main (String[] args) {
        IntArrayList test = new IntArrayList();
        test.append(2);
        test.append(4);
        test.append(8);
        System.out.println(test.getAt(1)); // 4
    }
 
}

Everything looks good! On to the next…

Example

Next is the slightly-less-trivial public void removeAt(int index), which removes an item from the IntArrayList at the specified index.

This is less trivial because when we remove an item at an arbitrary index, we cannot leave a gaping hole in our array — space is precious after all, and we need to maintain our list!

We want to maintain that only indices 0 <= index < size are valid entries in the IntArrayList.

As such, we can simply shift all of the items in our array left from the point of deletion. Let’s make a helper method for this!

...    
    public void removeAt (int index) throws IllegalArgumentException {
        if (index < 0 || index >= this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        shiftLeft(index);
        this.size--;
    }
    
    ...
    
    /**
    * Shifts all elements to the right of the given
    * index one left
    *
    * @param index Index at which to shift all elements to the right left by 1
    */
    private void shiftLeft (int index) {
        for (int i = index; i < this.size-1; i++) {
            this.items[i] = this.items[i+1];
        }
    }
...

Testing our changes, just at a glance:

public class IntArrayListTests {
 
    public static void main (String[] args) {
        IntArrayList test = new IntArrayList();
        test.append(2);
        test.append(4);
        test.append(8);
        test.removeAt(1);
        System.out.println(test.getAt(1)); // 8
    }
 
}

Excellent! Our class is coming along! We made append, how about we try prepend?

Example

Implement public void prepend (int toAdd), which inserts an integer toAdd into our ArrayList at the index 0, placing it before all of the existing elements in the list.

There’s a few important things to remember for this one:

And we’re just inserting the item at the index 0, so it should be as easy as…

...
    public void prepend (int toAdd) {
        checkAndGrow();
        // [!] Warning: something missing here...
        this.items[0] = toAdd;
        this.size++;
    }
...

Hmm, but there’s one little snag in our current implementation…

...
    public void prepend (int toAdd) {
        checkAndGrow();
        for (int i = size; i > 0; i--) {
            items[i] = items[i - 1];
        }
        this.items[0] = toAdd;
        this.size++;
    }
...

While we’re handling insertion at 0, let’s handle Arbitrary Insertion!

Example

Implement public void insertAt (int toAdd, int index), which inserts an integer toAdd into our ArrayList at the specified index, maintaining the relative order of other elements around it.

There’s a few important things to remember for this one:

Much like prepend, we'll need to checkAndGrow() and we'll need to make room for the item we're inserting so we don't overwrite what was originally there!

...
    public void insertAt (int toAdd, int index) {
        if (index < 0 || index > this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        checkAndGrow();
        for (int i = size; i > index; i--) {
            items[i] = items[i - 1];
        }
        this.items[index] = toAdd;
        this.size++;
    }
...

Looks great! But if we compare insertAt() to prepend(), what do we notice?

So to avoid repetition (and to keep our code DRY), let’s pull that out into a helper method similar to our prior shiftLeft!

...
    public void prepend (int toAdd) {
        checkAndGrow();
        shiftRight(0);
        this.items[0] = toAdd;
        this.size++;
    }
 
    public void insertAt (int toAdd, int index) {
        if (index < 0 || index > this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        checkAndGrow();
        shiftRight(index);
        this.items[index] = toAdd;
        this.size++;
    }
    
    ...
 
    private void shiftRight(int index) {
        for (int i = size; i > index; i--) {
            items[i] = items[i - 1];
        }
    }
...
    public void prepend (int toAdd) {
        insertAt(toAdd, 0);
    }
 
    public void insertAt (int toAdd, int index) {
        if (index < 0 || index > this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        checkAndGrow();
        shiftRight(index);
        this.items[index] = toAdd;
        this.size++;
    }
...

And that’s our ArrayList!

(Mostly) Completed IntArrayList Code

public class IntArrayList implements IntList {
 
    private static final int START_SIZE = 8;
 
    private int size;
    private int[] items;
 
    IntArrayList () {
        this.items = new int[START_SIZE];
        this.size = 0;
    }
 
    public void append (int toAdd) {
        checkAndGrow();
        this.items[this.size] = toAdd;
        this.size++;
    }
 
    public int getAt (int index) {
        if (index < 0 || index >= this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        return this.items[index];
    }
 
    public void removeAt (int index) {
        if (index < 0 || index >= this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        shiftLeft(index);
        size--;
    }
 
    private void shiftLeft (int index) {
        for (int i = index; i < this.size-1; i++){
            this.items[i] = this.items[i+1];
        }
    }
 
    public void prepend (int toAdd) {
        insertAt(toAdd, 0);
    }
 
    public void insertAt (int toAdd, int index) {
        if (index < 0 || index > this.size) {
            throw new IllegalArgumentException("Requested index out of range");
        }
        checkAndGrow();
        shiftRight(index);
        this.items[index] = toAdd;
        this.size++;
    }
 
    private void shiftRight(int index) {
        for (int i = size; i > index; i--) {
            items[i] = items[i - 1];
        }
    }
 
    @Override
    public String toString () {
        String[] result = new String[this.size];
        for (int i = 0; i < this.size; i++) {
            result[i] = "" + this.items[i];
        }
        return "[" + String.join(", ", result) + "]";
    }
 
    private void checkAndGrow() {
        if (this.size < this.items.length) {
            return;
        }
 
        int[] newItems = new int[this.items.length * 2];
 
        for (int i=0; i<this.items.length; i++) {
            newItems[i] = this.items[i];
        }
 
        this.items = newItems;
    }
 
}

And there you have it! Your first, simple data structure.

Short Debugger Tutorial

Example

Let’s add a short toString override to our IntArrayList to showcase the power of our debugger!

@Override
public String toString () {
    String[] result = new String[this.size];
    for (int i = 0; i < this.size; i++) {
        result[i] = "" + this.items[i];
    }
    return String.join(", ", result);
}

A debugger is a tool used to inspect running portions of code to verify correct functionality.

Most IDEs come equipped with debuggers, though there are standalone versions as well.

Mastering the debugger is an important tool in your coding arsenal, so we’ll look at an example together in class… on VSCode of course.

In any debugger, however, there are essentially 3 main components:

(1) Set breakpoints where you want execution of your code to pause for inspection.

Generally, these are set by clicking on the space to the left of your IDE’s line numbers.

I’m going to add two more breakpoints by the for loop declaration and the return statement in our toString - we’ll see why in a moment.

(2) Inspection panels allow you to see the state of the stack, local variables, and state of your objects from within methods.

Once you have set breakpoints in parts of interest for your code, you can find the “Start Debugger” mechanism to open your inspection panels, and run your code (stopping at breakpoints).

In VSCode with Java, we can do this in the Testing tab! Instead of hitting the play button we’ve been using to run tests, we can just hit the button next to it with a little bug icon, and it’ll launch the debugger!

(3) Step tools allow you to move your code incrementally forward to carefully inspect the changes of variables.

At the top of your screen, you’ll see a little toolbar appear, and then you’ll notice we got a yellow line by one of our breakpoints.

What’s happening here is our code started executing, and then stopped just before running the line we set our breakpoint on.

If we press the Play button (Continue), it’ll continue execution until our next breakpoint.

Now it’s jumped to that for loop we put a breakpoint at - now on the left tab of your screen there should be a heading that says “Variables”:

This is why we use the debugger. This panel shows us the exact state of every variable in our program at this specific moment of execution.

When I press the Continue button again, it jumps to the return statement:

And look at that! I can see that the result array populated with the strings I wanted to add to it!

Set a breakpoint in the loop of the toString method and then step through them to observe how the debugger works!