Java Arena
Step 5 of 5

Class or record?

Records are a handy tool, but not for every class. This step shows what a record can't do, and how to choose.

What a record can't do

  • Change. Its values are fixed when it's made. Anything whose values change over time, such as a counter, a bank account's balance or a shopping cart that items are added to, needs a class.
  • Hide a value or keep one of its own. Every component has a public accessor, and a record has no instance variables besides its components.
  • Inherit. A record is final, so no class can extend it, and a record can't extend a class either. Every record already extends Java's class Record:
Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Tram("7A", 22));
    }
}
Vehicle.java
public class Vehicle {
}
Tram.java
public record Tram(String line, int stops) extends Vehicle {
}
What javac prints
Tram.java:1: error: '{' expected
public record Tram(String line, int stops) extends Vehicle {
                                          ^
1 error

javac doesn't even read on to the word extends: after a record's header it expects its body. A record can implement interfaces, though, as a class does (module 35), and that includes Comparable.

One more thing to know: a record is only as fixed as its components. A component that's an ArrayList can't be swapped for another list, but the list itself can still get new elements, through the accessor. Records work best with components that don't change either: numbers, Strings and other records.

Records and sorting

Accessors are methods, so they work as method references (module 37) in Comparator.comparing (module 38), and a record can implement Comparable for its natural order:

Main.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        ArrayList<Departure> board = new ArrayList<>();
        board.add(new Departure("R 14", 1015, 3));
        board.add(new Departure("IC 27", 940, 8));
        board.add(new Departure("S 102", 1015, 1));

        Collections.sort(board);
        System.out.println(board.get(0).train() + " leaves first");

        board.sort(Comparator.comparing(Departure::platform));
        for (Departure departure : board) {
            System.out.println(departure);
        }
    }
}
Departure.java
public record Departure(String train, int time, int platform) implements Comparable<Departure> {
    @Override
    public int compareTo(Departure other) {
        return this.time - other.time;
    }
}
Output
IC 27 leaves first
Departure[train=S 102, time=1015, platform=1]
Departure[train=R 14, time=1015, platform=3]
Departure[train=IC 27, time=940, platform=8]

Choosing

A record fits when the object is its values: two of them with the same values are interchangeable, as two 5 euro notes are. A point, an amount of money, a date range, a reading, a timetable entry. A class fits when the object has a life of its own: its values change, it keeps things hidden, or two objects with the same values are still different things, as two bank accounts that both hold 0 euros are.

Many programs use both: a class keeps changing state, and records are the values it holds and hands out.

Your task

Challenge 1 of 3

A quilting club's app has the class Quilt below, written the long way, with getters and its own equals, hashCode and toString. A finished quilt never changes, so it can be a record. Turn Quilt into a record with the components name and squares, with no equals or hashCode of your own, and change Main to use the accessors. The program must print exactly what it prints now:

true
Quilt[name=Starburst, squares=12]
21 squares in all
LOG CABIN

Challenge 2

A cable car's gondola takes riders up a mountain, each with a name and a weight in kilos. A rider's details never change; who is in the gondola changes all day. Write both, choosing a record for one and a class for the other:

  • Rider, with the components name (a String) and kilos (an int);
  • Gondola, with a constructor Gondola(int maxKilos) (the most it can carry), and the methods:
    • board(Rider rider), which lets the rider in and returns true, unless the same rider is already in the gondola or the load would go over maxKilos; then nobody gets in and it returns false;
    • totalKilos(), the weight of everyone in the gondola;
    • leave(String name), which lets out the rider with that name and returns true, or returns false when there's no such rider in the gondola.

The check runs:

Gondola gondola = new Gondola(300);
System.out.println(gondola.board(new Rider("Anni", 70)));
System.out.println(gondola.board(new Rider("Anni", 70)));
System.out.println(gondola.board(new Rider("Bruno", 95)));
System.out.println(gondola.board(new Rider("Carl", 140)));
System.out.println(gondola.totalKilos() + " kilos");
System.out.println(gondola.leave("Anni") + " " + gondola.leave("Zoe"));
System.out.println(gondola.totalKilos() + " kilos");

and expects:

true
false
true
false
165 kilos
true false
95 kilos

Challenge 3

A dentist's office lists the day's appointments by the hour, and appointments in the same hour in alphabetical order of the patients' names. Appointment is a record with the components patient and hour. In Main.java, write the method printSchedule(ArrayList<Appointment> appointments), which sorts the given list itself (with appointments.sort(...)) using Comparator.comparing(Appointment::hour) and thenComparing by the patient, and then prints each appointment as the hour, a colon and the patient's name. The check runs:

ArrayList<Appointment> day = new ArrayList<>();
day.add(new Appointment("Otto", 11));
day.add(new Appointment("Selma", 9));
day.add(new Appointment("Leo", 11));
day.add(new Appointment("Eeva", 10));
day.add(new Appointment("Aada", 11));
printSchedule(day);

and expects:

9: Selma
10: Eeva
11: Aada
11: Leo
11: Otto