Layers of Logic

10.3

The Collection Interface

Use the core Collection operations, bulk methods, array conversion, and optional operations shared across collection types.

Core16 min read3 exercises

Before you start

Topics used in this lesson

01

Prerequisites

Section 10.2 showed you Iterable, which promises exactly one thing: you can walk me.

That is not enough to be useful. You cannot add to an Iterable, remove from it, or ask how big it is. Collection is the next level down, and it adds all of that.

02

Why this matters

You are writing a method that reports on a group of things.

static void report(ArrayList<String> items) { ... }

That works, and it only works for one class. Somebody with a HashSet cannot call it. Neither can somebody with a LinkedList.

You could write the method three times. Or you could ask what all three have in common, and take that instead.

static void report(Collection<String> items) { ... }

Now every one of them fits. The question this section answers is: what exactly are you allowed to do with that parameter?

03

Core explanation

Collection holds the methods that make sense for every container, and nothing else.

Collection<Integer> c = new ArrayList<>();
c = new LinkedList<>();
c = new HashSet<>();
c = new ArrayDeque<>();      // all of these compile

Whatever is really behind the variable, the same methods are available. That is polymorphism from Section 7.2, applied across a whole framework.

The methods, in groups

GroupMethods
How big is itbasic questionssize(), isEmpty()
Is this in itsearchingcontains(o), containsAll(c)
Change itone at a timeadd(e), remove(o), clear()
Change it in bulkwhole collectionsaddAll(c), removeAll(c), retainAll(c), removeIf(p)
Walk itfrom 10.2iterator(), forEach(action)
Get it outconversiontoArray(), stream()

The rule for what belongs here

One question decides it: does this make sense for every collection?

MethodWhere it lives, and why
size()every container has a countCollection
contains(o)every container can be askedCollection
get(3)a Set has no positionsList only
add(3, e)insert at a positionList only
peek()who is next in lineQueue only

This keeps Collection small and honest. If a method were promoted here that some implementation cannot support, that implementation would have to throw an exception from a method it was forced to declare.

04

Optional internals

Core
Open optional internalsYou can finish the core lesson without this section.

The three bulk methods

These are worth knowing because they replace loops you would otherwise write by hand.

Collection<Integer> c = new ArrayList<>(List.of(1, 2, 3, 4, 5));

c.retainAll(List.of(2, 4, 9));      // [2, 4]        keep only what is in both
c.removeAll(List.of(2, 4));         // [1, 3, 5]     remove everything listed
c.containsAll(List.of(1, 3));       // true          are all of these present

retainAll is an intersection. removeAll is a difference. containsAll is a subset test. Three set operations, available on every collection.

Why add returns a boolean

List<Integer> list = new ArrayList<>();
list.add(3);       // true
list.add(3);       // true. A list takes duplicates.

Set<Integer> set = new HashSet<>();
set.add(3);        // true
set.add(3);        // false. Already there.

The boolean means did the collection change because of this call. On a List it is always true, which makes it look pointless until you meet a Set.

The overload trap, in a new place

You met this in Section 7.3. Here it is on a method you will genuinely call.

List<Integer> list = new ArrayList<>(List.of(10, 20, 30, 40));

list.remove(1);                    // removes position 1. Returns 20.
list.remove(Integer.valueOf(1));   // removes the value 1. Not present, returns false.

List has two remove methods:

E       remove(int index);      // from List
boolean remove(Object o);       // from Collection

Passing a plain int matches remove(int) in the first resolution pass from Section 5.1, using no conversion at all. Boxing is never considered.

Notice the return types differ too. One hands back the removed element, the other hands back a boolean. Same name, two completely different jobs.

Not every collection supports every method

List<Integer> fixed = List.of(1, 2, 3);
fixed.add(4);      // UnsupportedOperationException

List.of gives you an unmodifiable list. It implements List, so it must declare add. It cannot support it, so it throws.

This is the cost of a wide interface, and it is the exact problem the “does it make sense for everyone” rule exists to limit. add genuinely does make sense for most collections, so it stays on Collection, and the unmodifiable ones throw.

Worth knowing: List.of, Set.of and Map.of all produce unmodifiable collections. If you need to change one, wrap it:

List<Integer> changeable = new ArrayList<>(List.of(1, 2, 3));

That is why the exercises in this course keep writing new ArrayList<>(List.of(...)) rather than List.of(...) on its own.

Methods added later

Three of the methods in the table did not exist before Java 8: removeIf, forEach and stream.

They were added as default methods, from Section 8.5, which is the only reason they could be added at all. Every class implementing Collection anywhere in the world kept compiling.

Without that feature, stream() could never have been put on Collection, and Phase XI would look completely different.

05

Tradeoffs

A wide interface forces every implementation to declare everything, even the parts it cannot do. List.of(1,2,3).add(4) compiles and throws, and no compiler warning tells you it will.

Collection also promises nothing about speed. contains is one check on a HashSet and a full walk on an ArrayList. Same method, same signature, wildly different cost, and the interface cannot say so.

Nor does it promise order. Walk a List twice and you get the same order. Walk a HashSet twice and you get whatever order the buckets happen to produce.

The remove overload is a genuine trap, and it is on one of the most used methods in the framework.

What you get is a method signature that accepts every container in Java. Write report(Collection<String>) once and it works for callers who have not chosen their data structure yet, or who change their mind later.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. What decides whether a method belongs on `Collection` rather than on `List` or `Set`?

    Show the answer

    One question: does it make sense for every collection?

    size() does. Every container has a count. contains() does. Every container can be asked whether something is in it. So both live on Collection.

    get(3) does not. A Set has no positions, so there is nothing for it to return. It lives on List.

    The rule keeps Collection small and honest. If a method were promoted here that some implementation cannot support, that implementation would have to throw an exception from a method it was forced to declare. The framework does have a few of those, and they are considered mistakes.

  2. `Collection.add` returns a `boolean`. When is it ever false?

    Show the answer

    When the collection refused the element.

    A List always accepts, so add is always true there. It is easy to conclude the return value is pointless.

    A Set rejects duplicates. set.add(3) on a set that already holds 3 returns false and changes nothing.

    The boolean means "did the collection change because of this call". That is genuinely useful: if (seen.add(x)) { } is a one line way to do something only the first time you meet a value.

  3. `list.remove(1)` and `list.remove(Integer.valueOf(1))` on a `List<Integer>`. What does each one do?

    Show the answer

    remove(1) removes the element at position 1. On [10, 20, 30, 40] it removes 20 and returns it.

    remove(Integer.valueOf(1)) tries to remove the value 1. It is not there, so it returns false and changes nothing.

    The cause is the overload rule from Section 5.1. List has both remove(int index) and remove(Object o). Passing a plain int matches remove(int) in the first resolution pass, using no conversion at all, so boxing is never considered.

    Notice the return types differ too: one gives you the removed element, the other gives you a boolean. Two methods, one name, completely different jobs.

07

Exercises

Start with the warm-ups. Continue to practice exercises when the basic idea works. Challenges are optional: skip them on a first pass and return after the next lesson.

  1. Warm up: one idea with close guidance.
  2. Practice: combine the idea with earlier material.
  3. Challenge: investigate an edge case or implementation detail.

3 exercises55 pointsabout 60 minutes

Coding exercises provide starter code and concrete success checks. Exercises marked checked can also be run through the VS Code extension. Concept questions are selectable quizzes that give immediate feedback on this page.
A

Choose Between List.remove Overloads

Practice·20 min·20 points

checkedex-10-3-a

The overload trap from Phase VII, on a method you will call constantly.

Predict both lines, including what each one returns. The return types are the giveaway: one hands back the removed element, the other hands back a boolean. Two methods that happen to share a name.

Then do both operations deliberately and notice how differently the two calls have to look.

Finish with the Set case. The same call is unambiguous there, and working out why confirms you understand what the compiler is choosing between.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class RemoveTrap {
    static Integer removeAtPosition(List<Integer> l, int index)
    static boolean removeTheValue(List<Integer> l, int value)
    static boolean removeFromSet(Set<Integer> s, int value)
}

removeAtPosition returns the element that was there. removeTheValue returns whether anything was removed. Both change the list they are given.

Steps and success checks

  • Predict both results and both return types before running
  • Implement the three methods so value and position never get confused
  • Explain the choice using the three resolution passes from Phase V
  • Say why the same call on a Set has no ambiguity
RemoveTrap.java
import java.util.*;

public class RemoveTrap {

    // Take out whatever sits at this index, and return it.
    static Integer removeAtPosition(List<Integer> l, int index) {
        return null; // TODO
    }

    // Take out the element EQUAL to this value. Return whether anything went.
    static boolean removeTheValue(List<Integer> l, int value) {
        return false; // TODO
    }

    static boolean removeFromSet(Set<Integer> s, int value) {
        return false; // TODO
    }

    public static void main(String[] args) {
        List<Integer> a = new ArrayList<>(List.of(10, 20, 30, 40));
        List<Integer> b = new ArrayList<>(List.of(10, 20, 30, 40));

        // TODO: predict both, including the RETURN VALUES, before running
        System.out.println("a.remove(1)                  = " + a.remove(1) + "  -> " + a);
        System.out.println("b.remove(Integer.valueOf(1)) = " + b.remove(Integer.valueOf(1)) + "  -> " + b);

        // TODO: why does the same call on a Set have no ambiguity?
    }
}
Hint 1
There are two remove methods on List. remove(int) means the position. remove(Object) means the value.
Hint 2
Pass an int and the first pass finds an exact match on remove(int), so it never gets as far as considering boxing. To mean the value, box it yourself with Integer.valueOf(value).
Hint 3almost the answer
A Set has no positions, so Set declares only remove(Object). There is nothing for the compiler to choose between.
Why this exercise exists

This exercise checks whether you can apply overload resolution to the two List.remove methods.

B

The Bulk Methods Replace Loops

Warm up·20 min·15 points

checkedex-10-3-b

Three questions about two groups, and three methods that answer them in one line each.

Work on copies. Two of the three modify the collection you call them on, and that catches people who expect a new collection back.

Then write the intersection by hand and put the two versions next to each other. The hand version needs an iterator, because removing inside a for-each is the bug from Section 10.2.

That comparison is the argument for knowing these three exist.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class Bulk {
    static List<String> onDutyAndInjured(List<String> onDuty, List<String> injured)
    static List<String> onDutyNotInjured(List<String> onDuty, List<String> injured)
    static boolean allInjuredAreOnDuty(List<String> onDuty, List<String> injured)
}

All three must leave both lists exactly as they were found. retainAll and removeAll change the list you call them on, so copy first.

Steps and success checks

  • Use retainAll, removeAll and containsAll for the three questions
  • Copy the list before each bulk call, since they change the list they are called on
  • Write the intersection as a hand loop and compare the two versions
  • Say which bulk method surprised you by changing its receiver
Bulk.java
import java.util.*;

public class Bulk {

    // Who is on duty AND injured. Order follows onDuty.
    static List<String> onDutyAndInjured(List<String> onDuty, List<String> injured) {
        return List.of(); // TODO: retainAll
    }

    // Who is on duty but NOT injured.
    static List<String> onDutyNotInjured(List<String> onDuty, List<String> injured) {
        return List.of(); // TODO: removeAll
    }

    // Is every injured unit also on duty?
    static boolean allInjuredAreOnDuty(List<String> onDuty, List<String> injured) {
        return false; // TODO: containsAll
    }

    public static void main(String[] args) {
        List<String> onDuty  = new ArrayList<>(List.of("Atlas", "Beacon", "Cipher", "Drift"));
        List<String> injured = List.of("Beacon", "Ember");

        // TODO: print all three answers
        // TODO: write the intersection as a hand loop too, and compare the code
    }
}
Hint 1
retainAll keeps only what is in both. removeAll takes out everything in the other list. containsAll just answers yes or no.
Hint 2
All three of those first two work on the list you call them on, not on a copy. new ArrayList<>(onDuty) first, then call the method on the copy, then return the copy.
Hint 3almost the answer
The hand loop is four lines and does the same as one. The bulk version also says what it means, which the loop does not: a reader has to work out that the loop is an intersection.
Why this exercise exists

This exercise checks whether you know the three bulk operations exist.

C

Methods That Exist and Throw

Practice·20 min·20 points

checkedex-10-3-d

Nine combinations, and they do not all behave the same way.

Predict the whole grid before running. The middle column is the interesting one: Arrays.asList allows one operation and refuses another, and the reason is about what it is a view onto.

Then say what this shows about interface design. List promises add to everyone, and some implementations cannot keep that promise, so they throw instead.

That is the price of a wide interface, and it is exactly the problem the “does this make sense for everyone” rule was trying to limit.

Required class and methods

Keep these names and parameter types. The checker uses them to find your code.

class Unsupported {
    static String tryAdd(List<Integer> l)
    static String trySet(List<Integer> l)
    static String tryRemove(List<Integer> l)
}

Each one attempts the operation and returns either "ok" or "UnsupportedOperationException". Nothing else. tryAdd adds 99, trySet replaces slot 0 with 99, tryRemove takes out position 0.

Steps and success checks

  • Try add, set and remove on List.of, Arrays.asList and ArrayList
  • Predict all nine before running, then record which work and which throw
  • Explain why Arrays.asList allows set but refuses add
  • Say what this shows about the cost of a wide interface
Unsupported.java
import java.util.*;

public class Unsupported {

    // Attempt it. Return "ok" or "UnsupportedOperationException".
    static String tryAdd(List<Integer> l) {
        return "ok"; // TODO: add 99
    }

    static String trySet(List<Integer> l) {
        return "ok"; // TODO: put 99 at slot 0
    }

    static String tryRemove(List<Integer> l) {
        return "ok"; // TODO: take out position 0
    }

    public static void main(String[] args) {
        // TODO: predict all nine before running
        // TODO: print the nine results as a table
        // TODO: explain why Arrays.asList allows set but not add
    }
}
Hint 1
Wrap each attempt in try and catch UnsupportedOperationException. Return the name of the exception from the catch.
Hint 2
Arrays.asList is a window onto a real array. Replacing a slot is something an array can do. Adding a slot is not, because an array's length never changes.
Hint 3almost the answer
The cost is that a method exists on the type and still refuses to run. You cannot tell from the type whether add will work, so the failure arrives when the program runs rather than when it compiles.
Why this exercise exists

This exercise checks whether you understand the cost of a wide interface.

08

Where this leads

Collection left out everything about position. No get(3), no add(3, e), no indexOf.

Section 10.4 is where those come back, on List, along with the four implementations that support them: ArrayList, LinkedList, Vector and Stack.

That is also where the question from Section 4.2 finally gets settled. Contiguous memory against scattered nodes, random access against cheap insertion. You already have the theory, and you have already measured the 806 millisecond version in Section 10.2.

Two of the four implementations are historical mistakes that Java keeps for compatibility. Knowing which two, and why, separates people who use the framework from people who understand it.

Later connections