Java Arena
Step 5 of 5

Putting it together: what the compiler checks

This step builds one more generic class with tools from earlier modules, and then looks at the biggest gain from type parameters: the compiler checks the types for you, so many mistakes can't even run.

A history of the last few items

A web shop shows the products you viewed most recently, and a vet's app keeps a dog's last few weights, in kilograms. Both keep the newest few items of some type and forget older ones:

Main.java
public class Main {
    public static void main(String[] args) {
        History<String> viewed = new History<>(3);
        viewed.add("lamp");
        viewed.add("rug");
        viewed.add("kettle");
        viewed.add("mirror");
        System.out.println(viewed);
        System.out.println("Latest: " + viewed.latest());

        History<Double> weights = new History<>(2);
        weights.add(18.5);
        weights.add(21.0);
        weights.add(19.5);
        System.out.println(weights);

        try {
            History<Integer> none = new History<>(0);
            System.out.println(none);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}
History.java
import java.util.ArrayList;

public class History<T> {
    private ArrayList<T> items;
    private int limit;

    public History(int limit) {
        if (limit < 1) {
            throw new IllegalArgumentException("A history must keep at least 1 item, not " + limit);
        }
        this.items = new ArrayList<>();
        this.limit = limit;
    }

    public void add(T item) {
        this.items.add(item);
        if (this.items.size() > this.limit) {
            this.items.remove(0);
        }
    }

    public T latest() {
        if (this.items.isEmpty()) {
            throw new IllegalStateException("The history is empty");
        }
        return this.items.get(this.items.size() - 1);
    }

    @Override
    public String toString() {
        return this.items.toString();
    }
}
Output
[rug, kettle, mirror]
Latest: mirror
[21.0, 19.5]
A history must keep at least 1 item, not 0
  • Everything in History comes from earlier modules: an ArrayList, removing its first element, toString, and the exceptions of module 42. The only new part is T.
  • add puts the new item at the end, and when the list has grown past the limit, it removes the oldest, at index 0. remove(0) removes by index even in a History<Integer>, because 0 is an int.
  • The constructor refuses a limit below 1, and latest() on an empty history throws an IllegalStateException. A generic class checks its values like any other.
  • toString hands the job to the list's own toString, which calls toString on each item.

The compiler checks the types

A History<Double>'s add takes only Doubles, and a Box<Integer> from step 1 holds only Integers. Try to put a String in one, or take a String out of one, and the program doesn't compile:

Main.java
public class Main {
    public static void main(String[] args) {
        Box<Integer> coins = new Box<>(340);
        coins.setContent("a lot");
        String count = coins.getContent();
        System.out.println(count);
    }
}
Box.java
public class Box<T> {
    private T content;

    public Box(T content) {
        this.content = content;
    }

    public T getContent() {
        return this.content;
    }

    public void setContent(T content) {
        this.content = content;
    }
}
What javac prints
Main.java:4: error: incompatible types: String cannot be converted to Integer
        coins.setContent("a lot");
                         ^
Main.java:5: error: incompatible types: Integer cannot be converted to String
        String count = coins.getContent();
                                       ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
2 errors

With step 1's ObjectBox, javac would accept the first line, and the second too once it had a cast, (String) coins.getContent(). Neither mistake would be caught before the program ran. The cast fails with a ClassCastException when the box still holds 340, and a String put into a box of coins goes unnoticed until some code takes it out as an Integer, maybe much later and in a different place. With a type parameter, javac finds both before the program ever runs, and points at the very lines. That's the main reason type parameters exist.

The checks go everywhere the types go: a Rule<Integer> can't be passed to a method that wants a Rule<String>, and a Duo<String, Integer>'s swapped() can't be stored in a Duo<String, Integer> variable.

Inside the class, T could be anything

The checks work in the other direction too. Inside Box, nobody knows yet what T will be, so this doesn't compile:

Main.java
public class Main {
    public static void main(String[] args) {
        Box<String> pegs = new Box<>("tent pegs");
        System.out.println(pegs.letters());
    }
}
Box.java
public class Box<T> {
    private T content;

    public Box(T content) {
        this.content = content;
    }

    public int letters() {
        return this.content.length();
    }
}
What javac prints
Box.java:9: error: cannot find symbol
        return this.content.length();
                           ^
  symbol:   method length()
  location: variable content of type T
  where T is a type-variable:
    T extends Object declared in class Box
1 error

This box happens to hold a String, but another Box could hold an Integer, which has no length(). The last lines of the message say what javac knows about T: T extends Object, some type of object or other. So inside a generic class, a value of type T can do only what every object can: the methods of Object, such as toString() and equals. That's why History, Rotation and OneOf work for any type: they store values, hand them back, print them and compare them with equals, and that's all. (Java can also promise more about T, as in <T extends Comparable<T>>, a type parameter that must be Comparable. That's called a bounded type parameter. You saw one in module 38's long error message; this course doesn't need them.)

Your task

Challenge 1 of 3

A drawing app lets you undo changes to the brush. The brush color is a String and its size an Integer, and one class handles both. In Undoable.java, write the class Undoable<T> with:

  • a constructor Undoable(T start), which takes the first value
  • get(), which returns the current value
  • set(T value), which changes the value and remembers the old one
  • undo(), which goes back to the value before the last set. Each undo goes one step further back. When only the first value is left, undo throws an IllegalStateException with the message Nothing to undo, and the value stays as it is.

Keep the values in an ArrayList<T>, the current one last. The check runs:

Undoable<String> color = new Undoable<>("black");
color.set("red");
color.set("blue");
color.undo();
System.out.println(color.get());
color.undo();
System.out.println(color.get());
try {
    color.undo();
} catch (IllegalStateException e) {
    System.out.println(e.getMessage());
}
System.out.println(color.get());

and expects:

red
black
Nothing to undo
black

Challenge 2

A game keeps a player's last few scores in this step's History. Look closely at what remove(0) removes. What does this program print?

Challenge 3

A game's difficulty level must stay from 1 to 5, and a nickname must have at least 3 letters. Module 42's Oven refused a bad setting in its constructor and in setDegrees; one generic class can do that for any type, with a rule from step 4. Rule.java, Between.java and MinLength.java are ready. In Guarded.java, write the class Guarded<T> with:

  • a constructor Guarded(T start, Rule<T> rule). The start value must pass the rule too: if it doesn't, the constructor throws an IllegalArgumentException with the message Not allowed: followed by the value, like Not allowed: 9.
  • get(), which returns the value
  • set(T value), which changes the value if the rule allows it. If not, it throws an IllegalArgumentException with the same kind of message, and the old value stays.

The check runs:

Guarded<Integer> difficulty = new Guarded<>(3, new Between(1, 5));
difficulty.set(4);
try {
    difficulty.set(9);
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage());
}
System.out.println(difficulty.get());

and expects:

Not allowed: 9
4