Java Arena
Step 2 of 4

Sealed interfaces

Any class can implement an interface: that's what makes interfaces useful for things like Comparable, which classes everywhere implement. But some interfaces stand for a fixed list of kinds. In the chat app, the method preview of the last step had to end with a return for "something new", because javac can't know that ChatMessage has only three kinds. Since Java 17, an interface can be sealed: it lists, after the word permits, exactly the classes that may implement it.

A floor plan's shapes

A floor plan app draws rooms of two shapes: rectangles, and circles for round tower rooms. Nothing else is allowed:

Main.java
public class Main {
    public static void main(String[] args) {
        Shape kitchen = new Rectangle(4.0, 3.5);
        Shape tower = new Circle(2.0);
        System.out.println("Kitchen: " + kitchen.area() + " square meters");
        System.out.println("Tower room: " + tower.area() + " square meters");
    }
}
Shape.java
public sealed interface Shape permits Rectangle, Circle {
    double area();
}
Rectangle.java
public final class Rectangle implements Shape {
    private final double width;
    private final double depth;

    public Rectangle(double width, double depth) {
        this.width = width;
        this.depth = depth;
    }

    @Override
    public double area() {
        return this.width * this.depth;
    }
}
Circle.java
public final class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * this.radius * this.radius;
    }
}
Output
Kitchen: 14.0 square meters
Tower room: 12.566370614359172 square meters

sealed goes before interface, and the list of permitted classes comes after permits, separated by commas. The interface works as any other: a Shape variable can refer to a Rectangle or a Circle, and area() runs the object's own method.

A class that isn't on the list

A third shape that isn't on the list doesn't compile:

Main.java
public class Main {
    public static void main(String[] args) {
        Shape hall = new Triangle(3.0, 2.0);
        System.out.println(hall.area());
    }
}
Shape.java
public sealed interface Shape permits Rectangle, Circle {
    double area();
}
Rectangle.java
public final class Rectangle implements Shape {
    private final double width;
    private final double depth;

    public Rectangle(double width, double depth) {
        this.width = width;
        this.depth = depth;
    }

    @Override
    public double area() {
        return this.width * this.depth;
    }
}
Circle.java
public final class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * this.radius * this.radius;
    }
}
Triangle.java
public final class Triangle implements Shape {
    private final double base;
    private final double height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public double area() {
        return this.base * this.height / 2;
    }
}
What javac prints
Triangle.java:1: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)
public final class Triangle implements Shape {
             ^
1 error

javac says "class" and "extend" for interfaces too. To allow triangles, add Triangle to the list: permits Rectangle, Circle, Triangle. The list is in one place, so a reader sees at once every kind of Shape there is.

final, sealed or non-sealed

Each permitted class must say whether the list goes on below it, with one of three words:

  • final: no class can extend it. This is the usual choice, as above.
  • sealed: it has a permits list of its own.
  • non-sealed: any class can extend it again. Use it only when that's really what you want.

Without one of them, javac stops:

Main.java
public class Main {
    public static void main(String[] args) {
        Shape tower = new Circle(2.0);
        System.out.println(tower.area());
    }
}
Shape.java
public sealed interface Shape permits Rectangle, Circle {
    double area();
}
Rectangle.java
public final class Rectangle implements Shape {
    private final double width;
    private final double depth;

    public Rectangle(double width, double depth) {
        this.width = width;
        this.depth = depth;
    }

    @Override
    public double area() {
        return this.width * this.depth;
    }
}
Circle.java
public class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * this.radius * this.radius;
    }
}
What javac prints
Circle.java:1: error: sealed, non-sealed or final modifiers expected
public class Circle implements Shape {
       ^
1 error

The permitted classes must also be in the same package as the sealed interface. In this module's programs, all classes are in the default package, so they are.

Records are final already

A record can't be extended: every record is final without saying so. That makes records a natural fit for the permitted types. The floor plan's shapes as records:

Main.java
public class Main {
    public static void main(String[] args) {
        Shape hall = new Rectangle(6.0, 2.0);
        System.out.println(hall);
        System.out.println(hall.area());
    }
}
Shape.java
public sealed interface Shape permits Rectangle, Circle {
    double area();
}
Rectangle.java
public record Rectangle(double width, double depth) implements Shape {
    @Override
    public double area() {
        return this.width * this.depth;
    }
}
Circle.java
public record Circle(double radius) implements Shape {
    @Override
    public double area() {
        return Math.PI * this.radius * this.radius;
    }
}
Output
Rectangle[width=6.0, depth=2.0]
12.0

An abstract class can be sealed in the same way (public sealed abstract class Shape permits Rectangle, Circle), and then its permitted classes extend it. This module uses sealed interfaces. The next step shows what the closed list is good for: a switch that javac checks covers every kind.

Your task

Challenge 1 of 3

A café's till takes payments. Make Payment a sealed interface that permits Cash and BankCard, and make Cash final. Then write the class BankCard: it's final, implements Payment, its constructor takes the card's last four digits (a String) and the amount in cents, and receipt() returns card ending and the digits, a comma, and the cents, as below. The check runs:

Payment first = new Cash(450);
Payment second = new BankCard("4921", 1250);
System.out.println(first.receipt());
System.out.println(second.receipt());

and expects:

cash, 450 cents
card ending 4921, 1250 cents

Challenge 2

The café now sells gift vouchers too, and someone has written the class Voucher in Voucher.java. But the program doesn't compile any more. Press Run: javac says

Voucher.java:1: error: class is not allowed to extend sealed class: Payment (as it is not listed in its 'permits' clause)

Fix it while keeping Payment sealed: add Voucher to the interface's list. javac then has one more thing to say about Voucher; fix that too, by making it final. The program prints:

cash, 450 cents
card ending 4921, 1250 cents
voucher SPRING10, 1000 cents

Challenge 3

A web shop has two kinds of discount: a percentage off the price, and a fixed amount off. Discount.java holds the sealed interface; write its two permitted types in their files as records:

  • PercentOff(int percent): apply(price) computes the discount price * percent / 100 first (whole cents, so the division drops any fraction), then returns the price minus that discount;
  • AmountOff(int cents): apply(price) returns the price minus cents, but never less than 0.

Prices are in cents. The check runs:

Discount sale = new PercentOff(15);
Discount coupon = new AmountOff(500);
System.out.println(sale + ": " + sale.apply(2000));
System.out.println(coupon + ": " + coupon.apply(2000));
System.out.println(coupon + ": " + coupon.apply(300));

and expects:

PercentOff[percent=15]: 1700
AmountOff[cents=500]: 1500
AmountOff[cents=500]: 0