Add patterns module: pattern matching from JDK 21 to 27
Type, record and primitive patterns, guards, sealed exhaustiveness, MatchException via separate compilation, and eight captured transcripts. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
@@ -65,6 +65,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T
|
|||||||
| [`jep512/`](jep512) | Compact Source Files and Instance Main Methods in Java 25 (JEP 512) | `java Hello.java` on JDK 21 vs 25 vs 27, the class `javac` builds around `void main()`, implicit `java.base` imports, `java.lang.IO`, the measured launch order, a private-constructor difference between 25 and 27 |
|
| [`jep512/`](jep512) | Compact Source Files and Instance Main Methods in Java 25 (JEP 512) | `java Hello.java` on JDK 21 vs 25 vs 27, the class `javac` builds around `void main()`, implicit `java.base` imports, `java.lang.IO`, the measured launch order, a private-constructor difference between 25 and 27 |
|
||||||
| [`jep513/`](jep513) | Flexible Constructor Bodies in Java 25 (JEP 513): Validate Before super() | statements before `super(...)`/`this(...)`, the parent-calls-child bug and its fix, twelve compile errors that remain with the 25 and 27 wordings side by side, `--release` rejection |
|
| [`jep513/`](jep513) | Flexible Constructor Bodies in Java 25 (JEP 513): Validate Before super() | statements before `super(...)`/`this(...)`, the parent-calls-child bug and its fix, twelve compile errors that remain with the 25 and 27 wordings side by side, `--release` rejection |
|
||||||
| [`jep511/`](jep511) | Module Import Declarations in Java 25 (JEP 511) | `import module java.base;` against seven single imports, ambiguity errors and how single-type, on-demand and same-package names beat a module import, what one module import does and does not bring in (`java.sql` does not give `java.base`; `java.se` needs `--add-modules`), named modules, JEP 512 compact files, jshell's default imports, identical bytecode |
|
| [`jep511/`](jep511) | Module Import Declarations in Java 25 (JEP 511) | `import module java.base;` against seven single imports, ambiguity errors and how single-type, on-demand and same-package names beat a module import, what one module import does and does not bring in (`java.sql` does not give `java.base`; `java.se` needs `--add-modules`), named modules, JEP 512 compact files, jshell's default imports, identical bytecode |
|
||||||
|
| [`patterns/`](patterns) | Pattern Matching in Java: switch, Record Patterns and Primitive Patterns (JDK 21 to 27) | type and record patterns, guards and dominance errors, sealed exhaustiveness (with JDK 27's new `missing patterns` lines), `MatchException` from a throwing accessor and from separate compilation, unnamed patterns, primitive patterns with `--enable-preview` and the preview class-file trap |
|
||||||
|
|
||||||
## Captured output (`docs/output/`)
|
## Captured output (`docs/output/`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# patterns — Pattern matching from JDK 21 to 27
|
||||||
|
|
||||||
|
Companion code for the ankurm.com article **Pattern Matching in Java: switch, Record Patterns and Primitive Patterns (JDK 21 to 27)**. The explanation
|
||||||
|
lives in the article; this folder holds the runnable sources and the transcripts they produced.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
JDK25=/path/to/jdk-25 JDK27=/path/to/jdk-27 [JDK21=/path/to/jdk-21] ./run.sh # regenerates output/*.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
- `src/` compiles and runs on 21, 25 and 27 (except `Unnamed.java`, which needs 22 or later).
|
||||||
|
- `broken/` fails to compile on purpose.
|
||||||
|
- `preview/` uses primitive patterns (JEP 532, preview in 27) and needs `--enable-preview --release N`.
|
||||||
|
- `sep/` is a sealed hierarchy compiled in two versions, to show the one runtime failure an exhaustive `switch` cannot prevent.
|
||||||
|
|
||||||
|
Tested on Temurin 25.0.4.1+1 and 27+35, and OpenJDK 21.0.10 for the baseline.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
public class Dominated {
|
||||||
|
static String f(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case CharSequence cs -> "chars";
|
||||||
|
case String s -> "string"; // String is a CharSequence, so this can never be reached
|
||||||
|
default -> "other";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static void main(String[] args) { System.out.println(f("x")); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
public class GuardBeforeGeneral {
|
||||||
|
static String f(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case String s -> "any string";
|
||||||
|
case String s when s.isEmpty() -> "empty string"; // guarded case after the unguarded one
|
||||||
|
default -> "other";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static void main(String[] args) { System.out.println(f("")); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
public class NotExhaustive {
|
||||||
|
sealed interface Shape permits Circle, Square, Rect {}
|
||||||
|
record Circle(double r) implements Shape {}
|
||||||
|
record Square(double side) implements Shape {}
|
||||||
|
record Rect(double w, double h) implements Shape {}
|
||||||
|
|
||||||
|
static double area(Shape s) {
|
||||||
|
return switch (s) {
|
||||||
|
case Circle(double r) -> Math.PI * r * r;
|
||||||
|
case Square(double side) -> side * side;
|
||||||
|
// Rect is missing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static void main(String[] args) { System.out.println(area(new Circle(1))); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
public class NotExhaustiveObject {
|
||||||
|
static String f(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case String s -> "string";
|
||||||
|
case Integer i -> "int";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static void main(String[] args) { System.out.println(f("x")); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
$ javac src/TypePatterns.java && java TypePatterns (21.0.10+7-Ubuntu-124.04)
|
||||||
|
string of length 5 / string of length 5
|
||||||
|
3 -1
|
||||||
|
int 42
|
||||||
|
string JAVA
|
||||||
|
int array of 3
|
||||||
|
other Double
|
||||||
|
|
||||||
|
$ javac src/TypePatterns.java && java TypePatterns (25.0.4.1+1-LTS)
|
||||||
|
string of length 5 / string of length 5
|
||||||
|
3 -1
|
||||||
|
int 42
|
||||||
|
string JAVA
|
||||||
|
int array of 3
|
||||||
|
other Double
|
||||||
|
|
||||||
|
$ javac src/TypePatterns.java && java TypePatterns (27+35)
|
||||||
|
string of length 5 / string of length 5
|
||||||
|
3 -1
|
||||||
|
int 42
|
||||||
|
string JAVA
|
||||||
|
int array of 3
|
||||||
|
other Double
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
$ javac src/Guards.java && java Guards (25.0.4.1+1-LTS)
|
||||||
|
empty string
|
||||||
|
short string: abc
|
||||||
|
long string: abcdefgh
|
||||||
|
negative -4
|
||||||
|
int 12
|
||||||
|
other
|
||||||
|
|
||||||
|
$ javac src/Guards.java && java Guards (27+35)
|
||||||
|
empty string
|
||||||
|
short string: abc
|
||||||
|
long string: abcdefgh
|
||||||
|
negative -4
|
||||||
|
int 12
|
||||||
|
other
|
||||||
|
|
||||||
|
$ javac broken/Dominated.java (25.0.4.1+1-LTS)
|
||||||
|
broken/Dominated.java:5: error: this case label is dominated by a preceding case label
|
||||||
|
case String s -> "string"; // String is a CharSequence, so this can never be reached
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/Dominated.java (27+35)
|
||||||
|
broken/Dominated.java:5: error: this case label is dominated by a preceding case label
|
||||||
|
case String s -> "string"; // String is a CharSequence, so this can never be reached
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/GuardBeforeGeneral.java (25.0.4.1+1-LTS)
|
||||||
|
broken/GuardBeforeGeneral.java:5: error: this case label is dominated by a preceding case label
|
||||||
|
case String s when s.isEmpty() -> "empty string"; // guarded case after the unguarded one
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/GuardBeforeGeneral.java (27+35)
|
||||||
|
broken/GuardBeforeGeneral.java:5: error: this case label is dominated by a preceding case label
|
||||||
|
case String s when s.isEmpty() -> "empty string"; // guarded case after the unguarded one
|
||||||
|
^
|
||||||
|
1 error
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
$ javac src/RecordPatterns.java && java RecordPatterns (21.0.10+7-Ubuntu-124.04)
|
||||||
|
point at 3,4
|
||||||
|
not a point
|
||||||
|
7
|
||||||
|
box of string hi
|
||||||
|
box of int 7
|
||||||
|
box of 2.5
|
||||||
|
|
||||||
|
$ javac src/RecordPatterns.java && java RecordPatterns (25.0.4.1+1-LTS)
|
||||||
|
point at 3,4
|
||||||
|
not a point
|
||||||
|
7
|
||||||
|
box of string hi
|
||||||
|
box of int 7
|
||||||
|
box of 2.5
|
||||||
|
|
||||||
|
$ javac src/RecordPatterns.java && java RecordPatterns (27+35)
|
||||||
|
point at 3,4
|
||||||
|
not a point
|
||||||
|
7
|
||||||
|
box of string hi
|
||||||
|
box of int 7
|
||||||
|
box of 2.5
|
||||||
|
|
||||||
|
$ javac src/MatchExceptionDemo.java && java MatchExceptionDemo (25.0.4.1+1-LTS)
|
||||||
|
MatchException: java.lang.IllegalStateException: accessor blew up
|
||||||
|
cause: java.lang.IllegalStateException: accessor blew up
|
||||||
|
|
||||||
|
$ javac src/MatchExceptionDemo.java && java MatchExceptionDemo (27+35)
|
||||||
|
MatchException: java.lang.IllegalStateException: accessor blew up
|
||||||
|
cause: java.lang.IllegalStateException: accessor blew up
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
$ javac src/SealedExhaustive.java && java SealedExhaustive (21.0.10+7-Ubuntu-124.04)
|
||||||
|
Circle[r=1.0] -> 3.14
|
||||||
|
Square[side=2.0] -> 4.00
|
||||||
|
Rect[w=2.0, h=3.0] -> 6.00
|
||||||
|
|
||||||
|
$ javac src/SealedExhaustive.java && java SealedExhaustive (25.0.4.1+1-LTS)
|
||||||
|
Circle[r=1.0] -> 3.14
|
||||||
|
Square[side=2.0] -> 4.00
|
||||||
|
Rect[w=2.0, h=3.0] -> 6.00
|
||||||
|
|
||||||
|
$ javac src/SealedExhaustive.java && java SealedExhaustive (27+35)
|
||||||
|
Circle[r=1.0] -> 3.14
|
||||||
|
Square[side=2.0] -> 4.00
|
||||||
|
Rect[w=2.0, h=3.0] -> 6.00
|
||||||
|
|
||||||
|
$ javac broken/NotExhaustive.java (21.0.10+7-Ubuntu-124.04)
|
||||||
|
broken/NotExhaustive.java:8: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (s) {
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/NotExhaustive.java (25.0.4.1+1-LTS)
|
||||||
|
broken/NotExhaustive.java:8: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (s) {
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/NotExhaustive.java (27+35)
|
||||||
|
broken/NotExhaustive.java:8: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (s) {
|
||||||
|
^
|
||||||
|
missing patterns:
|
||||||
|
Rect _
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/NotExhaustiveObject.java (21.0.10+7-Ubuntu-124.04)
|
||||||
|
broken/NotExhaustiveObject.java:3: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (o) {
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/NotExhaustiveObject.java (25.0.4.1+1-LTS)
|
||||||
|
broken/NotExhaustiveObject.java:3: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (o) {
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac broken/NotExhaustiveObject.java (27+35)
|
||||||
|
broken/NotExhaustiveObject.java:3: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (o) {
|
||||||
|
^
|
||||||
|
missing patterns:
|
||||||
|
Object _
|
||||||
|
1 error
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
$ java -cp old Client (Client and Shape compiled together, v1) (25.0.4.1+1-LTS)
|
||||||
|
circle: 3.141592653589793
|
||||||
|
square: 4.0
|
||||||
|
|
||||||
|
$ java -cp new:old Main (Shape gained Triangle; Client.class NOT recompiled) (25.0.4.1+1-LTS)
|
||||||
|
Exception in thread "main" java.lang.MatchException
|
||||||
|
at Client.area(Client.java:4)
|
||||||
|
at Main.main(Main.java:4)
|
||||||
|
|
||||||
|
$ javac -cp new sep/client/Client.java (now recompile Client against v2) (25.0.4.1+1-LTS)
|
||||||
|
sep/client/Client.java:4: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (s) {
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ java -cp old Client (Client and Shape compiled together, v1) (27+35)
|
||||||
|
circle: 3.141592653589793
|
||||||
|
square: 4.0
|
||||||
|
|
||||||
|
$ java -cp new:old Main (Shape gained Triangle; Client.class NOT recompiled) (27+35)
|
||||||
|
Exception in thread "main" java.lang.MatchException
|
||||||
|
at Client.area(Client.java:4)
|
||||||
|
at Main.main(Main.java:4)
|
||||||
|
|
||||||
|
$ javac -cp new sep/client/Client.java (now recompile Client against v2) (27+35)
|
||||||
|
sep/client/Client.java:4: error: the switch expression does not cover all possible input values
|
||||||
|
return switch (s) {
|
||||||
|
^
|
||||||
|
missing patterns:
|
||||||
|
Triangle _
|
||||||
|
1 error
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
$ javac src/NullCase.java && java NullCase (25.0.4.1+1-LTS)
|
||||||
|
without case null: NullPointerException
|
||||||
|
with case null: null!
|
||||||
|
case null, default: null or something else
|
||||||
|
instanceof with null: false
|
||||||
|
|
||||||
|
$ javac src/NullCase.java && java NullCase (27+35)
|
||||||
|
without case null: NullPointerException
|
||||||
|
with case null: null!
|
||||||
|
case null, default: null or something else
|
||||||
|
instanceof with null: false
|
||||||
|
|
||||||
|
$ javac src/Unnamed.java && java Unnamed (21.0.10+7-Ubuntu-124.04)
|
||||||
|
src/Unnamed.java:9: error: unnamed variables are a preview feature and are disabled by default.
|
||||||
|
case Circle _ -> "round"; // the type matters, the value does not
|
||||||
|
^
|
||||||
|
(use --enable-preview to enable unnamed variables)
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac src/Unnamed.java && java Unnamed (25.0.4.1+1-LTS)
|
||||||
|
round angular
|
||||||
|
5
|
||||||
|
|
||||||
|
$ javac src/Unnamed.java && java Unnamed (27+35)
|
||||||
|
round angular
|
||||||
|
5
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
$ javac preview/PrimitiveInstanceof.java (25.0.4.1+1-LTS)
|
||||||
|
preview/PrimitiveInstanceof.java:3: error: primitive patterns are a preview feature and are disabled by default.
|
||||||
|
if (i instanceof byte b) return i + " fits in a byte: " + b;
|
||||||
|
^
|
||||||
|
(use --enable-preview to enable primitive patterns)
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac preview/PrimitiveInstanceof.java (27+35)
|
||||||
|
preview/PrimitiveInstanceof.java:3: error: primitive patterns are a preview feature and are disabled by default.
|
||||||
|
if (i instanceof byte b) return i + " fits in a byte: " + b;
|
||||||
|
^
|
||||||
|
(use --enable-preview to enable primitive patterns)
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 25 preview/PrimitiveInstanceof.java && java --enable-preview PrimitiveInstanceof (25.0.4.1+1-LTS)
|
||||||
|
100 fits in a byte: 100
|
||||||
|
1000 fits in a short: 1000
|
||||||
|
100000 needs an int
|
||||||
|
0.5 converts to float 0.5 without loss
|
||||||
|
0.1 does NOT convert to float without loss
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 27 preview/PrimitiveInstanceof.java && java --enable-preview PrimitiveInstanceof (27+35)
|
||||||
|
100 fits in a byte: 100
|
||||||
|
1000 fits in a short: 1000
|
||||||
|
100000 needs an int
|
||||||
|
0.5 converts to float 0.5 without loss
|
||||||
|
0.1 does NOT convert to float without loss
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 25 preview/PrimitiveSwitch.java && java --enable-preview PrimitiveSwitch (25.0.4.1+1-LTS)
|
||||||
|
OK | other success 204 | client error 404 | something else 500
|
||||||
|
yes no
|
||||||
|
zero one many (9223372036854775807)
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 27 preview/PrimitiveSwitch.java && java --enable-preview PrimitiveSwitch (27+35)
|
||||||
|
OK | other success 204 | client error 404 | something else 500
|
||||||
|
yes no
|
||||||
|
zero one many (9223372036854775807)
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 25 preview/PrimitiveInRecord.java && java --enable-preview PrimitiveInRecord (25.0.4.1+1-LTS)
|
||||||
|
whole degrees: 21
|
||||||
|
fractional: 21.5
|
||||||
|
not a reading
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 27 preview/PrimitiveInRecord.java && java --enable-preview PrimitiveInRecord (27+35)
|
||||||
|
whole degrees: 21
|
||||||
|
fractional: 21.5
|
||||||
|
not a reading
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 25 preview/PrimitiveEdges.java && java --enable-preview PrimitiveEdges (25.0.4.1+1-LTS)
|
||||||
|
0.0 int? true float? true
|
||||||
|
-0.0 int? false float? true
|
||||||
|
NaN int? false float? true
|
||||||
|
1.6777217E7 int? true float? false
|
||||||
|
1.0E10 int? false float? true
|
||||||
|
3.0 int? true float? true
|
||||||
|
1099511627776 instanceof int? false
|
||||||
|
Object holding Integer matches int pattern: 42
|
||||||
|
Short in Object does not match int
|
||||||
|
65 as char: A
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 27 preview/PrimitiveEdges.java && java --enable-preview PrimitiveEdges (27+35)
|
||||||
|
0.0 int? true float? true
|
||||||
|
-0.0 int? false float? true
|
||||||
|
NaN int? false float? true
|
||||||
|
1.6777217E7 int? true float? false
|
||||||
|
1.0E10 int? false float? true
|
||||||
|
3.0 int? true float? true
|
||||||
|
1099511627776 instanceof int? false
|
||||||
|
Object holding Integer matches int pattern: 42
|
||||||
|
Short in Object does not match int
|
||||||
|
65 as char: A
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 25 preview/BoxedToNarrower.java && java --enable-preview BoxedToNarrower (25.0.4.1+1-LTS)
|
||||||
|
preview/BoxedToNarrower.java:4: error: incompatible types: Integer cannot be converted to byte
|
||||||
|
if (boxed instanceof byte b) System.out.println("fits a byte: " + b); // Integer to byte is not allowed
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
Error: Could not find or load main class BoxedToNarrower
|
||||||
|
Caused by: java.lang.ClassNotFoundException: BoxedToNarrower
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 27 preview/BoxedToNarrower.java && java --enable-preview BoxedToNarrower (27+35)
|
||||||
|
preview/BoxedToNarrower.java:4: error: incompatible types: Integer cannot be converted to byte
|
||||||
|
if (boxed instanceof byte b) System.out.println("fits a byte: " + b); // Integer to byte is not allowed
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
Error: Could not find or load main class BoxedToNarrower
|
||||||
|
Caused by: java.lang.ClassNotFoundException: BoxedToNarrower
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
$ javap -v PrimitiveSwitch | grep -E 'minor|major' (compiled by 25.0.4.1+1-LTS with --enable-preview)
|
||||||
|
minor version: 65535
|
||||||
|
major version: 69
|
||||||
|
|
||||||
|
$ java --enable-preview PrimitiveSwitch (class compiled on 25, run on 27+35)
|
||||||
|
Error: LinkageError occurred while loading main class PrimitiveSwitch
|
||||||
|
java.lang.UnsupportedClassVersionError: PrimitiveSwitch (class file version 69.65535) was compiled with preview features that are unsupported. This version of the Java Runtime only recognizes preview features for class file version 71.65535
|
||||||
|
|
||||||
|
$ java PrimitiveSwitch (same class, no flag, run on 25.0.4.1+1-LTS)
|
||||||
|
Error: LinkageError occurred while loading main class PrimitiveSwitch
|
||||||
|
java.lang.UnsupportedClassVersionError: Preview features are not enabled for PrimitiveSwitch (class file version 69.65535). Try running with '--enable-preview'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
public class BoxedToNarrower {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Integer boxed = 300;
|
||||||
|
if (boxed instanceof byte b) System.out.println("fits a byte: " + b); // Integer to byte is not allowed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
public class PrimitiveEdges {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
double[] ds = {0.0, -0.0, Double.NaN, 16777217.0, 1e10, 3.0};
|
||||||
|
for (double d : ds) {
|
||||||
|
System.out.printf("%-12s int? %-5b float? %-5b%n", d, d instanceof int, d instanceof float);
|
||||||
|
}
|
||||||
|
long big = 1L << 40;
|
||||||
|
System.out.println(big + " instanceof int? " + (big instanceof int));
|
||||||
|
Object o = 42;
|
||||||
|
if (o instanceof int i) System.out.println("Object holding Integer matches int pattern: " + i);
|
||||||
|
Object s = (short) 7;
|
||||||
|
if (s instanceof int i) System.out.println("Short in Object matches int? " + i); else System.out.println("Short in Object does not match int");
|
||||||
|
int x = 65;
|
||||||
|
if (x instanceof char c) System.out.println("65 as char: " + c);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
public class PrimitiveInRecord {
|
||||||
|
record Reading(double celsius) {}
|
||||||
|
|
||||||
|
static String classify(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case Reading(int whole) -> "whole degrees: " + whole; // double component matched against an int pattern
|
||||||
|
case Reading(double d) -> "fractional: " + d;
|
||||||
|
default -> "not a reading";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(classify(new Reading(21.0)));
|
||||||
|
System.out.println(classify(new Reading(21.5)));
|
||||||
|
System.out.println(classify("x"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
public class PrimitiveInstanceof {
|
||||||
|
static String describe(int i) {
|
||||||
|
if (i instanceof byte b) return i + " fits in a byte: " + b;
|
||||||
|
if (i instanceof short s) return i + " fits in a short: " + s;
|
||||||
|
return i + " needs an int";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void floats(double d) {
|
||||||
|
if (d instanceof float f) System.out.println(d + " converts to float " + f + " without loss");
|
||||||
|
else System.out.println(d + " does NOT convert to float without loss");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(describe(100));
|
||||||
|
System.out.println(describe(1000));
|
||||||
|
System.out.println(describe(100000));
|
||||||
|
floats(0.5);
|
||||||
|
floats(0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
public class PrimitiveSwitch {
|
||||||
|
static String httpFamily(int status) {
|
||||||
|
return switch (status) {
|
||||||
|
case 200 -> "OK";
|
||||||
|
case int i when i >= 200 && i < 300 -> "other success " + i;
|
||||||
|
case int i when i >= 400 && i < 500 -> "client error " + i;
|
||||||
|
case int i -> "something else " + i;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String flag(boolean b) {
|
||||||
|
return switch (b) {
|
||||||
|
case true -> "yes";
|
||||||
|
case false -> "no";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String big(long n) {
|
||||||
|
return switch (n) {
|
||||||
|
case 0L -> "zero";
|
||||||
|
case 1L -> "one";
|
||||||
|
case long l -> "many (" + l + ")";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(httpFamily(200) + " | " + httpFamily(204) + " | " + httpFamily(404) + " | " + httpFamily(500));
|
||||||
|
System.out.println(flag(true) + " " + flag(false));
|
||||||
|
System.out.println(big(0) + " " + big(1) + " " + big(Long.MAX_VALUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pattern matching from JDK 21 to 27: type/record patterns, guards, exhaustiveness, unnamed patterns (final),
|
||||||
|
# and primitive patterns (JEP 532, preview in 27; also preview on 25 with --enable-preview).
|
||||||
|
# Regenerates every file in output/. JDK25=/path JDK27=/path [JDK21=/path] ./run.sh
|
||||||
|
set -uo pipefail
|
||||||
|
unset JAVA_TOOL_OPTIONS
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
JDK25="${JDK25:-/opt/jdks/jdk-25.0.4.1+1}"; JDK27="${JDK27:-/opt/jdks/jdk27}"; JDK21="${JDK21:-}"
|
||||||
|
OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT
|
||||||
|
cd "$HERE"
|
||||||
|
t() { "$1/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'; }
|
||||||
|
rel() { case "$1" in "$JDK25") echo 25;; "$JDK27") echo 27;; *) echo 21;; esac; }
|
||||||
|
mk() { mktemp -d -p "$B"; }
|
||||||
|
# compile + run (no flags)
|
||||||
|
cr() { local jdk="$1" src="$2" cls="$3" o; o="$(mk)"
|
||||||
|
echo "\$ javac $src && java $cls ($(t "$jdk"))"
|
||||||
|
"$jdk/bin/javac" -d "$o" "$src" 2>&1 && "$jdk/bin/java" -cp "$o" "$cls" 2>&1; }
|
||||||
|
# compile only, show diagnostics
|
||||||
|
co() { local jdk="$1" src="$2" o; o="$(mk)"
|
||||||
|
echo "\$ javac $src ($(t "$jdk"))"
|
||||||
|
"$jdk/bin/javac" -d "$o" "$src" 2>&1 | grep -v '^Note:'; }
|
||||||
|
# preview compile + run
|
||||||
|
crp() { local jdk="$1" src="$2" cls="$3" o r; o="$(mk)"; r="$(rel "$jdk")"
|
||||||
|
echo "\$ javac --enable-preview --release $r $src && java --enable-preview $cls ($(t "$jdk"))"
|
||||||
|
"$jdk/bin/javac" --enable-preview --release "$r" -d "$o" "$src" 2>&1 | grep -v '^Note:'
|
||||||
|
"$jdk/bin/java" --enable-preview -cp "$o" "$cls" 2>&1; }
|
||||||
|
both() { local fn="$1"; shift; "$fn" "$JDK25" "$@"; echo; "$fn" "$JDK27" "$@"; }
|
||||||
|
all3() { local fn="$1"; shift; [ -n "$JDK21" ] && { "$fn" "$JDK21" "$@"; echo; }; "$fn" "$JDK25" "$@"; echo; "$fn" "$JDK27" "$@"; }
|
||||||
|
|
||||||
|
# 01 - type patterns
|
||||||
|
{ all3 cr src/TypePatterns.java TypePatterns; } > "$OUT/01-type-patterns.txt"
|
||||||
|
|
||||||
|
# 02 - guards and dominance
|
||||||
|
{ both cr src/Guards.java Guards; echo; both co broken/Dominated.java; echo; both co broken/GuardBeforeGeneral.java; } > "$OUT/02-guards-and-dominance.txt"
|
||||||
|
|
||||||
|
# 03 - record patterns, and what happens when an accessor throws
|
||||||
|
{ all3 cr src/RecordPatterns.java RecordPatterns; echo; both cr src/MatchExceptionDemo.java MatchExceptionDemo; } > "$OUT/03-record-patterns.txt"
|
||||||
|
|
||||||
|
# 04 - exhaustiveness: the compiler's check, and what separate compilation can still break
|
||||||
|
{ all3 cr src/SealedExhaustive.java SealedExhaustive; echo
|
||||||
|
all3 co broken/NotExhaustive.java; echo
|
||||||
|
all3 co broken/NotExhaustiveObject.java; } > "$OUT/04-exhaustiveness.txt"
|
||||||
|
{ for j in "$JDK25" "$JDK27"; do
|
||||||
|
o="$(mk)"; mkdir "$o/old" "$o/new"
|
||||||
|
"$j/bin/javac" -d "$o/old" sep/v1/*.java sep/client/Client.java 2>&1
|
||||||
|
"$j/bin/javac" -cp "$o/old" -d "$o/new" sep/v2/*.java 2>&1
|
||||||
|
echo "\$ java -cp old Client (Client and Shape compiled together, v1) ($(t "$j"))"; "$j/bin/java" -cp "$o/old" Client 2>&1; echo
|
||||||
|
echo "\$ java -cp new:old Main (Shape gained Triangle; Client.class NOT recompiled) ($(t "$j"))"; "$j/bin/java" -cp "$o/new:$o/old" Main 2>&1 | head -4; echo
|
||||||
|
echo "\$ javac -cp new sep/client/Client.java (now recompile Client against v2) ($(t "$j"))"
|
||||||
|
"$j/bin/javac" -cp "$o/new" -d "$(mk)" sep/client/Client.java 2>&1; echo
|
||||||
|
done; } > "$OUT/05-separate-compilation.txt"
|
||||||
|
|
||||||
|
# 06 - null and unnamed patterns
|
||||||
|
{ both cr src/NullCase.java NullCase; echo; all3 cr src/Unnamed.java Unnamed; } > "$OUT/06-null-and-unnamed.txt"
|
||||||
|
|
||||||
|
# 07 - primitive patterns (preview)
|
||||||
|
{ for f in PrimitiveInstanceof; do both co preview/$f.java; done; echo
|
||||||
|
for f in PrimitiveInstanceof PrimitiveSwitch PrimitiveInRecord PrimitiveEdges; do both crp preview/$f.java $f; echo; done
|
||||||
|
both crp preview/BoxedToNarrower.java BoxedToNarrower; } > "$OUT/07-primitive-patterns.txt"
|
||||||
|
{ o="$(mk)"; "$JDK25/bin/javac" --enable-preview --release 25 -d "$o" preview/PrimitiveSwitch.java 2>&1 | grep -v '^Note:'
|
||||||
|
echo "\$ javap -v PrimitiveSwitch | grep -E 'minor|major' (compiled by $(t "$JDK25") with --enable-preview)"
|
||||||
|
"$JDK25/bin/javap" -v -cp "$o" PrimitiveSwitch | grep -E 'minor|major'; echo
|
||||||
|
echo "\$ java --enable-preview PrimitiveSwitch (class compiled on 25, run on $(t "$JDK27"))"; "$JDK27/bin/java" --enable-preview -cp "$o" PrimitiveSwitch 2>&1 | head -2; echo
|
||||||
|
echo "\$ java PrimitiveSwitch (same class, no flag, run on $(t "$JDK25"))"; "$JDK25/bin/java" -cp "$o" PrimitiveSwitch 2>&1 | head -2
|
||||||
|
} > "$OUT/08-preview-class-files.txt"
|
||||||
|
echo "wrote $OUT"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
public class Client {
|
||||||
|
// Compiled against Shape v1, where the only permitted subtypes are Circle and Square.
|
||||||
|
public static double area(Shape s) {
|
||||||
|
return switch (s) {
|
||||||
|
case Circle c -> Math.PI * c.r() * c.r();
|
||||||
|
case Square q -> q.side() * q.side();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("circle: " + area(new Circle(1)));
|
||||||
|
System.out.println("square: " + area(new Square(2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
public record Circle(double r) implements Shape {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
public sealed interface Shape permits Circle, Square {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
public record Square(double side) implements Shape {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
public record Circle(double r) implements Shape {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
public class Main {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Triangle exists only in v2. Client was compiled before it did.
|
||||||
|
System.out.println("triangle: " + Client.area(new Triangle(3, 4)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
public sealed interface Shape permits Circle, Square, Triangle {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
public record Square(double side) implements Shape {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
public record Triangle(double base, double height) implements Shape {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
public class Guards {
|
||||||
|
static String size(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case String s when s.isEmpty() -> "empty string";
|
||||||
|
case String s when s.length() < 5 -> "short string: " + s;
|
||||||
|
case String s -> "long string: " + s;
|
||||||
|
case Integer i when i < 0 -> "negative " + i;
|
||||||
|
case Integer i -> "int " + i;
|
||||||
|
default -> "other";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
for (Object o : new Object[]{"", "abc", "abcdefgh", -4, 12, 1.5}) System.out.println(size(o));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
public class MatchExceptionDemo {
|
||||||
|
record Fragile(int v) {
|
||||||
|
public int v() { throw new IllegalStateException("accessor blew up"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Object o = new Fragile(1);
|
||||||
|
try {
|
||||||
|
if (o instanceof Fragile(int v)) System.out.println("matched " + v);
|
||||||
|
} catch (MatchException e) {
|
||||||
|
System.out.println("MatchException: " + e.getMessage());
|
||||||
|
System.out.println("cause: " + e.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
public class NullCase {
|
||||||
|
static String withoutCaseNull(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case String s -> "string";
|
||||||
|
default -> "other";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String withCaseNull(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case null -> "null!";
|
||||||
|
case String s -> "string";
|
||||||
|
default -> "other";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String combined(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case String s -> "string";
|
||||||
|
case null, default -> "null or something else";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try { System.out.println(withoutCaseNull(null)); }
|
||||||
|
catch (NullPointerException e) { System.out.println("without case null: NullPointerException"); }
|
||||||
|
System.out.println("with case null: " + withCaseNull(null));
|
||||||
|
System.out.println("case null, default: " + combined(null));
|
||||||
|
System.out.println("instanceof with null: " + (null instanceof String));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
public class RecordPatterns {
|
||||||
|
record Point(int x, int y) {}
|
||||||
|
record Line(Point from, Point to) {}
|
||||||
|
record Box<T>(T content) {}
|
||||||
|
|
||||||
|
// Deconstruct a record: the components are bound to x and y.
|
||||||
|
static String where(Object o) {
|
||||||
|
if (o instanceof Point(int x, int y)) {
|
||||||
|
return "point at " + x + "," + y;
|
||||||
|
}
|
||||||
|
return "not a point";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns nest: take a Line apart into its four coordinates in one step.
|
||||||
|
static int length1(Line line) {
|
||||||
|
if (line instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
|
||||||
|
return Math.abs(x2 - x1) + Math.abs(y2 - y1);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A nested pattern can also test the component's type.
|
||||||
|
static String box(Box<Object> b) {
|
||||||
|
return switch (b) {
|
||||||
|
case Box<Object>(String s) -> "box of string " + s;
|
||||||
|
case Box<Object>(Integer i) -> "box of int " + i;
|
||||||
|
case Box<Object>(var other) -> "box of " + other;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(where(new Point(3, 4)));
|
||||||
|
System.out.println(where("nope"));
|
||||||
|
System.out.println(length1(new Line(new Point(0, 0), new Point(3, 4))));
|
||||||
|
System.out.println(box(new Box<>("hi")));
|
||||||
|
System.out.println(box(new Box<>(7)));
|
||||||
|
System.out.println(box(new Box<>(2.5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
public class SealedExhaustive {
|
||||||
|
sealed interface Shape permits Circle, Square, Rect {}
|
||||||
|
record Circle(double r) implements Shape {}
|
||||||
|
record Square(double side) implements Shape {}
|
||||||
|
record Rect(double w, double h) implements Shape {}
|
||||||
|
|
||||||
|
// No default: the compiler checks that every permitted subtype is covered.
|
||||||
|
static double area(Shape s) {
|
||||||
|
return switch (s) {
|
||||||
|
case Circle(double r) -> Math.PI * r * r;
|
||||||
|
case Square(double side) -> side * side;
|
||||||
|
case Rect(double w, double h) -> w * h;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
for (Shape s : new Shape[]{new Circle(1), new Square(2), new Rect(2, 3)}) {
|
||||||
|
System.out.printf("%s -> %.2f%n", s, area(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
public class TypePatterns {
|
||||||
|
// The old way: test, cast, then use.
|
||||||
|
static String oldStyle(Object o) {
|
||||||
|
if (o instanceof String) {
|
||||||
|
String s = (String) o;
|
||||||
|
return "string of length " + s.length();
|
||||||
|
}
|
||||||
|
return "something else";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pattern way: test and bind in one step.
|
||||||
|
static String newStyle(Object o) {
|
||||||
|
if (o instanceof String s) {
|
||||||
|
return "string of length " + s.length();
|
||||||
|
}
|
||||||
|
return "something else";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The binding variable stays in scope wherever the test is known to have succeeded.
|
||||||
|
static int lengthOrMinusOne(Object o) {
|
||||||
|
if (!(o instanceof String s)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return s.length(); // s is in scope here: we only get here if the test succeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
// A switch can test many types in a row.
|
||||||
|
static String describe(Object o) {
|
||||||
|
return switch (o) {
|
||||||
|
case Integer i -> "int " + (i + 1);
|
||||||
|
case String s -> "string " + s.toUpperCase();
|
||||||
|
case int[] arr -> "int array of " + arr.length;
|
||||||
|
default -> "other " + o.getClass().getSimpleName();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(oldStyle("hello") + " / " + newStyle("hello"));
|
||||||
|
System.out.println(lengthOrMinusOne("abc") + " " + lengthOrMinusOne(42));
|
||||||
|
System.out.println(describe(41));
|
||||||
|
System.out.println(describe("java"));
|
||||||
|
System.out.println(describe(new int[]{1, 2, 3}));
|
||||||
|
System.out.println(describe(3.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
public class Unnamed {
|
||||||
|
record Point(int x, int y) {}
|
||||||
|
sealed interface Shape permits Circle, Square {}
|
||||||
|
record Circle(double r) implements Shape {}
|
||||||
|
record Square(double side) implements Shape {}
|
||||||
|
|
||||||
|
static String kind(Shape s) {
|
||||||
|
return switch (s) {
|
||||||
|
case Circle _ -> "round"; // the type matters, the value does not
|
||||||
|
case Square _ -> "angular";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static int xOnly(Object o) {
|
||||||
|
if (o instanceof Point(int x, _)) return x; // do not care about y
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(kind(new Circle(1)) + " " + kind(new Square(1)));
|
||||||
|
System.out.println(xOnly(new Point(5, 9)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user