Add java17-features module: runnable demos and captured output for the ankurm.com java17-features article
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")); }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+28
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"); }
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user