diff --git a/README.md b/README.md index 332fb30..d1c1cf9 100644 --- a/README.md +++ b/README.md @@ -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 | | [`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 | +| [`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/`) diff --git a/patterns/README.md b/patterns/README.md new file mode 100644 index 0000000..2bb5ff6 --- /dev/null +++ b/patterns/README.md @@ -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. diff --git a/patterns/broken/Dominated.java b/patterns/broken/Dominated.java new file mode 100644 index 0000000..d62a26c --- /dev/null +++ b/patterns/broken/Dominated.java @@ -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")); } +} diff --git a/patterns/broken/GuardBeforeGeneral.java b/patterns/broken/GuardBeforeGeneral.java new file mode 100644 index 0000000..e553811 --- /dev/null +++ b/patterns/broken/GuardBeforeGeneral.java @@ -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("")); } +} diff --git a/patterns/broken/NotExhaustive.java b/patterns/broken/NotExhaustive.java new file mode 100644 index 0000000..268e706 --- /dev/null +++ b/patterns/broken/NotExhaustive.java @@ -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))); } +} diff --git a/patterns/broken/NotExhaustiveObject.java b/patterns/broken/NotExhaustiveObject.java new file mode 100644 index 0000000..dfb054e --- /dev/null +++ b/patterns/broken/NotExhaustiveObject.java @@ -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")); } +} diff --git a/patterns/output/01-type-patterns.txt b/patterns/output/01-type-patterns.txt new file mode 100644 index 0000000..e736be5 --- /dev/null +++ b/patterns/output/01-type-patterns.txt @@ -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 diff --git a/patterns/output/02-guards-and-dominance.txt b/patterns/output/02-guards-and-dominance.txt new file mode 100644 index 0000000..35316ae --- /dev/null +++ b/patterns/output/02-guards-and-dominance.txt @@ -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 diff --git a/patterns/output/03-record-patterns.txt b/patterns/output/03-record-patterns.txt new file mode 100644 index 0000000..ead4310 --- /dev/null +++ b/patterns/output/03-record-patterns.txt @@ -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 diff --git a/patterns/output/04-exhaustiveness.txt b/patterns/output/04-exhaustiveness.txt new file mode 100644 index 0000000..1b08012 --- /dev/null +++ b/patterns/output/04-exhaustiveness.txt @@ -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 diff --git a/patterns/output/05-separate-compilation.txt b/patterns/output/05-separate-compilation.txt new file mode 100644 index 0000000..f99eea0 --- /dev/null +++ b/patterns/output/05-separate-compilation.txt @@ -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 + diff --git a/patterns/output/06-null-and-unnamed.txt b/patterns/output/06-null-and-unnamed.txt new file mode 100644 index 0000000..58949a9 --- /dev/null +++ b/patterns/output/06-null-and-unnamed.txt @@ -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 diff --git a/patterns/output/07-primitive-patterns.txt b/patterns/output/07-primitive-patterns.txt new file mode 100644 index 0000000..220a799 --- /dev/null +++ b/patterns/output/07-primitive-patterns.txt @@ -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 diff --git a/patterns/output/08-preview-class-files.txt b/patterns/output/08-preview-class-files.txt new file mode 100644 index 0000000..86da886 --- /dev/null +++ b/patterns/output/08-preview-class-files.txt @@ -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' diff --git a/patterns/preview/BoxedToNarrower.java b/patterns/preview/BoxedToNarrower.java new file mode 100644 index 0000000..15f07b6 --- /dev/null +++ b/patterns/preview/BoxedToNarrower.java @@ -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 + } +} diff --git a/patterns/preview/PrimitiveEdges.java b/patterns/preview/PrimitiveEdges.java new file mode 100644 index 0000000..109cb7d --- /dev/null +++ b/patterns/preview/PrimitiveEdges.java @@ -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); + } +} diff --git a/patterns/preview/PrimitiveInRecord.java b/patterns/preview/PrimitiveInRecord.java new file mode 100644 index 0000000..a8ff24a --- /dev/null +++ b/patterns/preview/PrimitiveInRecord.java @@ -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")); + } +} diff --git a/patterns/preview/PrimitiveInstanceof.java b/patterns/preview/PrimitiveInstanceof.java new file mode 100644 index 0000000..145fd50 --- /dev/null +++ b/patterns/preview/PrimitiveInstanceof.java @@ -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); + } +} diff --git a/patterns/preview/PrimitiveSwitch.java b/patterns/preview/PrimitiveSwitch.java new file mode 100644 index 0000000..3b988b2 --- /dev/null +++ b/patterns/preview/PrimitiveSwitch.java @@ -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)); + } +} diff --git a/patterns/run.sh b/patterns/run.sh new file mode 100755 index 0000000..6668a49 --- /dev/null +++ b/patterns/run.sh @@ -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" diff --git a/patterns/sep/client/Client.java b/patterns/sep/client/Client.java new file mode 100644 index 0000000..27a217e --- /dev/null +++ b/patterns/sep/client/Client.java @@ -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))); + } +} diff --git a/patterns/sep/v1/Circle.java b/patterns/sep/v1/Circle.java new file mode 100644 index 0000000..54aa892 --- /dev/null +++ b/patterns/sep/v1/Circle.java @@ -0,0 +1 @@ +public record Circle(double r) implements Shape {} diff --git a/patterns/sep/v1/Shape.java b/patterns/sep/v1/Shape.java new file mode 100644 index 0000000..9ff8813 --- /dev/null +++ b/patterns/sep/v1/Shape.java @@ -0,0 +1 @@ +public sealed interface Shape permits Circle, Square {} diff --git a/patterns/sep/v1/Square.java b/patterns/sep/v1/Square.java new file mode 100644 index 0000000..4f65bd7 --- /dev/null +++ b/patterns/sep/v1/Square.java @@ -0,0 +1 @@ +public record Square(double side) implements Shape {} diff --git a/patterns/sep/v2/Circle.java b/patterns/sep/v2/Circle.java new file mode 100644 index 0000000..54aa892 --- /dev/null +++ b/patterns/sep/v2/Circle.java @@ -0,0 +1 @@ +public record Circle(double r) implements Shape {} diff --git a/patterns/sep/v2/Main.java b/patterns/sep/v2/Main.java new file mode 100644 index 0000000..7254c68 --- /dev/null +++ b/patterns/sep/v2/Main.java @@ -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))); + } +} diff --git a/patterns/sep/v2/Shape.java b/patterns/sep/v2/Shape.java new file mode 100644 index 0000000..0a87ca8 --- /dev/null +++ b/patterns/sep/v2/Shape.java @@ -0,0 +1 @@ +public sealed interface Shape permits Circle, Square, Triangle {} diff --git a/patterns/sep/v2/Square.java b/patterns/sep/v2/Square.java new file mode 100644 index 0000000..4f65bd7 --- /dev/null +++ b/patterns/sep/v2/Square.java @@ -0,0 +1 @@ +public record Square(double side) implements Shape {} diff --git a/patterns/sep/v2/Triangle.java b/patterns/sep/v2/Triangle.java new file mode 100644 index 0000000..7f1ddbd --- /dev/null +++ b/patterns/sep/v2/Triangle.java @@ -0,0 +1 @@ +public record Triangle(double base, double height) implements Shape {} diff --git a/patterns/src/Guards.java b/patterns/src/Guards.java new file mode 100644 index 0000000..d9fb88b --- /dev/null +++ b/patterns/src/Guards.java @@ -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)); + } +} diff --git a/patterns/src/MatchExceptionDemo.java b/patterns/src/MatchExceptionDemo.java new file mode 100644 index 0000000..32e48de --- /dev/null +++ b/patterns/src/MatchExceptionDemo.java @@ -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()); + } + } +} diff --git a/patterns/src/NullCase.java b/patterns/src/NullCase.java new file mode 100644 index 0000000..97fcb32 --- /dev/null +++ b/patterns/src/NullCase.java @@ -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)); + } +} diff --git a/patterns/src/RecordPatterns.java b/patterns/src/RecordPatterns.java new file mode 100644 index 0000000..31ec063 --- /dev/null +++ b/patterns/src/RecordPatterns.java @@ -0,0 +1,39 @@ +public class RecordPatterns { + record Point(int x, int y) {} + record Line(Point from, Point to) {} + record Box(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 b) { + return switch (b) { + case Box(String s) -> "box of string " + s; + case Box(Integer i) -> "box of int " + i; + case Box(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))); + } +} diff --git a/patterns/src/SealedExhaustive.java b/patterns/src/SealedExhaustive.java new file mode 100644 index 0000000..ed580fd --- /dev/null +++ b/patterns/src/SealedExhaustive.java @@ -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)); + } + } +} diff --git a/patterns/src/TypePatterns.java b/patterns/src/TypePatterns.java new file mode 100644 index 0000000..ab51a24 --- /dev/null +++ b/patterns/src/TypePatterns.java @@ -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)); + } +} diff --git a/patterns/src/Unnamed.java b/patterns/src/Unnamed.java new file mode 100644 index 0000000..c72282f --- /dev/null +++ b/patterns/src/Unnamed.java @@ -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))); + } +}