Java Arena
Step 3 of 4

switch on sealed types

In module 57, a switch expression over an enum needed no default when it had a case for every constant. Since Java 21, a switch can also choose by an object's type, and a sealed interface's permitted types work like an enum's constants: when every one has a case, the switch is complete.

Type patterns in case labels

The café's payments, now as records:

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(receipt(new Cash(450)));
        System.out.println(receipt(new BankCard("4921", 1250)));
        System.out.println(receipt(new Voucher("SPRING10", 1000)));
    }

    public static String receipt(Payment payment) {
        return switch (payment) {
            case Cash cash -> "Cash: " + cash.cents() + " cents";
            case BankCard card -> "Card ending " + card.lastDigits() + ": " + card.cents() + " cents";
            case Voucher voucher -> "Voucher " + voucher.code() + ": " + voucher.cents() + " cents";
        };
    }
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
Output
Cash: 450 cents
Card ending 4921: 1250 cents
Voucher SPRING10: 1000 cents
  • case Cash cash -> has a type pattern, like the ones instanceof takes. The case matches when the payment is a Cash, and then cash is the payment as a Cash, for that case only.
  • Java tries the cases from the top, and the first one that matches gives the value.
  • There is no default, and none is needed: Payment is sealed, so javac knows that every payment is a Cash, a BankCard or a Voucher. A switch that covers every possible value is exhaustive.

The same choice with instanceof needs a last return for "none of these", which can never happen. The switch says exactly what the program means, and javac checks it.

javac finds the switches to update

The café starts taking mobile payments. Someone adds MobilePay to the permits list and writes the record, but forgets the switch:

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(receipt(new MobilePay("040 123 4567", 300)));
    }

    public static String receipt(Payment payment) {
        return switch (payment) {
            case Cash cash -> "Cash: " + cash.cents() + " cents";
            case BankCard card -> "Card ending " + card.lastDigits() + ": " + card.cents() + " cents";
            case Voucher voucher -> "Voucher " + voucher.code() + ": " + voucher.cents() + " cents";
        };
    }
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher, MobilePay {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
MobilePay.java
public record MobilePay(String phone, int cents) implements Payment {
}
What javac prints
Main.java:7: error: the switch expression does not cover all possible input values
        return switch (payment) {
               ^
1 error

In a large program, javac points at every switch that needs a case for the new type. With a default in them, the program would compile, and every mobile payment would quietly get whatever the default gives. So with sealed types, leave default out.

A switch statement (one that gives no value) with type patterns must cover every type too. And as with every switch, a null payment throws a NullPointerException; since Java 21 a switch can have case null -> for it, but it's usually better not to pass null around at all.

Guards: case ... when

A case can have a condition after the word when. The case matches only when the type matches and the condition is true. A card payment over 50 euros needs the card's PIN:

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(receipt(new BankCard("4921", 1250)));
        System.out.println(receipt(new BankCard("4921", 8900)));
        System.out.println(receipt(new Cash(8900)));
    }

    public static String receipt(Payment payment) {
        return switch (payment) {
            case Cash cash -> "Cash: " + cash.cents() + " cents";
            case BankCard card when card.cents() > 5000 -> "Card ending " + card.lastDigits() + ": " + card.cents() + " cents, PIN needed";
            case BankCard card -> "Card ending " + card.lastDigits() + ": " + card.cents() + " cents";
            case Voucher voucher -> "Voucher " + voucher.code() + ": " + voucher.cents() + " cents";
        };
    }
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
Output
Card ending 4921: 1250 cents
Card ending 4921: 8900 cents, PIN needed
Cash: 8900 cents

A case with when doesn't count toward covering BankCard, since its condition can be false; the plain case BankCard card after it does. The order matters: the guarded case must come first. In the other order, the plain case would match every card, and the guarded one could never be reached. javac doesn't allow that:

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(receipt(new BankCard("4921", 8900)));
    }

    public static String receipt(Payment payment) {
        return switch (payment) {
            case Cash cash -> "Cash: " + cash.cents() + " cents";
            case BankCard card -> "Card ending " + card.lastDigits() + ": " + card.cents() + " cents";
            case BankCard card when card.cents() > 5000 -> "Card ending " + card.lastDigits() + ": " + card.cents() + " cents, PIN needed";
            case Voucher voucher -> "Voucher " + voucher.code() + ": " + voucher.cents() + " cents";
        };
    }
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
What javac prints
Main.java:10: error: this case label is dominated by a preceding case label
            case BankCard card when card.cents() > 5000 -> "Card ending " + card.lastDigits() + ": " + card.cents() + " cents, PIN needed";
                 ^
1 error

Your task

Challenge 1 of 3

Back to the floor plan. This time the shapes are records with no methods of their own, and Main.java works out the areas. Write area(Shape shape) with a switch expression on shape: a rectangle's area is its width times its depth, and a circle's is Math.PI times the radius times the radius. Don't use default or instanceof. The program prints the area of each room and then the total:

Living room: 22.5
Bedroom: 12.0
Tower: 7.0685834705770345
Total: 41.56858347057703

Challenge 2

The floor plan app gets a third shape, Triangle(double base, double height), for attic rooms. It's already in Triangle.java and in Shape's permits list, but now Main.java doesn't compile: press Run, and javac names both switch expressions. Add a case for triangles to each: a triangle's area is base times height divided by 2, and its label is triangle, the base, x, and the height, like the other labels. Don't add a default. The program prints:

rectangle 5.0 x 4.5: 22.5
circle radius 1.5: 7.0685834705770345
triangle 4.0 x 2.5: 5.0

Challenge 3

Back in the chat app, ChatMessage is now sealed, with three records. Write preview(ChatMessage message) with a switch expression, using when for the special cases:

  • a text message longer than 20 characters: the sender, : , its first 20 characters and ...;
  • any other text message: the sender, : and the text;
  • a photo over 1000 kB: the sender and : [large photo, and the size and kB];
  • any other photo: the sender and : [photo];
  • a sticker: the sender and : [sticker and the sticker's name and ].

Don't use default. The check runs:

System.out.println(preview(new TextMessage("Aino", "Lunch at noon?")));
System.out.println(preview(new TextMessage("Ben", "Running late, the bus never came")));
System.out.println(preview(new PhotoMessage("Chen", "menu.jpg", 840)));
System.out.println(preview(new PhotoMessage("Chen", "view.png", 4200)));
System.out.println(preview(new StickerMessage("Dora", "thumbs up")));

and expects:

Aino: Lunch at noon?
Ben: Running late, the bu...
Chen: [photo]
Chen: [large photo, 4200 kB]
Dora: [sticker thumbs up]