Add java17-features module: runnable demos and captured output for the ankurm.com java17-features article

This commit is contained in:
2026-10-20 17:13:58 +00:00
parent 8b9447a393
commit 5a076c826f
23 changed files with 261 additions and 0 deletions
+1
View File
@@ -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
+13
View File
@@ -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.
@@ -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) { }
}
@@ -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) { }
}
@@ -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")); }
}
+6
View File
@@ -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
+6
View File
@@ -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
@@ -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!
@@ -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
@@ -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
@@ -0,0 +1,2 @@
$ javac src/HelpfulNpeDemo.java && java HelpfulNpeDemo (17.0.20.1)
Cannot read field "city" because "u.address" is null
@@ -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 @<hash>
$ java --add-opens java.base/java.lang=ALL-UNNAMED EncapsulationDemo (17.0.20.1)
read String.value: byte[]
@@ -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
@@ -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
+28
View File
@@ -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 @<hash>#' > "$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
@@ -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());
}
}
}
+10
View File
@@ -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()); }
}
}
@@ -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"); }
}
}
+12
View File
@@ -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));
}
}
+21
View File
@@ -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<String> 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<String> 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");
}
}
+21
View File
@@ -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());
}
}
+20
View File
@@ -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());
}
}
+19
View File
@@ -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"));
}
}