Lecture 17 Array-Backed Lists

Joseph Haugh

University of New Mexico

Free Recall

  • Get out a sheet of paper or open a text editor
  • For 2 minutes write down whatever comes to mind about the last class
    • This could be topics you learned
    • Questions you had
    • Connections you made

Today’s Topics

  • The limitation of arrays: fixed size
  • Lists: a sequence that can grow and shrink
  • Implementing a list using a class with an array inside
  • How the list grows when it runs out of space
  • Exceptions: signaling errors at runtime

The Problem with Arrays

  • Suppose you want to record scores as students submit them
  • You don’t know ahead of time how many students there are
int[] scores = new int[30]; // guess: at most 30 students?
int count = 0;

The Problem with Arrays

int[] scores = new int[30];
int count = 0;

scores[count] = 92; count++;
scores[count] = 87; count++;
scores[count] = 74; count++;
// ... keep going ...
scores[count] = 81; count++; // 31st student: ArrayIndexOutOfBoundsException!
  • If you guess too small, you crash when you exceed the limit
  • If you guess too large, you waste memory and still have to track count yourself
  • The array’s size is fixed at creation; you cannot add more room later

What We Really Want: A List

  • A list is a sequence of values that can:
    • grow as you add elements
    • shrink as you remove elements
    • be accessed by index, just like an array
  • You should never need to worry about running out of space
  • Java’s standard library has ArrayList for exactly this, but today we’ll build one ourselves to understand how it works

IntList: The Big Picture

  • We’ll build an IntList class that acts like a resizable list of int values
  • Internally it still uses an array, but the class manages all the bookkeeping
  • When the array fills up, the class automatically allocates a bigger one
  • From the outside, it just looks like a list that can hold as many ints as you need

IntList: Fields and Constructor

public class IntList {
    private static final int INITIAL_CAPACITY = 10;

    private int[] xs;
    private int nextIndex;

    public IntList() {
        xs = new int[INITIAL_CAPACITY];
        nextIndex = 0;
    }
}
  • xs is the backing array: the actual storage
  • nextIndex tracks how many elements have been added
  • The constructor starts with a small array and an empty list

IntList: size() and isEmpty()

public class IntList {
    // fields, constructor ...

    public int size() {
        return nextIndex;
    }

    public boolean isEmpty() {
        return size() == 0;
    }
}
  • nextIndex always equals the number of elements, so size() just returns it
  • isEmpty() reuses size() rather than checking nextIndex directly

IntList: add()

public class IntList {
    // fields and constructor ...

    public void add(int x) {
        growIfFull();
        xs[nextIndex] = x;
        nextIndex++;
    }
}
  • Before adding, it calls growIfFull() to ensure there is room
  • Then it stores x at position nextIndex and advances the counter

IntList: Growing the Backing Array

public class IntList {
    // fields, constructor, add ...

    private void growIfFull() {
        if (nextIndex >= xs.length) {
            int[] newXs = new int[xs.length * 2];
            for (int i = 0; i < xs.length; i++) {
                newXs[i] = xs[i];
            }
            xs = newXs;
        }
    }
}
  • If the array is full (nextIndex >= xs.length), allocate a new array twice as large
  • Copy every element from the old array into the new one
  • Point xs at the new array; the old one is discarded
  • Doubling keeps the average cost of add() low; most calls do no copying at all

IntList: get()

public class IntList {
    // fields, constructor, add, growIfFull ...

    public int get(int i) {
        if (i < 0 || i >= size()) {
            throw new IndexOutOfBoundsException(
                "Index " + i + " out of bounds for size " + size());
        }
        return xs[i];
    }
}
  • Checks that i is a valid index before accessing the array
  • If not, it throws an exception to signal the error (more on this shortly)
  • Otherwise returns xs[i] directly

IntList: pop()

public class IntList {
    // fields, constructor, add, growIfFull, get ...

    public int pop() {
        if (nextIndex <= 0) {
            throw new IllegalStateException("List is empty");
        }
        int result = xs[nextIndex - 1];
        nextIndex--;
        return result;
    }
}
  • Removes and returns the last element of the list
  • Decreasing nextIndex is all it takes; the value in xs is simply ignored from now on
  • Throws an exception if the list is already empty

Using IntList

void main() {
    IntList scores = new IntList();

    scores.add(92);
    scores.add(87);
    scores.add(74);

    IO.println(scores.size());   // 3
    IO.println(scores.get(0));   // 92
    IO.println(scores.get(2));   // 74

    int last = scores.pop();
    IO.println(last);            // 74
    IO.println(scores.size());   // 2
}
  • No need to track a count variable yourself
  • No need to declare a size upfront; the list grows as needed

Only Works for ints…

  • IntList only stores int values
  • If you wanted a list of String values you’d have to write a separate StringList
  • And another DoubleList for doubles, and so on
  • Java has a feature called generics that lets you write one class that works for any type: List<Integer>, List<String>, List<Double>
  • We won’t cover generics in detail today, but this is exactly what Java’s ArrayList uses under the hood

Exceptions

  • An exception is how Java signals that something went wrong at runtime
  • get(-1) is not a logic error you can prevent in the compiler; it depends on what value i has when the program runs
  • When an exception is thrown, the program stops at that point and prints an error message
  • You have already seen this: ArrayIndexOutOfBoundsException, NullPointerException
  • You can throw your own exceptions with throw new ExceptionType("message")

Throwing an Exception

if (i < 0 || i >= size()) {
    throw new IndexOutOfBoundsException(
        "Index " + i + " out of bounds for size " + size());
}
  • throw stops execution immediately and sends the exception up to the caller
  • The string message helps whoever reads the error understand what went wrong
  • Common exception types:
    • IndexOutOfBoundsException: bad index
    • IllegalStateException: operation not valid in current state
    • IllegalArgumentException: caller passed an invalid argument

Summary

Arrays vs Lists

Array IntList
Size Fixed at creation Grows automatically
Access by index Yes Yes
Track count Manual Built-in (size())
Add beyond capacity Crash Handled

IntList Operations

  • add(x): append to end
  • get(i): read element at index
  • pop(): remove and return last
  • size(): number of elements
  • isEmpty(): true if no elements
  • indexOf(x): returns the index of the first occurrence of x