Java Modern Puzzlers, a guide to new programming features

SimonRitter 1 views 58 slides Oct 15, 2025
Slide 1
Slide 1 of 58
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58

About This Presentation

A selection of puzzles around new Java language features.
This focuses on the switch expression, pattern matching and sealed classes (sort of).


Slide Content

Modern Java Puzzlers Simon Ritter, Deputy CTO | Azul

Disclaimer This is intended to be a fun session to make you think about how Java works and some of the unexpected things that can happen with modern Java features It is not intended to be a critique of language feature design I think the OpenJDK architects have done an unbelievable job of adding complex language features without breaking compatibility (or the feel of Java)

Modern Java Project Amber has introduced a selection of new language features to Java For this session, we'll be focusing on these: Patterns Switch expressions Sealed classes

Puzzlers 1 Exhausted by Exhaustive

Exhausted By Exhaustive java: the switch expression does not cover all possible input values int x = getValue(); String d = switch ( x ) { case -> "zero" ; case 2 , 4 , 6 -> "even" ; };

Exhausted By Exhaustive Yes, the switch expression is now exhaustive int x = getValue(); String d = switch ( x ) { case -> "zero" ; case 2 , 4 , 6 -> "even" ; default -> "other" ; };

Exhausted By Exhaustive Yes, a switch statement does not need to be exhaustive Imagine the code that would break if that was enforced retrospectively int x = getValue(); String d = null ; switch ( x ) { case -> d = "zero" ; case 2 , 4 , 6 -> d = "even" ; }

Exhausted By Exhaustive This is an enhanced switch statement (it includes a null) so must be exhaustive java: the switch statement does not cover all possible input values Integer x = getValue(); String d = null ; switch ( x ) { case -> d = "zero" ; case 2 , 4 , 6 -> d = "even" ; case null -> d = "null" ; }

Exhausted By Exhaustive Yes, the switch expression is exhaustive public enum AB { A , B }; private String testAB ( AB ab ) { return switch ( ab ) { case A -> "A" ; case B -> "B" ; }; }

Exhausted By Exhaustive public enum AB { A , B }; private String testAB ( AB ab ) { switch ( ab ) { case A -> { return "A" ; } case B -> { return "B" ; } } } "Adding or reordering constants from an enum type will not break compatibility with pre-existing binaries." JLS 13.4.26 java: missing return statement

Exhausted By Exhaustive As with sealed classes, although adding an enum constant to an enum class is considered a binary compatible change, it may cause the execution of an exhaustive switch to fail if the switch encounters the new enum constant that was not known at compile time. public enum AB { A , B }; private String testAB ( AB ab ) { return switch ( ab ) { case A -> "A" ; case B -> "B" ; }; }

Puzzlers 2 Primitives In Patterns

Java Primitives v Objects Primitives are not objects in Java (but arrays of primitives are) Primitive variables store values, not references Hence why we have wrapper classes And autoboxing and unboxing Since JDK 23, we can now include primitives in patterns (as a preview feature) We can also mix and match primitives and wrapper types How (and when) exhaustiveness is required can sometimes require careful consideration Similarly for pattern dominance

Primitives In Switch Yes, the switch expression is exhaustive public void testInt ( int n ) { String r = switch ( n ) { case byte b -> "Byte" ; case int i -> "Integer" ; }; System . out .println( r ); }

Primitives In Switch java: switch has both an unconditional pattern and a default label public void testInt ( int n ) { String r = switch ( n ) { case byte b -> "Byte" ; case int i -> "Integer" ; default -> "Other" ; }; System . out .println( r ); }

Primitives In Switch What is the result of testInt(1024); ? Integer public void testInt ( int n ) { String r = switch ( n ) { case byte b -> "Byte" ; case int i -> "Integer" ; }; System . out .println( r ); }

Primitives In Switch What is the result of testInt(42); ? Byte public void testInt ( int n ) { String r = switch ( n ) { case byte b -> "Byte" ; case int i -> "Integer" ; }; System . out .println( r ); }

Primitives In Switch What is the result of testInt((int)42); ? Byte public void testInt ( int n ) { String r = switch ( n ) { case byte b -> "Byte" ; case int i -> "Integer" ; }; System . out .println( r ); }

Primitives In Switch What is the result of testInt(Integer.valueOf(42)); ? Byte public void testInt ( int n ) { String r = switch ( n ) { case byte b -> "Byte" ; case int i -> "Integer" ; }; System . out .println( r ); }

Primitives In Switch java: this case label is dominated by a preceding case label public void testInt ( int n ) { String r = switch ( n ) { case int i -> "Integer" ; case byte b -> "Byte" ; }; System . out .println( r ); } testInt( 42 );

Primitives With instanceof int x = 42 ; if ( x instanceof int ) System . out .println( " int" ); // prints int (quite logically) if ( x instanceof byte ) System . out .println( " byte" ); // prints byte (42 fits in a byte) // prints byte (42 still fits in a byte) if ( x instanceof byte ) System . out .println( "byte" ); else if ( x instanceof int ) System . out .println( "int" ); if ( x instanceof int ) System . out .println( " int" ); else if ( x instanceof byte ) System . out .println( " byte" ); // prints int (first match, not best match)

Primitive And Reference Types In Switch public void testObj ( Object o ) { switch ( o ) { case byte b -> System . out .println( "Byte" ); case Integer i -> System . out .println( " Integer" ); default -> System . out .println( "Something else" ); }; } What is the result of testObj(Integer.valueOf(42)); ? Integer

Primitive And Reference Types In Switch public void testObj ( Object o ) { switch ( o ) { case byte b -> System . out .println( "Byte" ); case Integer i -> System . out .println( " Integer" ); default -> System . out .println( "Something else" ); }; } What is the result of testObj(42); ? Integer

Primitive And Reference Types In Switch public void testObj ( Object o ) { switch ( o ) { case byte b -> System . out .println( "Byte" ); case Integer i -> System . out .println( " Integer" ); default -> System . out .println( "Something else" ); }; } What is the result of: byte b = 42; testObj(b); ? Byte

Primitive And Reference Types In Switch public void testObj ( Object o ) { switch ( o ) { case Integer i -> System . out .println( "Integer" ); case byte b -> System . out .println( "Byte" ); default -> System . out .println( "Something else" ); }; } testObj(42): Integer

Primitive And Reference Types In Switch public void testObj ( Object o ) { switch ( o ) { case Integer i -> System . out .println( "Integer" ); case byte b -> System . out .println( "Byte" ); default -> System . out .println( "Something else" ); }; } byte bb = 42; testObj(bb): Byte

Primitive And Reference Types In Switch public void testObj ( Object o ) { switch ( o ) { case Integer i -> System . out .println( "Integer" ); case byte b -> System . out .println( "Byte" ); case int i -> System . out .println( "int" ); default -> System . out .println( "Something else" ); }; } java: this case label is dominated by a preceding case label

Primitive And Reference Types In Switch public void testObj ( Object o ) { switch ( o ) { case Integer i -> System . out .println( "Integer" ); case short s -> System . out .println( "short" ); case byte b -> System . out .println( "Byte" ); default -> System . out .println( "Something else" ); }; } java: this case label is dominated by a preceding case label

Primitive And Reference Types In Switch Compiles fine, all values print, Integer public void testInt () { int n = getValue(); switch ( n ) { case Integer i -> System . out .println( "Integer" ); case byte b -> System . out .println( "Byte" ); } }

Puzzlers 3 Guarded Patterns

Guards And Exhaustive The compiler does not evaluate the guard to determine exhaustiveness java: the switch expression does not cover all possible input values int x = getValue(); String d = switch ( x ) { case -> "zero" ; case int i when i < -> d = "negative" ; case int i when i > -> d = "positive" ; };

Guards And Pattern Dominance private void positiveOrNegative ( int x ) { String d = switch ( x ) { case Integer i when i > -> "positive" ; default -> "negative" ; case -> "zero" ; }; System . out .println( "Result is: " + d ); } positiveOrNegative(42); positive

Guards And Pattern Dominance private void positiveOrNegative ( int x ) { String d = switch ( x ) { case Integer i when i > -> "positive" ; default -> "negative" ; case -> "zero" ; }; System . out .println( "Result is: " + d ); } positiveOrNegative(-1); negative

Guards And Pattern Dominance private void positiveOrNegative ( int x ) { String d = switch ( x ) { case Integer i when i > -> "positive" ; default -> "negative" ; case -> "zero" ; }; System . out .println( "Result is: " + d ); } positiveOrNegative(0); zero

Guards And Pattern Dominance private void positiveOrNegative ( int x ) { String d = switch ( x ) { case Integer i when i > -> "positive" ; default -> "negative" ; case Integer i when i == -> "zero" ; }; System . out .println( "Result is: " + d ); } positiveOrNegative(0); java: this case label is dominated by a preceding case label

Guards And Pattern Dominance private void positiveOrNegative ( int x ) { String d = switch ( x ) { case Integer i when i > -> "positive" ; default -> "negative" ; case -> "zero" ; }; System . out .println( "Result is: " + d ); } positiveOrNegative(0); zero Guarded pattern labels don't dominate constant labels What about changing when i > 0 when i == 0

Guards And Pattern Dominance private void positiveOrNegative ( int x ) { String d = switch ( x ) { case Integer i -> "positive" ; default -> "negative" ; case -> "zero" ; }; System . out .println( "Result is: " + d ); } positiveOrNegative(0); java: this case label is dominated by a preceding case label

Multiple Patterns With Guards enum Type { ISOSCELES , EQUILATERAL , RIGHT_ANGLE } record Triangle ( Type t ) {} record Square ( double root ) {} private void tryTwo ( Object o ) { String n = switch ( o ) { case Triangle ( Type t ) when t == Type . ISOSCELES -> "Found" ; case Square ( double r ) when r > 20 -> "Found" ; default -> "Lost" ; }; }

Multiple Patterns With Guards private void tryTwo ( Object o ) { String n = switch ( o ) { case Triangle ( Type t ) when t == Type . ISOSCELES, Square ( double r ) when r > 20 -> "Found" ; default -> "Lost" ; }; } error: : or -> expected Guards are attached to cases, not patterns

Multiple Patterns private void tryTwo ( Object o ) { String n = switch ( o ) { case Triangle t -> "Found" ; case Square s -> "Found" ; default -> "Lost" ; }; }

Multiple Patterns private void tryTwo ( Object o ) { String n = switch ( o ) { case Triangle t , Square s -> "Found" ; default -> "Lost" ; }; } error: illegal fall-through from a pattern (the current case label is missing a break) Invalid case label combination: multiple patterns are allowed only if none of them declare any pattern variables

Multiple Patterns private void tryTwo ( Object o ) { String n = switch ( o ) { case Triangle _ , Square _ -> "Found" ; default -> "Lost" ; }; }

Puzzler 4 Sealed And Non-Sealed

Java Restricted Identifiers From JDK 16, these are now referred to as Reserved keywords abstract case continue else float import long private short synchronized transient true boolean catch default extends for instanceof native protected static this try false break char do final if int new public super throw void null byte class double finally implements interface package return switch throws volatile assert enum goto const strictfp _

Java Restricted Keywords requires requires; module { } exports exports to to; module opens opens; module-info.java

Java Reserved Type JDK 10 introduced local variable type inference (var) This is a reserved type (and restricted identifier) class var { ... } Possible (but not advised) until JDK 9 class Var { ... } var var = new Var();

Java Contextual Keywords (JDK 16) Reclassified from restricted keywords Only work as keywords when used in specific places They can still be used as identifiers module requires opens exports opens to transitive uses provides with open var record yield when permits sealed non-sealed

When Is non-sealed, sealed? public sealed class Sealed permits NonSealed { } non-sealed class NonSealed extends Sealed { public NonSealed (){ int non = 4 ; int sealed = 2 ; System . out .printf( "Class is " + ( non - sealed )); // Prints "Class is 2" } } public void confused () { int non­-sealed ; non-sealed = 2 ; // No compiler error non-sealed = 3 ; // Compiler error }

When Is non-sealed, sealed? public sealed class Sealed permits NonSealed { } non-sealed class NonSealed extends Sealed { public NonSealed (){ int non = 4 ; int sealed = 2 ; System . out .printf( "Class is " + ( non - sealed )); // Prints "Class is 2" } } Unicode character \u001d is a soft hyphen and classified as a identifier-ignorable character public void confused () { int non­-sealed ; non-sealed = 2 ; // No compiler error non-sealed = 3 ; // Cannot find symbol, symbol: variable non }

When Is non-sealed, sealed? public sealed class Sealed permits NonSealed { } non-sealed class NonSealed extends Sealed { public NonSealed (){ int non = 4 ; int sealed = 2 ; System . out .printf( "Class is " + ( non - sealed )); // Prints "Class is 2" } } public void confused () { int non­-sealed ; non-sealed = 2 ; // No compiler error int nonsealed = 3 ; // variable nonsealed is already defined in method confused() }

Puzzler 5 Comment without comment

Comment Without Comment public class EndOfFile { public EndOfFile() { ... } } interface EOFInterface { public int getValue (); } /* Enum */ enum EOFEnum { START , MIDDLE , END }; /* Record */ record EOFRecord ( int i ) {} /* Annotation */ @ interface EOFAnnotation {} How to get the compiler to ignore EOFEnum, EOFRecord and EOFAnnotation by adding the minimum number of characters? (We cannot remove any characters)

Comment Without Comment public class EndOfFile { public EndOfFile() { ... } } interface EOFInterface { public int getValue (); } /* Enum */ /* enum EOFEnum { START , MIDDLE , END };*/ /* Record */ /* record EOFRecord ( int i ) {}*/ /* Annotation */ /*@ interface EOFAnnotation {}*/ The block comment syntax requires 12 characters

Comment Without Comment public class EndOfFile { public EndOfFile() { ... } } interface EOFInterface { public int getValue (); } /* Enum */ // enum EOFEnum { // START , MIDDLE , END // }; /* Record */ // record EOFRecord ( int i ) {} /* Annotation */ //@ interface EOFAnnotation {} Single line comments require 10 characters

Comment Without Comment public class EndOfFile { public EndOfFile() { ... } } interface EOFInterface { public int getValue (); } \u001a/* Enum */ enum EOFEnum { START , MIDDLE , END }; /* Record */ record EOFRecord ( int i ) {} /* Annotation */ @ interface EOFAnnotation {} \u001a is the Unicode end-of-file character Prior to JDK 16, this would cause a compiler error Now, everything after \u001a is ignored

Summary

Conclusions Modern Java features add more power and less boilerplate to the language Sometimes things don't behave quite how you would expect Almost always, this is logical and well thought out Your IDE will help you a lot

Thank you! @speakjava