This document summarizes changes to the Java programming language from JDK 9 to JDK 16, including new features and modules. Some key points: - Java has moved to a six-month release cycle, delivering more features faster than before. - Modules were introduced in JDK 9 to improve modularity. Modules group related code and dependencies. - Incubator modules and preview features allow testing of non-final APIs before inclusion in the Java SE platform. - Local variable type inference using 'var' was added in JDK 10 for simpler declaration of local variables when types can be inferred. - Modules, the module system, and tools like jlink and jdeps help manage dependencies
Incubator Modules 3 • Definedby JEP 11 • Non-final APIs and non-final tools ‒ Deliver to developers to solicit feedback ‒ Can result in changes or even removal ‒ First example: HTTP/2 API (Introduced in JDK 9, final in JDK 11)
4.
Preview Features 4 • Definedby JEP 12 • New feature of the Java language, JVM or Java SE APIs ‒ Fully specified, fully implemented but not permanent ‒ Solicit developer real-world use and experience ‒ May lead to becoming a permanent feature in future release • Must be explicitly enabled ‒ javac --release 14 --enable-preview ... ‒ java --enable-preview ... • Preview APIs ‒ May be required for a preview language feature ‒ Part of the Java SE API (java or javax namespace)
Why Do WeNeed Modularity in Java? 7 • JDK 1.0 ‒ 122 library classes for simplifying application development • JDK 8.0 ‒ Over 4,500 public classes in the rt.jar file • JDK 9 ‒ 27 Java SE modules, 48 JDK modules • Two sides to modularity: 1. JDK core libraries 2. Application code
8.
Module Fundamentals 8 • Moduleis a grouping of code ‒ For Java this is a collection of packages • Modules can contain other things ‒ Native code ‒ Resources ‒ Configuration data com.azul.zoop com.azul.zoop.alpha.Name com.azul.zoop.alpha.Position com.azul.zoop.beta.Animal com.azul.zoop.beta.Reptile com.azul.zoop.theta.Zoo com.azul.zoop.theta.Lake
Accessibility • For apackage to be visible ‒ The package must be exported by the containing module ‒ The containing module must be read by the using module • Public types from those packages can then be used com.azul.zoop com.azul.zapp reads
Application Execution • -pis the abbreviation of --module-path $ java –p mods –m com.azul.zapp/com.azul.zapp.Main Azul application initialised! module name main class
25.
Packaging With ModularJAR Files mods/module-info.class mods/com/azul/zapp/Main.class $ jar --create --file=mylib/zapp.jar --main-class=com.azul.zapp.Main -C mods . module-info.class com/azul/zapp/Main.class zapp.jar
The Implications Ofjlink • "Write once, run anywhere" Long term Java slogan, mainly held true • jlink generated runtime may not include all Java SE modules ‒ But is still a conforming Java implementation ‒ To conform to the specification, the runtime: Must include the java.base module If other modules are included, all transitive module dependencies must also be included Defined as a closed implementation
Typical Application (JDK9) jar jar jar module java.base module java.desktop module java.datatransfer module java.xml jar jar jar jar jar jar jar jar jar Unnamed module
Automatic Modules 38 • Realmodules • Simply place unmodified jar file on module path ‒ Rather than classpath • No changes to JAR file • Module name derived from JAR file name • Exports all its packages ‒ No selectivity • Automatically requires all modules on the module path
Migration Guidance FromOracle "Clean applications that just depend on java.se should just work"
43.
Migrating Applications toJPMS 43 • Initially, leave everything on the classpath • Anything on the classpath is in the unnamed module ‒ All packages are exported ‒ The unnamed module depends on all modules • Migrate to modules as required ‒ Try automatic modules ‒ Move existing jar files from classpath to modulepath
Reversing Encapsulation • Bigkill switch overrides encapsulation ‒ From the unnamed module (i.e. classpath) ‒ Allows unlimited refective access to named modules Not from named modules • Warning messages written to error stream • Useful to understand use of --add-exports and --add-opens when migrating to named modules
Java Storage Basics 50 •Fields (instance and static) ‒ Part of an object, exist for the lifetime of an object ‒ Initialisation guaranteed by compiler • Local variables ‒ Scoped to region of code (method, statement, block) ‒ Initialisation not guaranteed ‒ Cheap when allocated on the stack ‒ Name and use is often more important than the type
51.
Local Variable TypeInference • Why? ‒ JavaScript has it so it must be good ‒ Streams hide intermediate types ‒ Why not expand this elsewhere? ‒ var can make code more concise Without sacrificing readability List l = names.stream() // Stream<String> .filter(s -> s.length() > 0) // Stream<String> .map(s -> getRecord(s)) // Stream<Record> .collect(toList()); // ArrayList<Record>
52.
Simple Uses ofLocal Variables ArrayList<String> nameList = new ArrayList<String>(); List<String> userList = new ArrayList<>(); Stream<String> stream = userList.stream(); for (String name : userList) { ... } for (int i = 0; i < 10; i++) { ... }
53.
Simple Uses ofLocal Variable Type Inference var userList = new ArrayList<String>(); // Infers ArrayList<String> var stream = userList.stream(); // Infers Stream<String> for (var name : userList) { // Infers String ... } for (var i = 0; i < 10; i++) { // Infers int ... }
54.
Simple Uses ofvar • Clearer try-with-resources try (InputStream inputStream = socket.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8); BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { // Use bufferedReader }
55.
Simple Uses ofvar • Clearer try-with-resources try (var inputStream = socket.getInputStream(); var inputStreamReader = new inputStreamReader(inputStream, UTF_8); var bufferedReader = new BufferedReader(inputStreamReader)) { // Use bufferedReader }
56.
More Typing WithLess Typing • Java is still statically typed var name = "Simon"; // Infers String, so name is String name = "Dylan"; // Still String name = 42; // Not a String error: incompatible types: int cannot be converted to String
57.
Final and Non-Final 57 •var simply indicates the compiler infers the type • Use of var does not make a variable final • Still need final • No shortcut for final var (like val) ‒ How often do you make local-variables final? var name = "Simon"; name = "Dylan"; // name is not final final var name = "Simon";
58.
Action At ADistance var l = new ArrayList<String>(); var s = l.stream(); // Lots of lines of complex code var n = l.get(0); // Err, what is n?
59.
Not Everything CanBe Inferred int[] firstSixPrimes = {2, 3, 5, 7, 11, 13}; var firstSixPrimes = {2, 3, 5, 7, 11, 13}; error: cannot infer type for local variable firstSixPrimes var firstSixPrimes = {2, 3, 5, 7, 11, 13}; ^ (array initializer needs an explicit target-type) var firstSixPrimes = new int[]{2, 3, 5, 7, 11, 13}; var firstTwoPrimes = new Integer[]{2, 3};
60.
Types Required OnBoth Sides var list = null; error: cannot infer type for local variable list var list = null; ^ (variable initializer is 'null')
61.
Types Required OnBoth Sides var list; list = new ArrayList<String>(); error: cannot infer type for local variable list var list; ^ (cannot use 'var' on variable without initializer)
62.
No Multiple VariableDeclaration 62 var a, b = 1; error: 'var' is not allowed in a compound declaration
63.
Literals with var(Good) • Original ‒ boolean ready = true; ‒ char ch = 'x'; ‒ long sum = 0L; ‒ String label = "Foo";
64.
Literals with var(Good) • With var ‒ var ready = true; ‒ var ch = 'x'; ‒ var sum = 0L; ‒ var label = "Foo";
65.
Literals with var(Dangerous) • Original ‒ byte flags = 0; ‒ short mask = 0x7fff; ‒ long base = 10;
66.
Literals with var(Dangerous) • Original ‒ var flags = 0; ‒ var mask = 0x7fff; ‒ var base = 10; All these cases will infer int
67.
Beware of MultipleType Inference var itemQueue = new PriorityQueue<>(); Diamond operator, primary type inference Secondary type inference itemQueue infered as PriorityQueue<Object>(); PriorityQueue<Integer> itemQueue = new PriorityQueue<>();
68.
Programming To AnInterface • Common to use interface rather than concrete type • Using var always infers concrete type • Polymorphism still works ‒ArrayList<String> is still a List ‒You can use myList wherever a List is required List<String> myList = new ArrayList<>(); var myList = new ArrayList<String>();
69.
Lambdas and var Predicate<String>blankLine = s -> s.isBlank(); var blankLine = s -> s.isBlank(); error: cannot infer type for local variable blankLine var blankLine = s -> s.isBlank(); ^ (lambda expression needs an explicit target-type)
70.
Intersection Types <T extendsCloseable & Iterable<E>> T getData() { // Return something that implements both // Closeable and Iterable<E> } Valid Java syntax
71.
Intersection Types • Theproblem <E> Optional<E> firstMatch(Predicate<? super E> condition) { XXX elements = getData(); try (elements) { return StreamSupport.stream(elements.spliterator(), false) .filter(condition) .findAny(); } } elements must implement Closeable elements must implement Iterable<E> XXX is Closeable & Iterable<E>, which won't compile
72.
Intersection Types • Thesolution: var <E> Optional<E> firstMatch(Predicate<? super E> condition) { var elements = getData(); try (elements) {` return StreamSupport.stream(elements.spliterator(), false) .filter(condition) .findAny(); } } Compiler infers a valid type that is impossible to express with Java syntax
73.
Inner Classes 73 • Thiscode won't compile Object fruit = new Object() { String name = "apple"; String inFrench() { return "pomme"; } }; System.out.println("Fruit = " + fruit.name); System.out.println("In French = " + fruit.inFrench());
74.
Inner Classes 74 • Usingvar this will compile and run ‒ The key is non-denotable types var fruit = new Object() { String name = "apple"; String inFrench() { return "pomme"; } }; System.out.println("Fruit = " + fruit.name); System.out.println("In French = " + fruit.inFrench());
75.
var: Reserved Type(Not Keyword) var var = new Var(); public class var { public var(String x) { ... } } public class Var { public Var(String x) { ... } }
Puzzler 1 • 0x12345678fits in an int ‒ Using var will infer an int, not a long • Passing a mask < 0 will give different results ‒ Using long, mask < 0 returns true ‒ Using var (infer int), mask < 0 returns false
79.
Puzzler 2 static List<String>create1(boolean foo, boolean bar) { List<String> list = new ArrayList<>(); if (foo) list.add("foo"); if (bar) list.add("bar"); return list; }
80.
Puzzler 2 • Shouldwe use var? Yes? No? static List<String> create1(boolean foo, boolean bar) { var list = new ArrayList<>(); if (foo) list.add("foo"); if (bar) list.add("bar"); return list; }
81.
Puzzler 2 • Remember,"Beware of multiple type inference" ‒ var list = new ArrayList<>(); ‒ Diamond operator infers Object error: incompatible types: ArrayList<Object> cannot be converted to List<String> return list;
82.
Puzzler 3 • Gooduse of var? Yes? No? static List<Integer> listRemoval() { List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 5, 7, 9)); var v = valueToRemove(); list.remove(v); return list; } // Separate class, package or module (in a galaxy far, far away) static Integer valueToRemove() { return 3; }
83.
Puzzler 3 • Newintern arrives and changes code... • Unexpected change in behaviour static int valueToRemove() { return 3; } List.remove(int) // Remove element at given index List.remove(Object) // Remove first instance of object {1,3,7,9} // Using var with changed valueToRemove {1,5,7,9} // Using var with unchanged valueToRemove
84.
Guidelines For Useof var 84 1. Reading code is more important that writing it 2. Code should be clear from local reasoning 3. Code readability shouldn't depend on an IDE 4. Choose variable names that provide useful information 5. Minimise the scope of local variables 6. Consider var when the initialiser provides sufficient information 7. Use var to break up chained or nested expressions 8. Take care when using var with generics 9. Take care when using var with literals
Missing Modules 90 • Remember,"Clean applications that only use java.se..." • The java.se.ee aggregator module removed in JDK 11 • Affected modules ‒ java.corba ‒ java.transaction ‒ java.activation ‒ java.xml.bind ‒ java.xml.ws ‒ java.xml.ws.annotation
91.
Resolving Missing Modules 91 •All modules (except CORBA) have standalone versions ‒ Maven central ‒ Relevant JSR RI • Deploy standalone version on the upgrade module path ‒ --upgrade-module-path <path> • Deploy standalone version on the classpath
Switch Expressions (Preview) 93 •Switch construct was a statement ‒ No concept of generating a result that could be assigned • Rather clunky syntax ‒ Every case statement needs to be separated ‒ Must remember break (default is to fall through) ‒ Scope of local variables is not intuitive
94.
Old-Style Switch Statement 94 intnumberOfLetters; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: numberOfLetters = 6; break; case TUESDAY: numberOfLetters = 7; break; case THURSDAY: case SATURDAY: numberOfLetters = 8; break; case WEDNESDAY: numberOfLetters = 9; break; default: throw new IllegalStateException("Huh?: " + day); };
95.
New-Style Switch Expression intnumberOfLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; default -> throw new IllegalStateException("Huh?: " + day); };
96.
New Old-Style SwitchExpression int numberOfLetters = switch (day) { case MONDAY: case FRIDAY: case SUNDAY: break 6; case TUESDAY break 7; case THURSDAY case SATURDAY break 8; case WEDNESDAY break 9; default: throw new IllegalStateException("Huh?: " + day); };
97.
New Old-Style SwitchExpression outside: for (day : dayList) int numberOfLetters = switch (day) { case MONDAY: case FRIDAY: case SUNDAY: break 6; case TUESDAY break 7; case THURSDAY case SATURDAY break 8; case WEDNESDAY break 9; default: continue outside; }; Illegal jump through switch expression
98.
Streams • New collector,teeing ‒ teeing(Collector, Collector, BiFunction) • Collect a stream using two collectors • Use a BiFunction to merge the two collections 98 Collector 1 Collector 2 BiFunction Stream Result
Text Blocks (Preview) StringwebPage = """ <html> <body> <p>My web page</p> </body> </html>"""; System.out.println(webPage); $ java WebPage <html> <body> <p>My web page</p> </body> </html> $ incidental white space
102.
Text Blocks (Preview) StringwebPage = """ <html> <body> <p>My web page</p> </body> </html> """; System.out.println(webPage); $ java WebPage <html> <body> <p>My web page</p> </body> </html> $ Additional blank line incidental white space Intentional indentation
103.
Switch Expression int numberOfLetters= switch (day) { case MONDAY: case FRIDAY: case SUNDAY: break 6; case TUESDAY break 7; case THURSDAY case SATURDAY break 8; case WEDNESDAY break 9; default: throw new IllegalStateException("Huh?: " + day); };
104.
Switch Expression int numberOfLetters= switch (day) { case MONDAY: case FRIDAY: case SUNDAY: yield 6; case TUESDAY yield 7; case THURSDAY case SATURDAY yield 8; case WEDNESDAY yield 9; default: throw new IllegalStateException("Huh?: " + day); };
Simple Java DataClass 106 class Point { private final double x; private final double y; public Point(double x, double y) { this.x = x; this.y = y; } public double x() { return x; } public double y() { return y; } }
107.
Records (Preview) 107 record Point(doublex, double y) { } record Range(int low, int high) { public Range { // Compact constructor if (low > high) throw new IllegalArgumentException("Bad values"); } }
108.
Record Additional Details 108 •Compact constructor can only throw unchecked exception ‒ Syntax does not allow for specifying a checked exception • Object methods equals(), hashCode() and toString() can be overridden • The base class of all records is java.lang.Record ‒ This is an example of a preview feature Java SE API ‒ Records cannot sub-class (but may implement interfaces) • Records do not follow the Java bean pattern ‒ x() not getX() in previous example • Instance fields cannot be added to a record ‒ Static fields can • Records can be generic
Pattern Matching instanceof(Preview) 110 if (obj instanceof String s) System.out.println(s.length()); else // Use of s not allowed here if (obj instanceof String s && s.length() > 0) System.out.println(s.length()); // Compiler error if (obj instanceof String s || s.length() > 0) System.out.println(s.length());
111.
Pattern Matching instanceof(Preview) 111 if (!(o instanceof String s && s.length() > 3) return; System.out.println(s.length());
112.
Pattern Matching instanceof 112 •Be careful of scope! class BadPattern { String s = "One"; void testMyObject(Object o) { if (o instanceof String s) { System.out.println(s); // Prints contents of o s = s + " Two"; // Modifies pattern variable } System.out.println(s); // Prints "One" } }
113.
Text Blocks • Secondpreview • Two new escape sequences String continuous = """This line will not contain a newline in the middle and solves the extra blank line issue """; String endSpace = """This line will not s lose the trailing spaces s""";
114.
Helpful NullPointerException 114 • Who'snever had an NullPointerException? • Enabled with -XX:+ShowCodeDetailsInExceptionMessages a.b.c.i = 99; Exception in thread "main" java.lang.NullPointerException at Prog.main(Prog.java:5) Exception in thread "main" java.lang.NullPointerException: Cannot read field "c" because "a.b" is null at Prog.main(Prog.java:5)
Java Inheritance 116 • Aclass (or interface) in Java can be sub-classed by any class ‒ Unless it is marked as final Shape Triangle Square Pentagon
117.
Sealed Classes (JEP360) 117 • Preview feature • Sealed classes allow control over which classes can sub-class a class ‒ Think of final as the ultimate sealed class • Although called sealed classes, this also applies to interfaces
118.
Sealed Classes (JEP360) 118 • Uses contextual keywords ‒ New idea replacing restricted identifiers and keywords ‒ sealed, permits and non-sealed • Classes must all be in the same package or module public sealed class Shape permits Triangle, Square, Pentagon { ... } Shape Triangle Square Pentagon Circle X
119.
Sealed Classes (JEP360) 119 • All sub-classes must have inheritance capabilities explicitly specified // Restrict sub-classes to defined set public sealed class Triangle permits Equilateral, Isosoles extends Shape { ... } // Prevent any further sub-classing public final class Square extends Shape { ... } // Allow any classes to sub-class this one (open) public non-sealed class Pentagon extends Shape { ... }
Hidden Classes (JEP371) 121 • JVM rather than language-level feature • Classes that cannot be used directly by the bytecodes of other classes • Several situations where bytecodes generated at runtime ‒ Use of invokedynamic bytecode ‒ Lambdas are a good example ‒ Mostly bound to static class (not for use elsewhere) ‒ Often only used for short time • Hidden classes can only be accessed via reflection ‒ Primarily intended for framework developers
122.
Records (Second Preview) 122 •Record fields are now (really) final ‒ Cannot be changed via reflection (will throw IllegalAccessException) • Native methods now explicitly prohibited ‒ Could introduce behaviour dependent on external state
123.
Records (Second Preview) 123 •Local records ‒ Like a local class ‒ Implicitly static List<Seller> findTopSellers(List<Seller> sellers, int month) { // Local record record Sales(Seller seller, double sales) {} return sellers.stream() .map(seller -> new Sales(seller, salesInMonth(seller, month))) .sorted((s1, s2) -> Double.compare(s2.sales(), s1.sales())) .map(Sales::seller) .collect(toList()); }
124.
Records (Second Preview) 124 •Records work with sealed classes (interfaces) public sealed interface Car permits RedCar, BlueCar { ... } public record RedCar(int w) implements Car { ... } public record BlueCar(long w, int c) implements Car { ... }
Pattern Matching instanceof(JEP 394) 126 • Now final, i.e. part of the Java SE specification • Two minor changes to previous iterations ‒ Pattern variables are no longer implicitly final ‒ Compile-time error to compare an expression of type S against a pattern of type T where S is a sub-type of T if (x instanceof Object o) … It will always succeed and so is pointless
127.
Records (JEP 395) 127 •Records now final, i.e., part of the Java SE specification • Record fields are (really) final ‒ Cannot be changed via reflection (will throw IllegalAccessException) • Native methods explicitly prohibited ‒ Could introduce behaviour dependent on external state • Inner classes can now declare explicit/implicit static members ‒ Allows an inner class to declare a member that is a Record class
128.
Add UNIX-Domain SocketChannels 128 • Add UNIX_AF socket channels ‒ Used for IPC on UNIX-based OSs and Windows • Better security and performance than TCP/IP loopback connections ‒ Behaviour is identical • No constructor, use factory methods var unix = UnixDomainSocketAddress.of("/tmp/foo");
129.
Streams mapMulti 129 • Similarto flatMap ‒ Each element on the input stream is mapped to zero or more elements on the output stream ‒ Difference is that a mapping can be applied at the same time ‒ Uses a BiConsumer • 1 to (0..1) example Stream.of("Java", "Python", "JavaScript", "C#", "Ruby") .mapMulti((str, consumer) -> { if (str.length() > 4) consumer.accept(str.length()); // lengths larger than 4 }) .forEach(i -> System.out.print(i + " ")); // 6 10
Vector API (JEP338) 132 • Incubator module (not part of the Java SE specification) • API to express vector computations ‒ Compile at runtime to optimal hardware instructions ‒ Deliver superior performance to equivalent scalar operations • Ideally, this would not be necessary ‒ Compiler should identify where vector operations can be used
133.
Vector API (JEP338) 133 void scalarComputation(float[] a, float[] b, float[] c) { for (int i = 0; i < a.length; i++) c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; } static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256; void vectorComputation(float[] a, float[] b, float[] c) { for (int i = 0; i < a.length; i += SPECIES.length()) { var m = SPECIES.indexInRange(i, a.length); var va = FloatVector.fromArray(SPECIES, a, i, m); var vb = FloatVector.fromArray(SPECIES, b, i, m); var vc = va.mul(va). add(vb.mul(vb)). neg(); vc.intoArray(c, i, m); } }
134.
Foreign-Memory Access API(JEP 393) 134 • Introduced in JDK 14, now third incubator iteration • API for safe and efficient access to memory outside of the Java heap • MemorySegment ‒ Models a contiguous area of memory • MemoryAddress ‒ Models an individual memory address (on or off heap) • MemoryLayout ‒ Programmatic description of a MemorySegment try (MemorySegment segment = MemorySegment.allocateNative(100)) { for (int i = 0; i < 25; i++) MemoryAccess.setIntAtOffset(segment, i * 4, i); }
135.
Foreign-Memory Access API(JEP 393) 135 • Example using MemoryLayout and VarHandle ‒ Simpler access of structured data SequenceLayout intArrayLayout = MemoryLayout.ofSequence(25, MemoryLayout.ofValueBits(32, ByteOrder.nativeOrder())); VarHandle indexedElementHandle = intArrayLayout.varHandle(int.class, PathElement.sequenceElement()); try (MemorySegment segment = MemorySegment.allocateNative(intArrayLayout)) { for (int i = 0; i < intArrayLayout.elementCount().getAsLong(); i++) indexedElementHandle.set(segment, (long) i, i); }
136.
Foreign Linker API(JEP 389): Incubator 136 • Provides statically-typed, pure-Java access to native code ‒ Works in conjunction with the Foreign Memory Access API ‒ Initially targeted at C native code. C++ should follow • More powerful when combined with Project Panama jextract command public static void main(String[] args) throws Throwable { var linker = CLinker.getInstance(); var lookup = LibraryLookup.ofDefault(); // get a native method handle for 'getpid' function var getpid = linker.downcallHandle(lookup.lookup("getpid").get(), MethodType.methodType(int.class), FunctionDescriptor.of(CLinker.C_INT)); System.out.println((int)getpid.invokeExact()); }
137.
Warnings for Value-BasedClasses 137 • Part of Project Valhalla, which adds value-types to Java ‒ Introduces the concept of primitive classes • Primitive wrapper classes (Integer, Float, etc.) designated value-based ‒ Constructors were deprecated in JDK 9 ‒ Now marked as for removal ‒ Attempting to synchronize on an instance of a value-based class will issue a warning
Zulu Enterprise 139 • Enhancedbuild of OpenJDK source code Fully TCK tested JDK 6, 7, 8, 11 and 13 TLS1.3, Flight Recorder backports for Zulu 8 • Wide platform support: Intel 64-bit Windows, Mac, Linux Intel 32-bit Windows and Linux • Real drop-in replacement for Oracle JDK Many enterprise customers No reports of any compatibility issues
140.
Zulu Extended Support 140 •Backporting of bug fixes and security patches from supported OpenJDK release • Zulu 8 supported until December 2030 • LTS releases have 9 years active + 2 years passive support • JDK 15 is a Medium Term Support release ‒ Bridge to next LTS release (JDK 17) ‒ Supported until 18 months after JDK 17 release
141.
Conclusions 141 • The six-monthrelease cycle is working well • Not all releases will have lots of new features • Use Zulu builds of OpenJDK if you want to deploy to production • Java continues to evolve!