Lesson 30Phase 3: Abstraction and design
Sealed types and pattern matching
Closing a hierarchy with sealed and permits, then taking values apart with instanceof patterns, switch patterns and record patterns.
By BasithUpdated 50 min
- sealed
- permits
- non-sealed
- pattern matching
- instanceof pattern
- switch pattern
Records gave you a tidy way to hold data: a circle is its radius, a rectangle is its width and height. As soon as a program holds several kinds of record, it keeps asking the same question: which kind is this one, and what is inside it? This lesson gives you Java's tools for that question. You will test a value's type and unpack its parts in a single step, and you will close a family of types so the compiler can check that you handled every member.
- Replace an
instanceofcheck and a cast with oneinstanceofpattern, and say where its variable can be used. - Write a
switchthat picks a case by type, withwhenguards andcase null. - Unpack a record in one pattern, including nested record patterns and
var. - Close a family of types with
sealedandpermits, and mark each memberfinal,sealedornon-sealed. - Explain why a switch over a sealed type needs no
default, and read javac's error when a case is missing. - Choose between a method in every class and a switch over sealed records.
c.radius(), and what its toString prints. From polymorphism you need instanceof and the downcast (Dog) a. You also use interfaces, final classes from static and final, and the switch statement from making decisions.The old way: check, then cast#
Picture a small drawing app with three kinds of shape. A circle is a radius, a rectangle is a width and a height, and a triangle is a base and a height. That is plain data, so each kind is a record. The rectangle is called Rect to keep the lines short.
An interface called Shape ties the three together, so that one array can hold them all. Shapehas no methods yet. It only says “all of these are shapes”.
Now the app needs the area of any shape. With what you learned in polymorphism, you check the kind with instanceof, then cast to reach the record's accessors.
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(3.0, 4.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(area(shape)); } } static double area(Shape shape) { if (shape instanceof Circle) { Circle c = (Circle) shape; return Math.PI * c.radius() * c.radius(); } else if (shape instanceof Rect) { Rect r = (Rect) shape; return r.width() * r.height(); } else if (shape instanceof Triangle) { Triangle t = (Triangle) shape; return 0.5 * t.base() * t.height(); } return 0; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}It prints 3.141592653589793, 12.0 and 6.0. It works, but read one branch out loud: “if shape is a Circle, declare a Circle called c, and set it to shape cast to Circle.” That is the word Circle three times for one idea.
The repetition also hides a trap. The test and the cast are separate lines, and Java does not connect them. A copy-paste slip such as Rect r = (Rect) shape; inside the triangle branch compiles without complaint, then stops the program with a ClassCastException the first time a triangle arrives. And the last line, return 0;, is there only because the compiler cannot tell whether the three checks cover every possible shape.
The instanceof pattern: test and name in one step#
Since Java 16 you can put a variable name straight after the type in an instanceof test: shape instanceof Circle c. Read it as “is shape a Circle? If so, call it c”. When the test passes, Java creates c, already typed as Circle and already holding the shape. When the test fails, there is no c at all. You never write the cast yourself, so you cannot cast to the wrong type.
This is your first pattern: a description of what a value should look like, plus names for the parts you want if it does. Checking a value against a pattern is called pattern matching. Circle c is a type pattern, a type to test for and a name to use, and c is its pattern variable.
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(3.0, 4.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(area(shape)); } } static double area(Shape shape) { if (shape instanceof Circle c) { return Math.PI * c.radius() * c.radius(); } else if (shape instanceof Rect r) { return r.width() * r.height(); } else if (shape instanceof Triangle t) { return 0.5 * t.base() * t.height(); } return 0; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}Same three areas, and each branch now names its type once. Step through it and watch the trace: c, r and t each appear only inside the branch whose test passed.
Where the pattern variable exists
A pattern variable exists only where Java can be sure the test passed. Java works that out from the logic around the test, and these are the cases you will meet most:
- After
&&, it exists. Inshape instanceof Circle c && c.radius() > 10the right side only runs when the left side was true, socis ready to use. - After
||, it does not. Inshape instanceof Circle c || c.radius() > 10the right side runs exactly when the shape is not a circle, so there is no circle to ask. javac stops withcannot find symbol. - Below a negated test that returns, it exists. Write
if (!(shape instanceof Rect r)) return ...;and every line after thatifcan only be reached by a rectangle, sorworks for the rest of the method.
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(12.0), new Circle(0.5), new Rect(3.0, 4.0) }; for (Shape shape : shapes) { if (shape instanceof Circle c && c.radius() > 10) { System.out.println("Big circle: " + c); } } System.out.println(squareCheck(new Rect(5.0, 5.0))); System.out.println(squareCheck(new Rect(2.0, 5.0))); System.out.println(squareCheck(new Circle(1.0))); } static String squareCheck(Shape shape) { if (!(shape instanceof Rect r)) { return "not a rectangle"; } if (r.width() != r.height()) { return "a rectangle, but not a square"; } return "a square with side " + r.width(); }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}The loop prints a single line, Big circle: Circle[radius=12.0]. The small circle fails the size check and the rectangle fails the type check. Then squareCheck prints a square with side 5.0, a rectangle, but not a square and not a rectangle.
Look at how squareCheck begins. It deals with the odd case first and returns, so the rest of the method reads as the normal path, with r ready and no extra level of indentation.
Shape shape and the records from this page. Which one compiles?Patterns in a switch#
A quick recap: the arrow form of switch
You met the arrow form and switch expressions in making decisions: after an arrow only that case's code runs, with no fall-through and so no break, and a switch can produce a value. This lesson puts patterns into that form. Here it is on a plain number once more.
public class Main { public static void main(String[] args) { int sides = 4; String name = switch (sides) { case 3 -> "triangle"; case 4 -> "rectangle"; case 5 -> "pentagon"; default -> "some other polygon"; }; System.out.println(name); }}It prints rectangle. One rule from that lesson matters again below: a switch expression must produce a value whatever comes in. An int can hold over four billion different values and the cases cover three of them, so the default line is required.
Type patterns in case labels
A case label can hold a type pattern, the same Circle c you wrote after instanceof. Java tries the cases from top to bottom and runs the first one that matches. Here is the area method again:
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(3.0, 4.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(area(shape)); } } static double area(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rect r -> r.width() * r.height(); case Triangle t -> 0.5 * t.base() * t.height(); default -> 0; }; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}The same three areas come out. The method now reads like a table: a circle uses this formula, a rectangle that one, a triangle the third.
Why is the default there? Shape is an ordinary interface, so any class in any file may implement it. Tomorrow someone could write record Hexagon(double side) implements Shape {}, and this switch has no case for it. A switch expression must produce a value for every possible shape, and with an open interface that takes a catch-all case, which is what default is. Delete it and javac refuses with the switch expression does not cover all possible input values. Hold on to that: two sections from now, a new keyword makes the default unnecessary.
Guards: case ... when
Sometimes the type alone is not enough. A rectangle whose width equals its height is a square, and the app should say so. You can add a condition to a case with the word when. The condition is called a guard.
public class Main { public static void main(String[] args) { Shape[] shapes = { new Rect(2.0, 2.0), new Rect(2.0, 5.0), new Circle(0.0), new Circle(3.0) }; for (Shape shape : shapes) { System.out.println(describe(shape)); } } static String describe(Shape shape) { return switch (shape) { case Circle c when c.radius() == 0 -> "a dot"; case Circle c -> "a circle of radius " + c.radius(); case Rect r when r.width() == r.height() -> "a square of side " + r.width(); case Rect r -> "a " + r.width() + " by " + r.height() + " rectangle"; default -> "some other shape"; }; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}case Rect r when r.width() == r.height() matches when the shape is a rectangle and the condition is true. The guard can already use r. If the shape is a rectangle but the condition is false, Java carries on to the next case, case Rect r, which takes every other rectangle. The circles work the same way, so the four lines are a square of side 2.0, a 2.0 by 5.0 rectangle, a dot and a circle of radius 3.0.
Order matters. Put a guarded case above the plain case for the same type. If case Rect r came first, it would take every rectangle, squares included, and the square case could never run. javac rejects that with this case label is dominated by a preceding case label. A case is dominated when a case above it already matches everything it could match. The habit that avoids it: specific cases first, general ones last.
public class Main { public static void main(String[] args) { Shape[] shapes = { new Rect(20.0, 20.0), new Rect(5.0, 5.0), new Circle(2.0) }; for (Shape shape : shapes) { System.out.println(sizeOf(shape)); } } static String sizeOf(Shape shape) { return switch (shape) { case Rect r when r.width() > 10 -> "wide"; case Rect r when r.width() == r.height() -> "square"; case Circle c when c.radius() < 1 -> "tiny"; case Rect r -> "plain rectangle"; default -> "other"; }; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}The first rectangle is both wide and square. Decide which case takes it, then write the three lines this program prints.
case null
A Shape variable can also hold null, the value that means “no object”. A switch on null stops the program with a NullPointerException, and a default does not catch it. If null is a value you expect, give it a case of its own:
public class Main { public static void main(String[] args) { Shape picked = null; String message = switch (picked) { case null -> "nothing picked yet"; case Circle c -> "a circle"; case Rect r -> "a rectangle"; default -> "another shape"; }; System.out.println(message); }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}This prints nothing picked yet. When null and every unmatched shape deserve the same answer, put both on one line: case null, default -> "something else";.
Key ideaA switch with patterns tries its cases from top to bottom and runs the first one that matches. A guard narrows a case; when the guard is false, the search carries on below.
Record patterns: unpack the parts#
Look at what the area cases do with their variable: case Rect r -> r.width() * r.height(). The only reason to name the rectangle r is to call its two accessors. A record pattern skips that step. It checks the type and lifts the components straight out into variables of their own:
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(3.0, 4.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(area(shape)); } } static double area(Shape shape) { return switch (shape) { case Circle(double r) -> Math.PI * r * r; case Rect(double w, double h) -> w * h; case Triangle(double b, double h) -> 0.5 * b * h; default -> 0; }; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}Read Rect(double w, double h)as “a Rect, whose width I will call w and whose height I will call h”. It is new Rect(3.0, 4.0)run backwards. Construction puts values in, in the order of the record's header, and the record pattern takes them out in the same order. Java calls the accessors for you.
The names are yours to choose, which is why the triangle's base can be b here. The order and the types must fit the record.
var: let Java fill in the types
Writing double for every component gets repetitive. Inside a pattern you may write var instead, and Java uses the component's own type: Rect(var w, var h) still gives you two double variables. (var works for ordinary local variables too, which you will meet in modern Java syntax.)
A record pattern can carry a guard, and it works after instanceof as well:
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(2.0, 2.0), new Rect(2.0, 5.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(describe(shape)); } Shape picked = shapes[1]; if (picked instanceof Rect(var w, var h) && w == h) { System.out.println("The second shape is a square of side " + w); } } static String describe(Shape shape) { return switch (shape) { case Circle(var r) -> "a circle of radius " + r; case Rect(var w, var h) when w == h -> "a square of side " + w; case Rect(var w, var h) -> "a " + w + " by " + h + " rectangle"; case Triangle(var b, var h) -> "a triangle, base " + b + " and height " + h; default -> "some other shape"; }; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}The first four lines come from describe, where the guard when w == h uses the unpacked width and height directly. The last line comes from the if: picked instanceof Rect(var w, var h) && w == h tests the type, unpacks the rectangle and compares its sides in one condition, and prints The second shape is a square of side 2.0.
isSquare so it returns true only for a Rect whose width equals its height. Use instanceof with a record pattern, all in one return line.Nested record patterns
A record can hold another record. In the drawing app a sticker has a shape and a color: record Sticker(Shape shape, String color) {}. A record pattern can hold another record pattern in a component's place, so one case can unpack the sticker and the shape inside it.
public class Main { public static void main(String[] args) { Sticker[] stickers = { new Sticker(new Circle(2.0), "red"), new Sticker(new Rect(1.0, 1.0), "blue"), new Sticker(new Triangle(3.0, 4.0), "green") }; for (Sticker sticker : stickers) { System.out.println(label(sticker)); } } static String label(Sticker sticker) { return switch (sticker) { case Sticker(Circle(var r), var color) -> "a " + color + " circle, radius " + r; case Sticker(Rect(var w, var h), var color) when w == h -> "a " + color + " square"; case Sticker(var shape, var color) -> "a " + color + " " + shape; }; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}record Sticker(Shape shape, String color) {}Sticker(Circle(var r), var color)matches only a sticker whose shape is a circle, and it hands you the circle's radius and the sticker's color together. A nested pattern matches only if every part matches. If the inside does not fit, Java moves on to the next case. So the first two stickers print a red circle, radius 2.0 and a blue square.
The last case, Sticker(var shape, var color), takes any sticker at all, because var accepts whatever is in its slot. That is why this switch needs no default. The green triangle lands there and prints the shape through the record's toString: a green Triangle[base=3.0, height=4.0].
public class Main { public static void main(String[] args) { Sticker[] stickers = { new Sticker(new Rect(3.0, 3.0), "red"), new Sticker(new Circle(1.0), "red"), new Sticker(new Rect(2.0, 6.0), "green") }; for (Sticker sticker : stickers) { System.out.println(price(sticker)); } } static int price(Sticker sticker) { return switch (sticker) { case Sticker(Rect(var w, var h), var color) when w == h -> 3; case Sticker(var shape, var color) when color.equals("red") -> 2; case Sticker(var shape, var color) -> 1; }; }} interface Shape {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Sticker(Shape shape, String color) {}The first sticker is red and square, so two cases could take it. Pick the output.
Sealed types: closing the family#
Every switch over Shape so far has needed a default, because Shape is open: any class anywhere may implement it. But the drawing app has exactly three shapes, and you would like Java to know that too. Since Java 17 you can say so:
sealed interface Shape permits Circle, Rect, Triangle {} record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}sealed means the family is closed, and permits is its guest list: only the types named there may implement Shape. Try to add a fourth shape that is not on the list, such as record Star(int points) implements Shape {}, and javac stops you with class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause). The message says “class” and “extend” even for an interface and a record; it uses the general words.
Every member says what comes below it
A sealed type lists its direct members. Each member must then say whether the family goes on below it, with one of three modifiers:
| Modifier on the member | What it means | Who may extend the member |
|---|---|---|
final | The family stops here. | Nobody. |
sealed | The member is closed too, with a permits list of its own. | Only the classes on its list. |
non-sealed | The member opens the family up again. | Any class at all. |
Leave the modifier off and javac complains: sealed, non-sealed or final modifiers expected. The shapes above needed none of this, because a record is automatically final. Nothing can extend a record.
Sealed classes
sealed works on classes too. Here an abstract class closes its family, and its three members use one modifier each:
public class Main { public static void main(String[] args) { Vehicle[] fleet = { new Car(), new Truck(), new FireTruck(), new Bike(), new RacingBike() }; for (Vehicle v : fleet) { System.out.println(describe(v)); } } static String describe(Vehicle v) { return switch (v) { case Car c -> "a car with 4 wheels"; case FireTruck f -> "a fire truck with a ladder"; case Truck t -> "a truck with 6 wheels"; case Bike b -> "a bike with 2 wheels"; }; }} abstract sealed class Vehicle permits Car, Truck, Bike {} final class Car extends Vehicle {} sealed class Truck extends Vehicle permits FireTruck {} final class FireTruck extends Truck {} non-sealed class Bike extends Vehicle {} class RacingBike extends Bike {}Car is final, so that branch ends there. Truck is sealed with a list of its own, so only FireTruck may extend it. Bike is non-sealed, so any class may extend it, and RacingBike does without appearing on any list.
The switch still handles every vehicle. case Truck t takes plain trucks, because the fire truck was already taken by the case above it. case Bike b takes every bike, including kinds nobody has written yet. The output is a car with 4 wheels, a truck with 6 wheels, a fire truck with a ladder, then a bike with 2 wheels twice.
One more detail lets this switch skip the default: Vehicle is abstract, so no object is ever a plain Vehicle. If it were not abstract, the switch would need a case for Vehicle itself.
permits and Java collects the list for you. Writing it out keeps the list in front of the reader.abstract sealed class Vehicle permits Car, Bus {}, which declaration of Bus does not compile?Key ideaA sealed type names every direct member. Each member is final (nothing below it), sealed (its own short list) or non-sealed (open to anyone).
Exhaustiveness: the compiler checks every case#
Because Shape is sealed, the compiler knows that a shape can only be a circle, a rectangle or a triangle. A switch that handles those three handles every shape there can ever be, so the default can go:
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(3.0, 4.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(area(shape)); } } static double area(Shape shape) { return switch (shape) { case Circle(var r) -> Math.PI * r * r; case Rect(var w, var h) -> w * h; case Triangle(var b, var h) -> 0.5 * b * h; }; }} sealed interface Shape permits Circle, Rect, Triangle {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}A switch that handles every possible value is exhaustive. Java checks that every switch expression is exhaustive when it compiles your code, before anything runs. With an open interface you needed a catch-all such as default to pass that check. With a sealed one, listing every member is enough.
When you forget a case
Take the same program, delete the triangle case, and compile it with a real JDK:
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(3.0, 4.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(area(shape)); } } static double area(Shape shape) { return switch (shape) { case Circle(var r) -> Math.PI * r * r; case Rect(var w, var h) -> w * h; }; }} sealed interface Shape permits Circle, Rect, Triangle {}record Circle(double radius) implements Shape {}record Rect(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}$ javac Main.javaMain.java:10: error: the switch expression does not cover all possible input values return switch (shape) { ^1 errorRead it the way you read any compile error: the file and line (Main.java:10), the complaint, and a caret under the switch. The message does not say which kind is missing. You find it by comparing the cases with the permits list. You find out while compiling, at your desk, long before a user draws the first triangle.
MatchException, when a value arrives that no case matches. A real JDK refuses to compile the program at all, and that is the behavior to expect from Java.Adding a new member
The check earns its keep when the family grows. Say the app gets squares. You write record Square(double side) implements Shape {} and add Square to the permits list. Every switch over Shape that has no square case and no default now fails to compile, each with the error above. Together those errors are a complete to-do list of the places where squares need a decision.
Now imagine those switches had kept a default. Everything compiles, and every square quietly takes the default branch: an area of 0, a description of “some other shape”. Nothing tells you. That is why, for a sealed type, you list every member and leave default out.
Key ideaOver a sealed type, leave out default. Then adding a member turns every switch that forgot it into a compile error: a to-do list you can trust.
The fine print
- A guarded case never counts.
case Rect r when r.width() > 10on its own does not cover rectangles, because its condition could be false, so you still need a case for the rest. Java does not try to reason about what the condition says. - The rule also covers
switchstatements that use patterns orcase null. Such a switch must be exhaustive even when it produces no value, and javac then reportsthe switch statement does not cover all possible input values. An old-style switch on anint, aStringor an enum, with no patterns, still does not have to be. - A chain of
ifandinstanceofgets no such check at all, sealed type or not. Exhaustiveness checking comes only with a switch.
Try the rule for yourself below. Comment cases out, bring them back, add a Square to the family, and read javac's verdict. When the switch compiles, send a value through and see which case takes it.
Tap a case line to comment it out or bring it back. Then add a Square to the family and see what still compiles.
sealed interface Shape permits Circle, Rect, Triangle {} String label = switch (shape) {};JAVAC: COMPILES
Every kind in the permits list has a case without a guard, so every possible Shape is handled.
Send a value through the switch
- 1
case Circle cnot a Circle, so try the next case - 2
case Rect r when r.width() > 10a Rect, but r.width() > 10 is false (the width is 3.0), so try the next case - 3
case Rect ra Rect: this case wins - label is "rectangle"
Shape is sealed and permits Circle, Rect and Triangle. Which switch expression over a Shape compiles?square for a 4 by 4 rectangle. Watch the order of the two Rect cases.Tap a line, then tap where it should go. Or use the arrows.
Two ways to give every shape an area#
Step back and compare all this with polymorphism. There, each class carried its own version of a method, and Java picked the right version at run time. The shapes can work that way too. The interface declares area(), and each record supplies its own:
public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(1.0), new Rect(3.0, 4.0), new Triangle(6.0, 2.0) }; for (Shape shape : shapes) { System.out.println(shape.area()); } }} sealed interface Shape permits Circle, Rect, Triangle { double area();} record Circle(double radius) implements Shape { public double area() { return Math.PI * radius * radius; }} record Rect(double width, double height) implements Shape { public double area() { return width * height; }} record Triangle(double base, double height) implements Shape { public double area() { return 0.5 * base * height; }}Same output, and no switch anywhere: the loop calls shape.area() and each record answers for itself. Neither version is wrong. They arrange the same pieces of code in two directions, which is easiest to see as a table.
| area | describe | draw | |
|---|---|---|---|
Circle | area of a circle | words for a circle | pixels for a circle |
Rect | area of a rectangle | words for a rectangle | pixels for a rectangle |
Triangle | area of a triangle | words for a triangle | pixels for a triangle |
A method in every class cuts this table into rows. Each record holds its own row: every job, for one kind of shape. A switch cuts the table into columns. Each method holds one column: one job, for every kind of shape.
The column style has a name: data-oriented programming. In plain words, you keep the data as plain records, close the set of kinds with sealed, and write each operation as a method with a switch over that set. The records know nothing about drawing or describing; each operation knows how to handle every kind.
Each style makes a different change easy. With a method in every class, a new kind is one new class and nothing else changes, but a new job means editing every class. With switches, a new job is one new method and the records stay as they are, while a new kind means visiting every switch. The compiler makes that visit safe by naming each switch you must touch.
Key ideaRecords hold the data, sealed closes the set of kinds, and each switch holds one job. Pick this style when the kinds are settled and the jobs keep coming.
Your turn: a ticket machine#
A train company sells three kinds of ticket, and each kind is a record holding the passenger's age. The family is sealed, so a switch over it needs no default. Write the pricing rule as one switch expression.
priceOf. Adults pay 12, seniors pay 8 and children pay 6, but children under 3 travel free. Use a switch with type patterns and one guard, and no default.Try it
Answer each one in your head or on paper first, then open the answer.
instanceof pattern: if (item instanceof Book) { Book b = (Book) item; System.out.println(b.title()); }case Rect r comes before case Rect r when r.width() == r.height()?permits list of a sealed class. Which three modifiers can it choose from, and what does each one allow below it?Shape ends with a default. You add Square to the permits list. What does javac say, and what happens when a square reaches that switch?Sticker(Rect(var w, var h), var color) match, and which variables does it give you?Practice
Work through these on your own. Each one builds on this page.
Shape program from the exhaustiveness section into a real Java file, and give it a describe method like the one in the record patterns section, but with no default. Then add record Square(double side) implements Shape {} and put Square in the permits list. Compile before you touch any switch, and write down every error javac gives you. Then add the square cases until it compiles and runs.Message with three records: Text(String body), Photo(String file, int width, int height) and Voice(int seconds). Write preview(Message m) as a switch expression with record patterns that returns, for example, Photo 800x600 or Voice note, 12 s. Add a guard so an empty text previews as (empty message).perimeter for Circle and Rect twice: once as a method in each record, once as a switch in Main. Then add Square to both versions. For each version, list the places you had to edit and say what told you about each one.Sub record for subtraction. Add it to permits first and compile, so you see which switch javac flags, then make new Sub(new Num(10), new Mul(new Num(2), new Num(3))) print 4.Further reading#
Spotted a mistake, or a part that did not make sense? Report it by email