Java Modern Puzzlers, a guide to new programming features
SimonRitter
1 views
58 slides
Oct 15, 2025
Slide 1 of 58
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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).
Size: 2.76 MB
Language: en
Added: Oct 15, 2025
Slides: 58 pages
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 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