{"id":"records-class-or-record","slug":"class-or-record","title":"Class or record?","text":"Records are a handy tool, but not for every class. This step shows what a record can't do, and how to choose.\n\n### What a record can't do\n\n- **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.\n- **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.\n- **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`:\n\n```java\npublic class Main {\n    public static void main(String[] args) {\n        System.out.println(new Tram(\"7A\", 22));\n    }\n}\n// ==== Vehicle.java ====\npublic class Vehicle {\n}\n// ==== Tram.java ====\npublic record Tram(String line, int stops) extends Vehicle {\n}\n```\n\n```javac\nTram.java:1: error: '{' expected\npublic record Tram(String line, int stops) extends Vehicle {\n                                          ^\n1 error\n```\n\njavac 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.\n\nOne 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.\n\n### Records and sorting\n\nAccessors 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:\n\n```java\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Departure> board = new ArrayList<>();\n        board.add(new Departure(\"R 14\", 1015, 3));\n        board.add(new Departure(\"IC 27\", 940, 8));\n        board.add(new Departure(\"S 102\", 1015, 1));\n\n        Collections.sort(board);\n        System.out.println(board.get(0).train() + \" leaves first\");\n\n        board.sort(Comparator.comparing(Departure::platform));\n        for (Departure departure : board) {\n            System.out.println(departure);\n        }\n    }\n}\n// ==== Departure.java ====\npublic record Departure(String train, int time, int platform) implements Comparable<Departure> {\n    @Override\n    public int compareTo(Departure other) {\n        return this.time - other.time;\n    }\n}\n```\n\n```output\nIC 27 leaves first\nDeparture[train=S 102, time=1015, platform=1]\nDeparture[train=R 14, time=1015, platform=3]\nDeparture[train=IC 27, time=940, platform=8]\n```\n\n### Choosing\n\nA 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.\n\nMany programs use both: a class keeps changing state, and records are the values it holds and hands out.\n","task":"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:\n\n```\ntrue\nQuilt[name=Starburst, squares=12]\n21 squares in all\nLOG CABIN\n```\n","kind":"code","seed":"// ==== Quilt.java ====\nimport java.util.Objects;\n\npublic class Quilt {\n    private final String name;\n    private final int squares;\n\n    public Quilt(String name, int squares) {\n        this.name = name;\n        this.squares = squares;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public int getSquares() {\n        return this.squares;\n    }\n\n    @Override\n    public boolean equals(Object compared) {\n        if (this == compared) {\n            return true;\n        }\n        if (!(compared instanceof Quilt)) {\n            return false;\n        }\n        Quilt other = (Quilt) compared;\n        return this.name.equals(other.name) && this.squares == other.squares;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(this.name, this.squares);\n    }\n\n    @Override\n    public String toString() {\n        return \"Quilt[name=\" + this.name + \", squares=\" + this.squares + \"]\";\n    }\n}\n\n// ==== Main.java ====\nimport java.util.ArrayList;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Quilt> quilts = new ArrayList<>();\n        quilts.add(new Quilt(\"Starburst\", 12));\n        quilts.add(new Quilt(\"Log Cabin\", 9));\n        System.out.println(quilts.contains(new Quilt(\"Log Cabin\", 9)));\n        System.out.println(quilts.get(0));\n        int total = 0;\n        for (Quilt quilt : quilts) {\n            total = total + quilt.getSquares();\n        }\n        System.out.println(total + \" squares in all\");\n        System.out.println(quilts.get(1).getName().toUpperCase());\n    }\n}\n","solution":"// ==== Quilt.java ====\npublic record Quilt(String name, int squares) {\n}\n\n// ==== Main.java ====\nimport java.util.ArrayList;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Quilt> quilts = new ArrayList<>();\n        quilts.add(new Quilt(\"Starburst\", 12));\n        quilts.add(new Quilt(\"Log Cabin\", 9));\n        System.out.println(quilts.contains(new Quilt(\"Log Cabin\", 9)));\n        System.out.println(quilts.get(0));\n        int total = 0;\n        for (Quilt quilt : quilts) {\n            total = total + quilt.squares();\n        }\n        System.out.println(total + \" squares in all\");\n        System.out.println(quilts.get(1).name().toUpperCase());\n    }\n}\n","hints":["A record puts its components in parentheses after its name, `record Quilt(...)`. The fields, the constructor, the getters, `equals`, `hashCode` and `toString` all go away, and so does the `import` of Objects.","The whole class becomes `public record Quilt(String name, int squares) { }`. The record's `toString` already prints `Quilt[name=Starburst, squares=12]`, and its `equals` makes `contains` work. In Main, `getSquares()` becomes `squares()` and `getName()` becomes `name()`."],"tests":[{"name":"Test 1","stdin":"","expect":"true\nQuilt[name=Starburst, squares=12]\n21 squares in all\nLOG CABIN","hidden":false},{"name":"Test 2","stdin":"","call":"Quilt sample = new Quilt(\"Pinwheel\", 7);\nSystem.out.println(sample.name() + \" \" + sample.squares() + \" \" + sample);\nSystem.out.println(sample.equals(new Quilt(\"Pinwheel\", 7)) + \" \" + sample.equals(new Quilt(\"Pinwheel\", 8)));\n","expect":"Pinwheel 7 Quilt[name=Pinwheel, squares=7]\ntrue false","hidden":true}],"require":[{"pattern":"\\brecord\\s+Quilt\\s*\\(","message":"Declare Quilt as a record: `public record Quilt(String name, int squares)`."}],"forbid":[{"pattern":"\\bboolean\\s+equals\\s*\\(","message":"Leave out the hand-written `equals`: the record has one already."},{"pattern":"\\bint\\s+hashCode\\s*\\(","message":"Leave out the hand-written `hashCode`: the record has one already."}],"more":[{"task":"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:\n\n- `Rider`, with the components `name` (a String) and `kilos` (an int);\n- `Gondola`, with a constructor `Gondola(int maxKilos)` (the most it can carry), and the methods:\n  - `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`;\n  - `totalKilos()`, the weight of everyone in the gondola;\n  - `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.\n\nThe check runs:\n\n```java\nGondola gondola = new Gondola(300);\nSystem.out.println(gondola.board(new Rider(\"Anni\", 70)));\nSystem.out.println(gondola.board(new Rider(\"Anni\", 70)));\nSystem.out.println(gondola.board(new Rider(\"Bruno\", 95)));\nSystem.out.println(gondola.board(new Rider(\"Carl\", 140)));\nSystem.out.println(gondola.totalKilos() + \" kilos\");\nSystem.out.println(gondola.leave(\"Anni\") + \" \" + gondola.leave(\"Zoe\"));\nSystem.out.println(gondola.totalKilos() + \" kilos\");\n```\n\nand expects:\n\n```\ntrue\nfalse\ntrue\nfalse\n165 kilos\ntrue false\n95 kilos\n```\n","kind":"code","seed":"// ==== Rider.java ====\npublic class Rider {\n\n}\n\n// ==== Gondola.java ====\nimport java.util.ArrayList;\n\npublic class Gondola {\n\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        Gondola gondola = new Gondola(300);\n        System.out.println(gondola.board(new Rider(\"Anni\", 70)));\n        System.out.println(gondola.board(new Rider(\"Anni\", 70)));\n        System.out.println(gondola.board(new Rider(\"Bruno\", 95)));\n        System.out.println(gondola.board(new Rider(\"Carl\", 140)));\n        System.out.println(gondola.totalKilos() + \" kilos\");\n        System.out.println(gondola.leave(\"Anni\") + \" \" + gondola.leave(\"Zoe\"));\n        System.out.println(gondola.totalKilos() + \" kilos\");\n    }\n}\n","solution":"// ==== Rider.java ====\npublic record Rider(String name, int kilos) {\n}\n\n// ==== Gondola.java ====\nimport java.util.ArrayList;\n\npublic class Gondola {\n    private int maxKilos;\n    private ArrayList<Rider> riders;\n\n    public Gondola(int maxKilos) {\n        this.maxKilos = maxKilos;\n        this.riders = new ArrayList<>();\n    }\n\n    public boolean board(Rider rider) {\n        if (this.riders.contains(rider)) {\n            return false;\n        }\n        if (this.totalKilos() + rider.kilos() > this.maxKilos) {\n            return false;\n        }\n        this.riders.add(rider);\n        return true;\n    }\n\n    public int totalKilos() {\n        int total = 0;\n        for (Rider rider : this.riders) {\n            total = total + rider.kilos();\n        }\n        return total;\n    }\n\n    public boolean leave(String name) {\n        for (Rider rider : this.riders) {\n            if (rider.name().equals(name)) {\n                this.riders.remove(rider);\n                return true;\n            }\n        }\n        return false;\n    }\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        Gondola gondola = new Gondola(300);\n        System.out.println(gondola.board(new Rider(\"Anni\", 70)));\n        System.out.println(gondola.board(new Rider(\"Anni\", 70)));\n        System.out.println(gondola.board(new Rider(\"Bruno\", 95)));\n        System.out.println(gondola.board(new Rider(\"Carl\", 140)));\n        System.out.println(gondola.totalKilos() + \" kilos\");\n        System.out.println(gondola.leave(\"Anni\") + \" \" + gondola.leave(\"Zoe\"));\n        System.out.println(gondola.totalKilos() + \" kilos\");\n    }\n}\n","hints":["Look back at the task's second sentence. Which of the two changes during the day, and which is just a few values that never change? Values that never change fit a record.","Rider is a record, `public record Rider(String name, int kilos) { }`, and Gondola is a class with an `ArrayList<Rider>` and the most it can carry. In `board`, `contains` finds an equal rider because records have `equals`. In `leave`, look for the rider whose `name()` equals the name, remove them and return `true` right away (returning ends the loop before the list's change can disturb it)."],"tests":[{"name":"Test 1","stdin":"","call":"Gondola gondola = new Gondola(300);\nSystem.out.println(gondola.board(new Rider(\"Anni\", 70)));\nSystem.out.println(gondola.board(new Rider(\"Anni\", 70)));\nSystem.out.println(gondola.board(new Rider(\"Bruno\", 95)));\nSystem.out.println(gondola.board(new Rider(\"Carl\", 140)));\nSystem.out.println(gondola.totalKilos() + \" kilos\");\nSystem.out.println(gondola.leave(\"Anni\") + \" \" + gondola.leave(\"Zoe\"));\nSystem.out.println(gondola.totalKilos() + \" kilos\");\n","expect":"true\nfalse\ntrue\nfalse\n165 kilos\ntrue false\n95 kilos","hidden":false},{"name":"Test 2","stdin":"","call":"Gondola small = new Gondola(100);\nSystem.out.println(small.totalKilos() + \" \" + small.leave(\"Ida\"));\nSystem.out.println(small.board(new Rider(\"Ida\", 100)) + \" \" + small.board(new Rider(\"Tom\", 1)));\nSystem.out.println(small.board(new Rider(\"Ida\", 99)) + \" \" + small.totalKilos());\nSystem.out.println(small.leave(\"Ida\") + \" \" + small.leave(\"Ida\") + \" \" + small.totalKilos());\nSystem.out.println(small.board(new Rider(\"Tom\", 1)) + \" \" + small.board(new Rider(\"Uma\", 99)) + \" \" + small.totalKilos());\nRider rider = new Rider(\"Vic\", 80);\nSystem.out.println(rider.name() + \" \" + rider.kilos() + \" \" + rider);\n","expect":"0 false\ntrue false\nfalse 100\ntrue false 0\ntrue true 100\nVic 80 Rider[name=Vic, kilos=80]","hidden":true}],"require":[{"pattern":"\\brecord\\s+Rider\\s*\\(","message":"A rider's details never change, so make Rider a record: `public record Rider(String name, int kilos)`."}],"forbid":[]},{"task":"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:\n\n```java\nArrayList<Appointment> day = new ArrayList<>();\nday.add(new Appointment(\"Otto\", 11));\nday.add(new Appointment(\"Selma\", 9));\nday.add(new Appointment(\"Leo\", 11));\nday.add(new Appointment(\"Eeva\", 10));\nday.add(new Appointment(\"Aada\", 11));\nprintSchedule(day);\n```\n\nand expects:\n\n```\n9: Selma\n10: Eeva\n11: Aada\n11: Leo\n11: Otto\n```\n","kind":"code","seed":"// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.Comparator;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Appointment> day = new ArrayList<>();\n        day.add(new Appointment(\"Otto\", 11));\n        day.add(new Appointment(\"Selma\", 9));\n        day.add(new Appointment(\"Leo\", 11));\n        day.add(new Appointment(\"Eeva\", 10));\n        day.add(new Appointment(\"Aada\", 11));\n        printSchedule(day);\n    }\n\n    public static void printSchedule(ArrayList<Appointment> appointments) {\n\n    }\n}\n\n// ==== Appointment.java ====\npublic record Appointment(String patient, int hour) {\n}\n","solution":"// ==== Main.java ====\nimport java.util.ArrayList;\nimport java.util.Comparator;\n\npublic class Main {\n    public static void main(String[] args) {\n        ArrayList<Appointment> day = new ArrayList<>();\n        day.add(new Appointment(\"Otto\", 11));\n        day.add(new Appointment(\"Selma\", 9));\n        day.add(new Appointment(\"Leo\", 11));\n        day.add(new Appointment(\"Eeva\", 10));\n        day.add(new Appointment(\"Aada\", 11));\n        printSchedule(day);\n    }\n\n    public static void printSchedule(ArrayList<Appointment> appointments) {\n        appointments.sort(Comparator.comparing(Appointment::hour).thenComparing(Appointment::patient));\n        for (Appointment appointment : appointments) {\n            System.out.println(appointment.hour() + \": \" + appointment.patient());\n        }\n    }\n}\n\n// ==== Appointment.java ====\npublic record Appointment(String patient, int hour) {\n}\n","hints":["The accessors work as method references: `Comparator.comparing(Appointment::hour)` compares by the hour, and `.thenComparing(Appointment::patient)` decides between appointments in the same hour.","Sort with `appointments.sort(...)`, then print each appointment in a for-each loop: `appointment.hour() + \": \" + appointment.patient()`."],"tests":[{"name":"Test 1","stdin":"","call":"ArrayList<Appointment> day = new ArrayList<>();\nday.add(new Appointment(\"Otto\", 11));\nday.add(new Appointment(\"Selma\", 9));\nday.add(new Appointment(\"Leo\", 11));\nday.add(new Appointment(\"Eeva\", 10));\nday.add(new Appointment(\"Aada\", 11));\nprintSchedule(day);\n","expect":"9: Selma\n10: Eeva\n11: Aada\n11: Leo\n11: Otto","hidden":false},{"name":"Test 2","stdin":"","call":"ArrayList<Appointment> day = new ArrayList<>();\nday.add(new Appointment(\"Veera\", 14));\nday.add(new Appointment(\"Ville\", 8));\nday.add(new Appointment(\"Saga\", 14));\nday.add(new Appointment(\"Sam\", 8));\nday.add(new Appointment(\"Mika\", 14));\nprintSchedule(day);\nSystem.out.println(day.get(0));\n","expect":"8: Sam\n8: Ville\n14: Mika\n14: Saga\n14: Veera\nAppointment[patient=Sam, hour=8]","hidden":true}],"require":[{"pattern":"Appointment::hour","message":"Sort by the hour with `Comparator.comparing(Appointment::hour)`."},{"pattern":"thenComparing\\s*\\(","message":"Sort appointments in the same hour by the patient's name with `thenComparing`."}],"forbid":[]}]}