diff --git a/README.md b/README.md index fdd8008..8494245 100644 --- a/README.md +++ b/README.md @@ -102,3 +102,4 @@ MIT licensed, see `LICENSE`. Modules for the Java 8 / 11 / 17 / 21 feature articles, one folder each, each with `src/`, `broken/`, `output/` and `run.sh`: - `java8-features/` — see its README - `java11-features/` — see its README +- `java17-features/` — see its README diff --git a/java17-features/README.md b/java17-features/README.md new file mode 100644 index 0000000..c5fc84f --- /dev/null +++ b/java17-features/README.md @@ -0,0 +1,13 @@ +# java17-features — Java 17 features with examples + +Companion code for the ankurm.com article **Java 17 Features with Examples: Records, Sealed Classes, Text Blocks, Switch Expressions**. The explanation lives in the article; this folder holds the runnable sources and the transcripts they produced. There is no `docs/` folder: the depth is inside the article itself. + +```bash +JDK11=/path/to/jdk11 JDK17=/path/to/jdk17 ./run.sh # regenerates output/*.txt +``` + +- `src/` compiles and runs unchanged on the JDK named in each output header. +- `broken/` fails to compile on purpose; the compiler's own message is committed in `output/`. +- `output/` is real captured output, regenerated by `run.sh`. + +Tested on: Temurin 17.0.20.1, plus 11.0.32.1 for the strong-encapsulation comparison. diff --git a/java17-features/broken/RecordFieldMutation.java b/java17-features/broken/RecordFieldMutation.java new file mode 100644 index 0000000..35e452d --- /dev/null +++ b/java17-features/broken/RecordFieldMutation.java @@ -0,0 +1,5 @@ +/** Fails to compile on purpose: record fields are final. */ +public class RecordFieldMutation { + record Point(int x, int y) { void move() { this.x = 10; } } + public static void main(String[] args) { } +} diff --git a/java17-features/broken/SealedViolation.java b/java17-features/broken/SealedViolation.java new file mode 100644 index 0000000..4a69b56 --- /dev/null +++ b/java17-features/broken/SealedViolation.java @@ -0,0 +1,7 @@ +/** Fails to compile on purpose: a class that is not in the permits list cannot extend a sealed type. */ +public class SealedViolation { + sealed interface Shape permits Circle {} + record Circle(double r) implements Shape {} + record Triangle(double b, double h) implements Shape {} + public static void main(String[] args) { } +} diff --git a/java17-features/broken/SwitchPatternOn17.java b/java17-features/broken/SwitchPatternOn17.java new file mode 100644 index 0000000..7271423 --- /dev/null +++ b/java17-features/broken/SwitchPatternOn17.java @@ -0,0 +1,5 @@ +/** Fails on 17 without --enable-preview: pattern matching for switch is only a preview feature in 17 (JEP 406). */ +public class SwitchPatternOn17 { + static String f(Object o) { return switch (o) { case String s -> "string"; default -> "other"; }; } + public static void main(String[] args) { System.out.println(f("x")); } +} diff --git a/java17-features/output/01-records.txt b/java17-features/output/01-records.txt new file mode 100644 index 0000000..fff0582 --- /dev/null +++ b/java17-features/output/01-records.txt @@ -0,0 +1,6 @@ +$ javac src/RecordsDemo.java && java RecordsDemo (17.0.20.1) +toString : Point[x=3, y=4] +equals : true hashCode equal: true +accessor : p.x() = 3, distance = 5.0 +compact ctor : negative coordinate: -1,2 +shallow : Team[name=core, members=[a, b]] <- the record's list changed after construction diff --git a/java17-features/output/02-sealed.txt b/java17-features/output/02-sealed.txt new file mode 100644 index 0000000..1572ff7 --- /dev/null +++ b/java17-features/output/02-sealed.txt @@ -0,0 +1,6 @@ +$ javac src/SealedDemo.java && java SealedDemo (17.0.20.1) +Circle[r=1.0] area = 3.142 +Square[side=2.0] area = 4.000 +Rect[w=2.0, h=3.0] area = 6.000 +permitted : [Circle, Square, Rect] +isSealed : true diff --git a/java17-features/output/03-text-blocks.txt b/java17-features/output/03-text-blocks.txt new file mode 100644 index 0000000..1abda94 --- /dev/null +++ b/java17-features/output/03-text-blocks.txt @@ -0,0 +1,8 @@ +$ javac src/TextBlocksDemo.java && java TextBlocksDemo (17.0.20.1) +{ + "name": "Ankur", + "city": "Mumbai" +} +lines : 4 +backslash-newline: joined on one line +formatted : Hello, reader! diff --git a/java17-features/output/04-switch-expressions.txt b/java17-features/output/04-switch-expressions.txt new file mode 100644 index 0000000..308ffb4 --- /dev/null +++ b/java17-features/output/04-switch-expressions.txt @@ -0,0 +1,5 @@ +$ javac src/SwitchExprDemo.java && java SwitchExprDemo (17.0.20.1) +MON -> 9 working hours +SAT -> 0 working hours +WED -> 8 working hours +old fall-through: one two diff --git a/java17-features/output/05-instanceof-pattern.txt b/java17-features/output/05-instanceof-pattern.txt new file mode 100644 index 0000000..8209ff5 --- /dev/null +++ b/java17-features/output/05-instanceof-pattern.txt @@ -0,0 +1,6 @@ +$ javac src/PatternInstanceofDemo.java && java PatternInstanceofDemo (17.0.20.1) +long string hello +other hi +int 42 +Stream.toList : [1, 2, 3] +toList() result is unmodifiable diff --git a/java17-features/output/06-helpful-npe.txt b/java17-features/output/06-helpful-npe.txt new file mode 100644 index 0000000..899b1d9 --- /dev/null +++ b/java17-features/output/06-helpful-npe.txt @@ -0,0 +1,2 @@ +$ javac src/HelpfulNpeDemo.java && java HelpfulNpeDemo (17.0.20.1) +Cannot read field "city" because "u.address" is null diff --git a/java17-features/output/07-strong-encapsulation.txt b/java17-features/output/07-strong-encapsulation.txt new file mode 100644 index 0000000..7ca37be --- /dev/null +++ b/java17-features/output/07-strong-encapsulation.txt @@ -0,0 +1,13 @@ +$ javac src/EncapsulationDemo.java && java EncapsulationDemo (11.0.32.1) +WARNING: An illegal reflective access operation has occurred +WARNING: Illegal reflective access by EncapsulationDemo (file:/…/) to field java.lang.String.value +WARNING: Please consider reporting this to the maintainers of EncapsulationDemo +WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations +WARNING: All illegal access operations will be denied in a future release +read String.value: byte[] + +$ javac src/EncapsulationDemo.java && java EncapsulationDemo (17.0.20.1) +java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @ + +$ java --add-opens java.base/java.lang=ALL-UNNAMED EncapsulationDemo (17.0.20.1) +read String.value: byte[] diff --git a/java17-features/output/08-random-generator.txt b/java17-features/output/08-random-generator.txt new file mode 100644 index 0000000..5e1bd44 --- /dev/null +++ b/java17-features/output/08-random-generator.txt @@ -0,0 +1,4 @@ +$ javac src/RandomDemo.java && java RandomDemo (17.0.20.1) +L64X128MixRandom nextInt(100) x3: 98 1 80 +ThreadLocalRandom implements RandomGenerator: true +algorithms >= 10: true diff --git a/java17-features/output/09-compile-errors.txt b/java17-features/output/09-compile-errors.txt new file mode 100644 index 0000000..7c93f93 --- /dev/null +++ b/java17-features/output/09-compile-errors.txt @@ -0,0 +1,18 @@ +$ javac broken/SealedViolation.java (17.0.20.1) +broken/SealedViolation.java:5: error: class is not allowed to extend sealed class: Shape (as it is not listed in its permits clause) + record Triangle(double b, double h) implements Shape {} + ^ +1 error + +$ javac broken/RecordFieldMutation.java (17.0.20.1) +broken/RecordFieldMutation.java:3: error: cannot assign a value to final variable x + record Point(int x, int y) { void move() { this.x = 10; } } + ^ +1 error + +$ javac broken/SwitchPatternOn17.java (17.0.20.1) +broken/SwitchPatternOn17.java:3: error: patterns in switch statements are a preview feature and are disabled by default. + static String f(Object o) { return switch (o) { case String s -> "string"; default -> "other"; }; } + ^ + (use --enable-preview to enable patterns in switch statements) +1 error diff --git a/java17-features/run.sh b/java17-features/run.sh new file mode 100755 index 0000000..a620786 --- /dev/null +++ b/java17-features/run.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Java 17 features: every demo compiled and run on a real JDK 17 (and 11 for the encapsulation comparison); regenerates output/*.txt +# JDK11=/path JDK17=/path ./run.sh +set -uo pipefail +unset JAVA_TOOL_OPTIONS +HERE="$(cd "$(dirname "$0")" && pwd)" +JDK11="${JDK11:-/opt/jdks/jdk11}"; JDK17="${JDK17:-/opt/jdks/jdk17}" +OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT +cd "$HERE" +ver() { "$1/bin/java" -version 2>&1 | sed -n 1p | sed 's/.*"\(.*\)".*/\1/'; } +cr() { local jdk="$1" src="$2" cls="$3" o; o="$(mktemp -d -p "$B")" + echo "\$ javac $src && java $cls ($(ver "$jdk"))" + "$jdk/bin/javac" ${JFLAGS:-} -encoding UTF-8 -d "$o" "$src" 2>&1 | grep -v '^Note:'; "$jdk/bin/java" -cp "$o" "$cls" 2>&1; } +co() { local jdk="$1" src="$2" o; o="$(mktemp -d -p "$B")" + echo "\$ javac $src ($(ver "$jdk"))"; local r; r="$("$jdk/bin/javac" -encoding UTF-8 -d "$o" "$src" 2>&1)"; if [ -n "$r" ]; then echo "$r"; else echo "(compiled cleanly)"; fi; } +cr "$JDK17" src/RecordsDemo.java RecordsDemo > "$OUT/01-records.txt" +cr "$JDK17" src/SealedDemo.java SealedDemo > "$OUT/02-sealed.txt" +cr "$JDK17" src/TextBlocksDemo.java TextBlocksDemo > "$OUT/03-text-blocks.txt" +cr "$JDK17" src/SwitchExprDemo.java SwitchExprDemo > "$OUT/04-switch-expressions.txt" +cr "$JDK17" src/PatternInstanceofDemo.java PatternInstanceofDemo > "$OUT/05-instanceof-pattern.txt" +JFLAGS=-g cr "$JDK17" src/HelpfulNpeDemo.java HelpfulNpeDemo > "$OUT/06-helpful-npe.txt" +{ cr "$JDK11" src/EncapsulationDemo.java EncapsulationDemo; echo; cr "$JDK17" src/EncapsulationDemo.java EncapsulationDemo + o="$(mktemp -d -p "$B")"; "$JDK17/bin/javac" -d "$o" src/EncapsulationDemo.java + echo; echo "\$ java --add-opens java.base/java.lang=ALL-UNNAMED EncapsulationDemo ($(ver "$JDK17"))" + "$JDK17/bin/java" --add-opens java.base/java.lang=ALL-UNNAMED -cp "$o" EncapsulationDemo 2>&1; } | sed -E 's#\(file:[^)]*\)#(file:/…/)#; s#unnamed module @[0-9a-f]+#unnamed module @#' > "$OUT/07-strong-encapsulation.txt" +cr "$JDK17" src/RandomDemo.java RandomDemo > "$OUT/08-random-generator.txt" +{ co "$JDK17" broken/SealedViolation.java; echo; co "$JDK17" broken/RecordFieldMutation.java; echo; co "$JDK17" broken/SwitchPatternOn17.java; } > "$OUT/09-compile-errors.txt" +exit 0 diff --git a/java17-features/src/EncapsulationDemo.java b/java17-features/src/EncapsulationDemo.java new file mode 100644 index 0000000..339edaf --- /dev/null +++ b/java17-features/src/EncapsulationDemo.java @@ -0,0 +1,15 @@ +import java.lang.reflect.Field; + +/** JEP 403: strong encapsulation of JDK internals. Same code, different outcome on 11 and 17. */ +public class EncapsulationDemo { + public static void main(String[] args) throws Exception { + Field f = String.class.getDeclaredField("value"); + try { + f.setAccessible(true); + Object v = f.get("abc"); + System.out.println("read String.value: " + v.getClass().getSimpleName()); + } catch (RuntimeException e) { + System.out.println(e.getClass().getName() + ": " + e.getMessage()); + } + } +} diff --git a/java17-features/src/HelpfulNpeDemo.java b/java17-features/src/HelpfulNpeDemo.java new file mode 100644 index 0000000..66f37ed --- /dev/null +++ b/java17-features/src/HelpfulNpeDemo.java @@ -0,0 +1,10 @@ +/** Helpful NullPointerException messages (JEP 358): on by default since 15. */ +public class HelpfulNpeDemo { + static class Address { String city; } + static class User { Address address; } + public static void main(String[] args) { + User u = new User(); + try { System.out.println(u.address.city.length()); } + catch (NullPointerException e) { System.out.println(e.getMessage()); } + } +} diff --git a/java17-features/src/PatternInstanceofDemo.java b/java17-features/src/PatternInstanceofDemo.java new file mode 100644 index 0000000..82a5f7f --- /dev/null +++ b/java17-features/src/PatternInstanceofDemo.java @@ -0,0 +1,16 @@ +/** Pattern matching for instanceof (final in 16, JEP 394) and Stream.toList() (16). */ +public class PatternInstanceofDemo { + static String describe(Object o) { + if (o instanceof String s && s.length() > 3) return "long string " + s; + if (o instanceof Integer i) return "int " + (i + 1); + return "other " + o; + } + public static void main(String[] args) { + System.out.println(describe("hello")); + System.out.println(describe("hi")); + System.out.println(describe(41)); + var list = java.util.stream.Stream.of(3, 1, 2).sorted().toList(); + System.out.println("Stream.toList : " + list); + try { list.add(4); } catch (UnsupportedOperationException e) { System.out.println("toList() result is unmodifiable"); } + } +} diff --git a/java17-features/src/RandomDemo.java b/java17-features/src/RandomDemo.java new file mode 100644 index 0000000..bd2c495 --- /dev/null +++ b/java17-features/src/RandomDemo.java @@ -0,0 +1,12 @@ +import java.util.random.RandomGenerator; +import java.util.random.RandomGeneratorFactory; + +/** JEP 356: RandomGenerator interface and named algorithms. Seeded, so the output is stable. */ +public class RandomDemo { + public static void main(String[] args) { + RandomGenerator g = RandomGeneratorFactory.of("L64X128MixRandom").create(42L); + System.out.println("L64X128MixRandom nextInt(100) x3: " + g.nextInt(100) + " " + g.nextInt(100) + " " + g.nextInt(100)); + System.out.println("ThreadLocalRandom implements RandomGenerator: " + (java.util.concurrent.ThreadLocalRandom.current() instanceof RandomGenerator)); + System.out.println("algorithms >= 10: " + (RandomGeneratorFactory.all().count() >= 10)); + } +} diff --git a/java17-features/src/RecordsDemo.java b/java17-features/src/RecordsDemo.java new file mode 100644 index 0000000..72a427e --- /dev/null +++ b/java17-features/src/RecordsDemo.java @@ -0,0 +1,21 @@ +/** Records (final in 16, JEP 395): what the compiler generates, compact constructors, and the shallow-immutability caveat. */ +public class RecordsDemo { + record Point(int x, int y) { + Point { if (x < 0 || y < 0) throw new IllegalArgumentException("negative coordinate: " + x + "," + y); } + double distance() { return Math.sqrt(x * x + y * y); } + } + record Team(String name, java.util.List members) {} + + public static void main(String[] args) { + Point p = new Point(3, 4); + System.out.println("toString : " + p); + System.out.println("equals : " + p.equals(new Point(3, 4)) + " hashCode equal: " + (p.hashCode() == new Point(3, 4).hashCode())); + System.out.println("accessor : p.x() = " + p.x() + ", distance = " + p.distance()); + try { new Point(-1, 2); } catch (IllegalArgumentException e) { System.out.println("compact ctor : " + e.getMessage()); } + + java.util.List list = new java.util.ArrayList<>(java.util.List.of("a")); + Team t = new Team("core", list); + list.add("b"); + System.out.println("shallow : " + t + " <- the record's list changed after construction"); + } +} diff --git a/java17-features/src/SealedDemo.java b/java17-features/src/SealedDemo.java new file mode 100644 index 0000000..d66cca4 --- /dev/null +++ b/java17-features/src/SealedDemo.java @@ -0,0 +1,21 @@ +/** Sealed classes (final in 17, JEP 409): the compiler knows every permitted subtype. */ +public class SealedDemo { + 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 {} + + // On 17 the switch must still be an if-chain: pattern matching for switch is only a preview here (JEP 406) + static double area(Shape s) { + if (s instanceof Circle c) return Math.PI * c.r() * c.r(); + if (s instanceof Square q) return q.side() * q.side(); + if (s instanceof Rect r) return r.w() * r.h(); + throw new AssertionError("unreachable for a sealed Shape"); + } + public static void main(String[] args) { + for (Shape s : new Shape[] { new Circle(1), new Square(2), new Rect(2, 3) }) + System.out.printf("%-26s area = %.3f%n", s, area(s)); + System.out.println("permitted : " + java.util.Arrays.stream(Shape.class.getPermittedSubclasses()).map(Class::getSimpleName).toList()); + System.out.println("isSealed : " + Shape.class.isSealed()); + } +} diff --git a/java17-features/src/SwitchExprDemo.java b/java17-features/src/SwitchExprDemo.java new file mode 100644 index 0000000..d113219 --- /dev/null +++ b/java17-features/src/SwitchExprDemo.java @@ -0,0 +1,20 @@ +/** Switch expressions (final in 14, JEP 361): arrow form, no fall-through, yield, and exhaustiveness on enums. */ +public class SwitchExprDemo { + enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } + public static void main(String[] args) { + for (Day d : new Day[] { Day.MON, Day.SAT, Day.WED }) { + int len = switch (d) { + case SAT, SUN -> 0; + case MON, TUE, WED, THU, FRI -> { + int hours = d == Day.MON ? 9 : 8; + yield hours; + } + }; + System.out.println(d + " -> " + len + " working hours"); + } + // old style: forgetting break falls through + int n = 1; StringBuilder sb = new StringBuilder(); + switch (n) { case 1: sb.append("one "); case 2: sb.append("two "); break; case 3: sb.append("three"); } + System.out.println("old fall-through: " + sb.toString().trim()); + } +} diff --git a/java17-features/src/TextBlocksDemo.java b/java17-features/src/TextBlocksDemo.java new file mode 100644 index 0000000..ad440b0 --- /dev/null +++ b/java17-features/src/TextBlocksDemo.java @@ -0,0 +1,19 @@ +/** Text blocks (final in 15, JEP 378): indentation stripping, trailing-space and escape rules. */ +public class TextBlocksDemo { + public static void main(String[] args) { + String json = """ + { + "name": "Ankur", + "city": "Mumbai" + } + """; + System.out.print(json); + System.out.println("lines : " + json.lines().count()); + String oneLine = """ + joined \ + on one line"""; + System.out.println("backslash-newline: " + oneLine); + System.out.println("formatted : " + """ + Hello, %s!""".formatted("reader")); + } +}