Java Arena
Step 4 of 5

Methods and factories

A record's body can hold methods, like a class's. They can read the components, and they're how a record does more than hold its values.

Methods that use the components

A shop keeps prices in whole cents, so that adding them up never meets the rounding errors of double. A record for an amount of money:

Main.java
public class Main {
    public static void main(String[] args) {
        Money coffee = new Money(350);
        Money bun = Money.euros(2, 5);
        Money order = coffee.plus(bun.times(2));
        System.out.println(coffee + " + 2 x " + bun + " = " + order);
        System.out.println(order.cents());
    }
}
Money.java
public record Money(int cents) {
    public static Money euros(int euros, int cents) {
        return new Money(euros * 100 + cents);
    }

    public Money plus(Money other) {
        return new Money(this.cents + other.cents);
    }

    public Money times(int count) {
        return new Money(this.cents * count);
    }

    @Override
    public String toString() {
        int rest = this.cents % 100;
        String zero = "";
        if (rest < 10) {
            zero = "0";
        }
        return this.cents / 100 + "." + zero + rest + " EUR";
    }
}
Output
3.50 EUR + 2 x 2.05 EUR = 7.60 EUR
760
  • Inside the record, this.cents is the instance variable. other.cents works too, since other is a Money and the code is inside Money; other.cents() would be the same value.
  • plus and times don't change anything. They return a new Money with the result, as String's toUpperCase returns a new String. coffee is still 3.50 after plus.
  • A record may override toString (or equals and hashCode, though you rarely need to). Here the amount is shown as euros and cents, with a 0 before a cents value under 10.
  • euros is a static factory: a static method that makes a record in another way than the constructor, Money.euros(2, 5) for 2 euros and 5 cents. Static methods, and static variables too, are allowed in a record.

A record never changes

A method can't assign a component, since its instance variable is final:

Main.java
public class Main {
    public static void main(String[] args) {
        Money price = new Money(350);
        price.add(new Money(205));
        System.out.println(price);
    }
}
Money.java
public record Money(int cents) {
    public void add(Money other) {
        this.cents = this.cents + other.cents;
    }
}
What javac prints
Money.java:3: error: cannot assign a value to final variable cents
        this.cents = this.cents + other.cents;
            ^
1 error

Nor can a record have instance variables of its own besides its components. Everything a record holds is in its header:

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Money(350));
    }
}
Money.java
public record Money(int cents) {
    private int timesUsed;
}
What javac prints
Money.java:2: error: field declaration must be static
    private int timesUsed;
                ^
  (consider replacing field with record component)
1 error

"Changing" one value: with-methods

To change one component, make a new record with that one value different and the others copied. Such methods are customarily named with and the component:

public Money withCents(int newCents) {
    return new Money(newCents);
}

For a record with one component, that's just the constructor. With several components it saves the caller from copying all the others, as the second task below shows.

Your task

Challenge 1 of 3

A hiking app marks waypoints on a map, each at a point x, y (in kilometers). In Waypoint.java, add to the record Waypoint(double x, double y) the method distanceTo(Waypoint other), which returns the straight-line distance between this waypoint and the other one: the square root (Math.sqrt) of the x difference squared plus the y difference squared. The check runs:

Waypoint camp = new Waypoint(1.0, 2.0);
Waypoint lake = new Waypoint(4.0, 6.0);
System.out.println(camp.distanceTo(lake));
System.out.println(lake.distanceTo(camp));
System.out.println(camp.distanceTo(camp));

and expects:

5.0
5.0
0.0

Challenge 2

A game shows each player as an avatar with a name, a color and a level. Saved avatars never change: a change in the settings makes a new one. In Avatar.java, add to the record Avatar(String name, String color, int level) two methods:

  • withColor(String newColor), which returns a new Avatar with the same name and level and the new color;
  • withLevel(int newLevel), which returns a new Avatar with the same name and color and the new level.

The check runs:

Avatar saved = new Avatar("Noor", "teal", 3);
Avatar repainted = saved.withColor("amber");
Avatar promoted = repainted.withLevel(4);
System.out.println(saved);
System.out.println(repainted);
System.out.println(promoted);

and expects:

Avatar[name=Noor, color=teal, level=3]
Avatar[name=Noor, color=amber, level=3]
Avatar[name=Noor, color=amber, level=4]

Challenge 3

A timetable stores times of day as minutes after midnight, but shows them as hours and minutes. In TimeOfDay.java, add to the record TimeOfDay(int hour, int minute):

  • a static factory fromMinutes(int minutes), which makes the TimeOfDay that many minutes after midnight. It wraps around the day: 1440 minutes (24 hours) is midnight again, and -15 is 23:45;
  • a static factory midnight(), which returns 00:00;
  • toMinutes(), which returns how many minutes after midnight this time is;
  • plusMinutes(int minutes), which returns the time that many minutes later (or earlier, for a negative number), wrapping around the day too;
  • toString(), which shows the time with two digits each, like 08:05.

The check runs:

TimeOfDay departure = TimeOfDay.fromMinutes(485);
TimeOfDay delayed = departure.plusMinutes(70);
System.out.println(departure + " becomes " + delayed);
System.out.println(delayed.toMinutes() + " " + delayed.hour());
System.out.println(new TimeOfDay(23, 50).plusMinutes(25));
System.out.println(TimeOfDay.midnight().plusMinutes(-15));

and expects:

08:05 becomes 09:15
555 9
00:15
23:45