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:
@@ -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