Skip to content

Many objects

50 minutesWeek 4, session 1Needs: What belongs to whom

By the end of this page you can hold a growing number of objects in a List, add to it, search it, remove from it, and work through it. You will also be able to say exactly what a list of accounts contains, which is not what most people assume.

One account is not a bank. A real system holds a number of objects that nobody knew in advance, and the tool for that in Java is the collection. Getting the mental picture right now saves you from a specific family of bugs in week 11.

Arrays run out first

You already know arrays, so the problem is quick to state.

Account[] accounts = new Account[3];
accounts[0] = new Account("Ada Lovelace", 500.00);
accounts[1] = new Account("Alan Turing", 2500.00);

Three slots, decided forever at the moment of creation. Open a fourth account and there is nowhere to put it. And accounts[2] right now is null, so anything that loops over this array and calls a method will fall over on the last element.

Removing is worse. There is no way to make an array shorter, so you either leave a hole or shift everything down by hand, which is the parallel-array problem from week 1 coming back for another go.

A list grows

import java.util.ArrayList;
import java.util.List;

List<Account> accounts = new ArrayList<>();

accounts.add(new Account("Ada Lovelace", 500.00));
accounts.add(new Account("Alan Turing", 2500.00));
accounts.add(new Account("Grace Hopper", 1500.00));

System.out.println(accounts.size());        // 3
System.out.println(accounts.get(0));        // Account AC-1001: 500.00 EUR

No size decided up front, no empty slots, no shifting.

List<Account> says this list holds accounts and nothing else. Put a String in and the compiler stops you, which is worth more than it sounds: the mistake is caught while you type rather than at 2am in week 11.

The declaration is worth a second look, because it is deliberately mismatched.

List<Account> accounts = new ArrayList<>();

The variable is a List, which is the promise: something you can add to, look through, and count. The object is an ArrayList, which is one particular way of keeping that promise.

Write it this way round from the start. The rest of your program only ever needs the promise, so if you later swap ArrayList for a different kind of list, one line changes instead of forty. Week 8 explains why Java lets you do this at all; for now it is a habit worth having early.

What the list actually holds

Here is the picture, and it is the same picture from week 2 with one more hop.

A list variable, the list object, and the accounts it refers toA variable named accounts holds an arrow to an ArrayList object. The ArrayList has three numbered slots, and each slot holds an arrow pointing out to a separate Account object. The accounts themselves sit outside the list.accountsArrayList012AC-1001AC-1002AC-1003500.002500.001500.00The accounts are not inside the list. The list holds the way to reach them.

The accounts are not in the list. The list holds arrows, and the accounts sit outside it, exactly where they were when new created them.

A list of objects is a list of arrows

This matters immediately.

Account ada = new Account("Ada Lovelace", 500.00);
accounts.add(ada);

ada.deposit(1000.00);

System.out.println(accounts.get(0).getBalance());   // 1500.00

add did not copy anything. The list and the variable ada both point at the same account, so a deposit through either one is visible through the other.

This is convenient more often than it is a problem. It stops being convenient in week 11, when you write the same object to a file twice and wonder why you have one account instead of two.

Working through them

The for-each loop is the normal way to visit every element.

for (Account account : accounts) {
    System.out.println(account);
}

Read it as "for each account in accounts". No index, no off-by-one, no way to run off the end.

The loop variable is another arrow to the same object, so this really does change the accounts:

for (Account account : accounts) {
    account.deposit(10.00);      // every account gains 10
}

But this does not:

for (Account account : accounts) {
    account = new Account("Nobody", 0.00);   // changes nothing
}

The loop variable gets pointed somewhere else and the list is untouched. Same rule as the method parameter in week 2: you can change the object, you cannot change which object the list is holding.

Use an ordinary indexed loop when you need the position:

for (int i = 0; i < accounts.size(); i++) {
    System.out.println(i + ": " + accounts.get(i));
}

Searching and removing

Account first = accounts.get(0);

accounts.remove(0);              // by position
accounts.remove(first);          // by object
System.out.println(accounts.contains(first));
System.out.println(accounts.isEmpty());

remove(int) and remove(Object) are different methods that happen to share a name, which is a design wart in Java worth knowing about. With a List<Account> it is unambiguous. With a List<Integer> it is a genuine trap.

More importantly, remove(Object) and contains do not compare account numbers. They ask each element whether it equals the one you passed, and without any instruction from you, "equals" still means "is the same object" from week 2. Two accounts with identical data are not the same account as far as these methods are concerned.

That is often what you want. When it is not, you teach Java what sameness means for your class, which is week 5.

So for now, search by writing the loop yourself:

public static Account findByNumber(List<Account> accounts, String number) {
    for (Account account : accounts) {
        if (account.getAccountNumber().equals(number)) {
            return account;
        }
    }
    return null;
}

.equals on the strings, never ==. And null when nothing matched, which is honest but puts the burden on the caller. Week 9 offers something better.

Do not remove from a list while looping over it

for (Account account : accounts) {
    if (account.getBalance() == 0) {
        accounts.remove(account);      // ConcurrentModificationException
    }
}

The loop is walking the list while you rearrange it underneath. Java notices and throws.

The simplest fix for now is to collect first and remove afterwards:

List<Account> empty = new ArrayList<>();
for (Account account : accounts) {
    if (account.getBalance() == 0) {
        empty.add(account);
    }
}
accounts.removeAll(empty);

Week 12 gives you a one-line version of this.

A list that cannot change

List<String> accountTypes = List.of("CHECKING", "SAVINGS", "DEPOSIT");

List.of makes a fixed list. Call add on it and it throws. That is the immutability idea from last session applied to a collection: a list of things that were decided once should not be a list anyone can extend.

Use it for fixed sets of values. Use new ArrayList<>() for anything that grows.

If you remember one thing. A list of objects is a list of arrows. The objects live outside it, and adding one to a list does not copy it.

Check yourself

Prove the list holds arrows

List<Account> accounts = new ArrayList<>();

Account ada = new Account("Ada Lovelace", 500.00);
accounts.add(ada);
accounts.add(new Account("Alan Turing", 2500.00));

ada.deposit(1000.00);

for (Account account : accounts) {
    System.out.println(account);
}

Before running, write down what the first line of output will be. If you expected 500.00, look at the diagram again before reading the actual answer.

Then add a second account object with the same opening balance as one already in the list, and check accounts.contains(...) with it. Predict the answer first. Then say in one sentence which week 2 idea explains it.

Think about it

  1. An array's size is fixed and a list's is not, which makes the list sound strictly better. Arrays are still everywhere in Java. Find a reason somebody would choose one, beyond "it was already there".

  2. You wrote List<Account> accounts = new ArrayList<>() rather than ArrayList<Account> accounts. Nothing in your program behaves differently today. What exactly did you buy, and when would you find out whether it was worth it?

  3. contains compares identity, so an account you just rebuilt from a file will not be found even with the same number. Whose problem is that: the list's, the account's, or the caller's? Say what you would change and what it would cost.

  4. findByNumber returns null when nothing matches. Every caller now has to remember to check. Suggest a different thing it could return, and say what makes your suggestion better or worse than null.