Java 10 and later New Features

Content:

  1. Version numbers
  2. Version 10 (18.3)
    1. Release
    2. Collections
    3. var declaration
    4. Other
  3. Version 11 LTS (18.9 LTS)
    1. Release
    2. Removed
    3. New HTTP client
    4. Other
    5. Support
  4. Version 12
    1. Release
    2. New switch
    3. Other
  5. Version 13
    1. Release
    2. New switch
    3. Text blocks
    4. Other
  6. Version 14
    1. Release
    2. Record
    3. Memory
    4. Other
  7. Version 15
    1. Release
    2. Sealed
    3. Removed
    4. Other
  8. Version 16
    1. Release
    2. Production ready
    3. Memory
    4. Function
    5. Vector
    6. Other
  9. Version 17 LTS
    1. Release
    2. Production ready
    3. Switch pattern
    4. Random number generator
    5. Removed
    6. Other
    7. License
  10. Version 18
    1. Release
    2. Finalizers
    3. Incubators
    4. Web server
  11. Version 19
    1. Release
    2. New previews and incubators
    3. Virtual threads
    4. Structured concurrency
  12. Version 20
    1. Release
    2. Updated previews and incubators
    3. Scoped value
  13. Version 21
    1. Release
    2. SequencedCollection
    3. Unnamed class
    4. Unnamed variable
    5. String template
    6. Out of preview
    7. Still in preview
    8. Other
  14. Version 22
    1. Release
    2. Statements before super
    3. Stream gatherer
    4. Class file API
    5. Preview and incubators

Version numbers:

Java version Official name Code name Release date
1.0 JDK 1.0 January 23rd 1996
1.1 JDK 1.1 February 19th 1997
1.2 J2SE 1.2 Playground December 8th 1998
1.3 J2SE 1.3 Kestrel May 8th 2000
1.4 J2SE 1.4 Merlin February 6th 2002
1.5/5 Java SE 5 Tiger September 30th 2004
1.6/6 Java SE 6 Mustang December 11th 2006
1.7/7 Java SE 7 Dolphin July 7th 2011
1.8/8 Java SE 8 March 18th 2014
1.9/9 Java SE 9 September 21st 2017
10/18.3 Java SE 10 March 20 2018
11/18.9 LTS Java SE 11 September 25th 2018
12 Java SE 12 March 19th 2019
13 Java SE 13 September 17th 2019
14 Java SE 14 March 17th 2020
15 Java SE 15 September 15th 2020
16 Java SE 16 March 16th 2021
17 LTS Java SE 17 September 14th 2021
18 Java SE 18 March 22nd 2022
19 Java SE 19 September 20th 2022
20 Java SE 20 March 21th 2023
21 Java SE 21 LTS September 19th 2023
22 Java SE 22 Expected March 19th 2024

Version 10 (18.3):

Release:

Java 10 was released March 20 2018.

It is the first release with the new release cycle and policy.

It is not a LTS release.

Collections:

Java 10 has some new ways to create unmodifiable copies of collections.

List/Map/Set got a copyOf method.

Collectors got toUmodifiableList/toUmodifiableMap/toUmodifiableSet.

Java 9 examples:

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Copy9 {
    public static void main(String[] args) {
        List<String> lst = List.of("A", "BB", "CCC");
        List<String> lst2 = Collections.unmodifiableList(new ArrayList<String>(lst));
        for(String s : lst2) {
            System.out.println(s);
        }
        Map<Integer,String> mp = Map.of(1, "A", 2, "BB", 3, "CCC");
        Map<Integer,String> mp2 = Collections.unmodifiableMap(new HashMap<Integer,String>(mp));
        System.out.println(mp2.get(2));
    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamCopy9 {
    public static void main(String[] args) {
        List<String> lst = List.of("A", "BB", "CCC");
        List<String> lst2 = Collections.unmodifiableList(lst.stream().collect(Collectors.toList()));
        for(String s : lst2) {
            System.out.println(s);
        }
        Map<Integer,String> mp = Map.of(1, "A", 2, "BB", 3, "CCC");
        Map<Integer,String> mp2 = Collections.unmodifiableMap(mp.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())));
        System.out.println(mp2.get(2));
    }
}

Java 10 examples:

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Copy10 {
    public static void main(String[] args) {
        List<String> lst = List.of("A", "BB", "CCC");
        List<String> lst2 = List.copyOf(lst);
        for(String s : lst2) {
            System.out.println(s);
        }
        Map<Integer,String> mp = Map.of(1, "A", 2, "BB", 3, "CCC");
        Map<Integer,String> mp2 = Map.copyOf(mp);
        System.out.println(mp2.get(2));
    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamCopy10 {
    public static void main(String[] args) {
        List<String> lst = List.of("A", "BB", "CCC");
        List<String> lst2 = lst.stream().collect(Collectors.toUnmodifiableList());
        for(String s : lst2) {
            System.out.println(s);
        }
        Map<Integer,String> mp = Map.of(1, "A", 2, "BB", 3, "CCC");
        Map<Integer,String> mp2 = mp.entrySet().stream().collect(Collectors.toUnmodifiableMap(e -> e.getKey(), e -> e.getValue()));
        System.out.println(mp2.get(2));
    }
}

OK feature but I do not think it is important.

var declaration:

Java 10 adds var declartion aka type inference similar to the one C# got in version 3 and as present in many newer JVM languages as well.

Example with traditional declarations:

public class Var9 {
    public static void main(String[] args) {
        int i1 = 123;
        int i2 = 456;
        System.out.println(i1 + i2);
        String s1 = "ABC";
        String s2 = "DEF";
        System.out.println(s1 + s2);
    }
}

Example with var declarations:

public class Var10 {
    public static void main(String[] args) {
        var i1 = 123;
        var i2 = 456;
        System.out.println(i1 + i2);
        var s1 = "ABC";
        var s2 = "DEF";
        System.out.println(s1 + s2);
    }
}

Note that this is exactly as type safe as before. The variable do get the same type as before and the exact same treatment. The only change is that the developer do not need to write the type name.

To prevent too much compatibility issues then var is implemented as a pseudo type or a context specific keyword, which means that variables being named var in existing code will continue to work.

It can be a nice feature. And I am sure that this feature will be used a lot. In fact I suspect that it will be misused. Overusing such a feature can actually reduce readability of code in my opinion, so it should only be used when the actual type is easy to deduce for humans.

Other:

javah utility is removed and instead the javac -h option introduced in Java 8 has to be used.

A new experimental JIT compiler has been added. It get activated by using:

java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler ...

So far the the new JIT compiler does seeem to be faster than the traditional - in fact it seems sligtly slower. But that may change as it matures.

Version 11 LTS (18.9 LTS):

Release:

Java 11 was released September 25th 2018.

It is the first LTS release with the new release cycle and policy.

Please read the license note on Oracle download page carefully. Oracle JDK is now only available under commercial terms (free for development, cost money for production). For free version for any usage (both development and production) the option is OpenJDK.

OpenJDK is available from many places including Eclipse Adoptium (previously AdoptOpenJDK).

Removed:

Java 11 is actually more about old features being removed than about new features being added.

Features removed:

I am sorry to see the web service client stuff go away. Yes - it is easy to get from the net (Apache Axis 2.x, Apache CXF or Metro). But I still think it is nice to have with Java SE.

CORBA support has been irrelevant for 15+ years and JTA is very much Java EE, so I don't see a problem with them being removed from Java SE.

I am sorry that JavaFX no longer being distributed with Java SE. JavaFX is actually a pretty nice GUI framework. See more here.

Metro kit with JAX-WS including wsimport utility can be downloaded from here.

Reference implementation of JAXB can be downloaded from here.

OpenJFX (JavaFX for Java 11+) can be downloaded here.

New HTTP client:

Java 11 got the new HTTP client that was introduced in uncubator mode in Java 9 (read more here).

Traditional Java code:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;

public class HttpTrad {
    public static void testGET() throws IOException {
        URL url = new URL("http://localhost:81/test.txt");
        URLConnection con = url.openConnection();
        con.connect();
        BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
    }
    public static void testPOST() throws IOException {
        URL url = new URL("http://localhost:81/test.php");
        URLConnection con = url.openConnection();
        con.setDoOutput(true);
        con.connect();
        PrintWriter pw = new PrintWriter(con.getOutputStream());
        pw.println("f=123");
        pw.close();
        BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
    }
    public static void main(String[] args) throws IOException {
        testGET();
        testPOST();
    }
}

This code is not that long, but it is not very clear what it is doing.

Apache HttpClient code:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.http.HttpResponse;

import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;

public class HttpApache {
    public static void testGET() throws IOException {
        HttpClient client = HttpClientBuilder.create().build();
        HttpGet request = new HttpGet("http://localhost:81/test.txt"); 
        HttpResponse response = client.execute(request);
        BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
    }
    public static void testPOST() throws IOException {
        HttpClient client = HttpClientBuilder.create().build();
        HttpPost request = new HttpPost("http://localhost:81/test.php"); 
        request.setEntity(new StringEntity("f=123", ContentType.APPLICATION_FORM_URLENCODED));
        HttpResponse response = client.execute(request);
        BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
    }
    public static void main(String[] args) throws IOException {
        testGET();
        testPOST();
    }
}

This code is much clearer.

New Java 11 code:

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.stream.Stream;

public class Http11 {
    public static void testGET() throws IOException, InterruptedException {
        HttpClient client = HttpClient.newBuilder().build();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create("http://localhost:81/test.txt")).GET().build();
        HttpResponse<Stream<String>> response = client.send(request, BodyHandlers.ofLines());
        response.body().forEach(line -> System.out.println(line));
    }
    public static void testPOST() throws IOException, InterruptedException {
        HttpClient client = HttpClient.newBuilder().build();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create("http://localhost:81/test.php")).POST(BodyPublishers.ofString("f=123")).header("Content-Type", "application/x-www-form-urlencoded").build();
        HttpResponse<Stream<String>> response = client.send(request, BodyHandlers.ofLines());
        response.body().forEach(line -> System.out.println(line));
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        testGET();
        testPOST();
    }
}

This code is short and concise. At least if one likes fluent style.

Other:

There are a few minor new features:

Support:

The support (patch availability) situation for Java is changing.

Java Support in the past Support going forward
Free Oracle JDK Some years N/A
(take OpenJDK)
Free OpenJDK Some years Non-LTS: 6 months
LTS: 6 months / 4 years
OpenJDK project itself will supposedly not offer long term support
but AdoptOpenJDK will - see https://adoptopenjdk.net/support.html
Azul Zulu (OpenJDK with support) Many years Non-MTS/LTS: 6 months
MTS: 3 years
LTS: 8 years
see https://www.azul.com/products/zulu-and-zulu-enterprise/zulu-enterprise-java-support-options/
Commercial Oracle JDK Many years Non-LTS: 6 months
LTS: 5 years/8 years/indefinite
see http://www.oracle.com/technetwork/java/javase/eol-135779.html
Other commercial vendors See their official EOL policy
(typical many years)
See their official EOL policy
(probably still many years for LTS versions)

Please check carefully yourself. The above is my best understanding at the time of writing, but I can have misunderstood what I read or the support policy can have been changed since then.

Version 12:

Release:

Java 12 was released March 19th 2019.

In my opinion Java 12 is a very small update.

New switch:

Java 12 introduces a new switch statement that seems inspired by Kotlin.

Traditional switch:

public class S11 {
    public enum Suits { CLUBS, SPADES, HEARTS, DIAMONDS }
    public static void test1(Suits s) {
        switch(s) {
            case CLUBS:
            case SPADES:
                System.out.println("Black");
                break;
            case HEARTS:
            case DIAMONDS:
                System.out.println("Red");
                break;
        }
    }
    public static void test2(Suits s) {
        int val = 0;
        switch(s) {
            case CLUBS:
                val = 1;
                break;
            case SPADES:
                val = 4;
                break;
            case HEARTS:
                val = 3;
                break;
            case DIAMONDS:
                val = 2;
                break;
        }
        System.out.println(val);
    }
    public static void main(String[] args) {
        test1(Suits.SPADES);
        test2(Suits.SPADES);
    }
}

New switch:

public class S12 {
    public enum Suits { CLUBS, SPADES, HEARTS, DIAMONDS }
    public static void test1(Suits s) {
        switch(s) {
            case CLUBS, SPADES -> System.out.println("Black");
            case HEARTS, DIAMONDS -> System.out.println("Red");
        }
    }
    public static void test2(Suits s) {
        int val = switch(s) {
            case CLUBS -> 1;
            case SPADES -> 4;
            case HEARTS -> 3;
            case DIAMONDS -> 2;
        };
        System.out.println(val);
    }
    public static void main(String[] args) {
        test1(Suits.SPADES);
        test2(Suits.SPADES);
    }
}

I think it is a nice feature, but it will not revolutionize the world.

Other:

There are also some changes to garbage collection.

Version 13:

Release:

Java 13 was released September 17th 2019.

In my opinion Java 13 is a very small update.

New switch:

Java 13 enhances the new switch introduced in Java 12.

Switch expression in Java 12 does not work with multiple statements:

public class SE12 {
    public enum Suits { CLUBS, SPADES, HEARTS, DIAMONDS }
    public static void test(Suits s) {
        int val = 0;
        switch(s) {
            case CLUBS:
                System.out.println("Black");
                val = 1;
                break;
            case SPADES:
                System.out.println("Black");
                val = 4;
                break;
            case HEARTS:
                System.out.println("Red");
                val = 3;
                break;
            case DIAMONDS:
                System.out.println("Red");
                val = 2;
                break;
        }
        System.out.println(val);
    }
    public static void main(String[] args) {
        test(Suits.SPADES);
    }
}

Java 13 adds yield keyword to support that:

public class SE13 {
    public enum Suits { CLUBS, SPADES, HEARTS, DIAMONDS }
    public static void test(Suits s) {
        int val = switch(s) {
            case CLUBS:
                System.out.println("Black");
                yield 1;
            case SPADES:
                System.out.println("Black");
                yield 4;
            case HEARTS:
                System.out.println("Red");
                yield 3;
            case DIAMONDS:
                System.out.println("Red");
                yield 2;
        };
        System.out.println(val);
    }
    public static void main(String[] args) {
        test(Suits.SPADES);
    }
}

This is preview only so one has to use:

javac --enable-preview --release 13 ...
java --enable-preview ...

to use this feature.

I think it is an OK feature, but not important.

Text blocks:

Java 13 add a syntax for multiline strings.

Traditionally one has had to explicit put line breaks in form of escape sequence into the string to get multiline:

public class TB12 {
    public static void main(String[] args) {
        String s = "A\r\n" +
                   "BB\r\n" +
                   "CCC\r\n";
        System.out.println(s);
    }
}

In Java 13 that can be done using triple quotes:

public class TB13 {
    public static void main(String[] args) {
        String s = """
A
BB
CCC
                   """;
        System.out.println(s);
    }
}

This is preview only so one has to use:

javac --enable-preview --release 13 ...
java --enable-preview ...

to use this feature.

I think it is a nice feature.

Other:

There are also some changes to socket implementation.

Version 14:

Release:

Java 14 was released March 17th 2020.

In my opinion Java 14 is a very small update.

Record:

Java 14 introduces a special family of types called record to be used for data classes.

Java 14 records are similar to Kotlin data class and scala case class.

The syntax is:

public record nameofrecord(type1 name1, type2 name2, ...) { };

The compiler generate a class with:

Traditional class:

public class Rec13 {
    public static class Data {
        private int iv;
        private String sv;
        public Data(int iv, String sv) {
            this.iv = iv;
            this.sv = sv;
        }
        public int getIv() {
            return iv;
        }
        public String getSv() {
            return sv;
        }
    }
    public static void main(String[] args) {
        Data o = new Data(123, "ABC");
        System.out.printf("iv=%d sv=%s\n", o.getIv(), o.getSv());
    }
}

Java 14 record:

public class Rec14 {
    public static record Data(int iv, String sv) { };
    public static void main(String[] args) {
        Data o = new Data(123, "ABC");
        System.out.printf("iv=%d sv=%s\n", o.iv(), o.sv());
    }
}

This is preview only so one has to use:

javac --enable-preview --release 14 ...
java --enable-preview ...

I am sure this feature will become a huge success. It saves a lot of boilerplate code.

Memory:

Java 14 introduces a new Foreign Memory API for direct memory access.

The features seems very inspired by C# 7.2 Span.

Demo:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;

import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemorySegment;

public class Mem14 {
    public static void test(MemorySegment ms) {
        System.out.println(ms.byteSize());
        VarHandle ih = MemoryHandles.varHandle(int.class, ByteOrder.LITTLE_ENDIAN);
        MemoryAddress ma = ms.baseAddress();
        for(int i = 0; i < 3; i++) {
            ih.set(ma.addOffset(4 * i), i + 1);
        }
        VarHandle ih2 = MemoryHandles.withStride(ih, 4);
        for(int i = 0; i < 3; i++) {
            System.out.println(ih2.get(ma, i));
        }
    }
    public static void main(String[] args) {
        try(MemorySegment ms = MemorySegment.allocateNative(12)) {
            test(ms);
        }
        byte[] b = new byte[12];
        try(MemorySegment ms = MemorySegment.ofArray(b)) {
            test(ms);
        }
        try(MemorySegment ms = MemorySegment.allocateNative(20)) {
            test(ms.asSlice(4, 12));
        }
    }
}

This is preview only so one has to use:

javac --enable-preview --release 14 ...
java --enable-preview ...

Honestly I do not see the point in this feature. If I want to do C style programming then I will use C!

Other:

Java 14 introduces an extension for instanceof to avoid an assignment and cast.

Before Java 14:

public class Patio13 {
    public static void test(Object o) {
        if(o instanceof String) {
            String s = (String)o;
            System.out.println("String = " + s);
        } else if(o instanceof Integer) {
            Integer i = (Integer)o;
            System.out.println("Integer = " + i);
        } else {
            System.out.println("Unknown class");
        }
    }
    public static void main(String[] args) {
        test("ABC");
        test(123);
    }
}

Java 14:

public class Patio14 {
    public static void test(Object o) {
        if(o instanceof String s) {
            System.out.println("String = " + s);
        } else if(o instanceof Integer i) {
            System.out.println("Integer = " + i);
        } else {
            System.out.println("Unknown class");
        }
    }
    public static void main(String[] args) {
        test("ABC");
        test(123);
    }
}

This is preview only so one has to use:

javac --enable-preview --release 14 ...
java --enable-preview ...

Nice but not important.

Java 14 ship with a tool to create installers with. Or to me more correct: Java ship with a wrapper that makes it easy to create an installer using the open source WIX tool.

Command:

jpackage --name nameofinstaller --input dirwithjarfiles --main-jar nameofjarfilewithmainclass

Given how few desktop apps that are written in Java, then the need for this must be minimal.

Switch expression from Java 12 and 13 are now out of preview and standard.

There are also some changes to garbage collection.

Version 15:

Release:

Java 15 was released September 15th 2020.

In my opinion Java 15 is a very small update.

Sealed:

Java 15 introduces a new way to restrict classes inheriting from a base class or implementing an interface to a specific list.

Syntax:

public abstract sealed class X permits A, B, C {
    ...
}

A, B and C should be final.

public sealed interface Y permits D, E, F {
    ...
}

D, E and F should be final.

Among other usages it allows one to test on all possible instance classes of an object. And even though that is really not good OOP, then it can be practical in the real world.

This is preview only so one has to use:

javac --enable-preview --release 15 ...
java --enable-preview ...

I think this is a pretty useful feature.

Removed:

Java 15 has removed the builtin JavaScript engine (Nashorn).

Other:

Text blocks from Java 13 are now out of preview and standard.

The ZGC and Shenandoah garbage collectors from Java 11 and 12 respectively are now out of preview and standard.

Version 16:

Release:

Java 16 was released March 16th 2021.

In my opinion Java 16 is another small update, but it may still be important because it opens up for some new stuff in the future.

Production ready:

Some features have been moved from preview to production ready:

Memory:

Java 14 introduced a new API for direct memory access.

This API has been changed in Java 16.

Java 14 code:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;

import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemorySegment;

public class Mem14 {
    public static void test(MemorySegment ms) {
        System.out.println(ms.byteSize());
        VarHandle ih = MemoryHandles.varHandle(int.class, ByteOrder.LITTLE_ENDIAN);
        MemoryAddress ma = ms.baseAddress();
        for(int i = 0; i < 3; i++) {
            ih.set(ma.addOffset(4 * i), i + 1);
        }
        VarHandle ih2 = MemoryHandles.withStride(ih, 4);
        for(int i = 0; i < 3; i++) {
            System.out.println(ih2.get(ma, i));
        }
    }
    public static void main(String[] args) {
        try(MemorySegment ms = MemorySegment.allocateNative(12)) {
            test(ms);
        }
        byte[] b = new byte[12];
        try(MemorySegment ms = MemorySegment.ofArray(b)) {
            test(ms);
        }
        try(MemorySegment ms = MemorySegment.allocateNative(20)) {
            test(ms.asSlice(4, 12));
        }
    }
}

Java 16 code:

import jdk.incubator.foreign.MemoryAccess;
import jdk.incubator.foreign.MemorySegment;

public class Mem16 {
    public static void test(MemorySegment ms) {
        System.out.println(ms.byteSize());
        for(int i = 0; i < 3; i++) {
            MemoryAccess.setIntAtIndex(ms, i, i + 1);
        }
        for(int i = 0; i < 3; i++) {
            System.out.println(MemoryAccess.getIntAtIndex(ms, i));
        }
    }
    public static void main(String[] args) {
        try(MemorySegment ms = MemorySegment.allocateNative(12)) {
            test(ms);
        }
        byte[] b = new byte[12];
        try(MemorySegment ms = MemorySegment.ofArray(b)) {
            test(ms);
        }
        try(MemorySegment ms = MemorySegment.allocateNative(20)) {
            test(ms.asSlice(4, 12));
        }
    }
}

Still in preview so use:

javac --enable-preview --release 16 ...
java --enable-preview ...

I am still not convinced about the relevance of this feature.

Function:

Since Java 1.0 then the way to call native code from Java has been JNI. The JNI API works but is a bit cumbersome to use.

Java 16 introduces a new Foreign Function API for calling native code.

This new API seems very much inspired by Python ctypes API.

There is a fundamental difference between JNI and the new call API:

Here comes an example.

C code:

#include <stdlib.h>
#include <string.h>

#ifdef WIN
#define EXTERNAL(t) __declspec(dllexport) t __stdcall
#else
#ifdef NIX
#define EXTERNAL(t) t
#else
#ifdef VMS
#define EXTERNAL(t) t
#else
#error "Platform not supported or not specified"
#endif
#endif
#endif

EXTERNAL(int) add(int a, int b)
{
    return a + b;
}

EXTERNAL(char *) dup(char *s)
{
    char *s2;
    s2 = malloc(2 * strlen(s) + 1);
    strcpy(s2, s);
    strcat(s2, s);
    return s2;
}

Java code:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.LibraryLookup;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;

public class C16 {
    public static void main(String[] args) throws Throwable {
        LibraryLookup dyn = LibraryLookup.ofLibrary("dyn");
        MethodHandle add = CLinker.getInstance().downcallHandle(dyn.lookup("add").get(),
                                                                MethodType.methodType(int.class, int.class, int.class),
                                                                FunctionDescriptor.of(CLinker.C_INT, CLinker.C_INT, CLinker.C_INT));
        MethodHandle dup = CLinker.getInstance().downcallHandle(dyn.lookup("dup").get(),
                                                                MethodType.methodType(MemoryAddress.class, MemoryAddress.class),
                                                                FunctionDescriptor.of(CLinker.C_POINTER, CLinker.C_POINTER));
        int a = 123;
        int b = 456;
        int c = (int)add.invoke(a, b);
        System.out.println(c);
        String s = "ABC";
        MemoryAddress tmp = (MemoryAddress)dup.invoke(CLinker.toCString(s).address());
        String s2 = CLinker.toJavaStringRestricted(tmp);
        CLinker.freeMemoryRestricted(tmp);
        System.out.println(s2);
    }
}

Test on Windows with GCC:

gcc -Wall -DWIN -shared -Wl,--kill-at dyn.c -o dyn.dll
javac --enable-preview --release 16 --add-modules jdk.incubator.foreign C16.java
java --enable-preview --add-modules jdk.incubator.foreign -Dforeign.restricted=permit C16

Note the -Dforeign.restricted=permit that is necessary to allow this feature.

I think this new API can become important.

Note that I do not think the introduction of this API means that the JNI design was a mistake. When Java was invented in 1996 the world was very different. Back then C and C++ were *the* languages and doing the glue code in those languages made sense. Today Java is a main language and C and C++ are getting rarer, so it makes sense to do the glue code in Java.

Vector:

Java 16 has added a vector API - a high level abstraction that makes it potentially possible to use vector capable hardware.

This feature may seem a bit unfinished, but it has huge potential. This feature may end up making Java a serious language for number crunching.

Other:

A lot of changes to how Java is developed, but those do not impact the Java developer.

Version 17 LTS:

Release:

Java 17 was released September 14th 2021.

It is a LTS release.

Java 16 to Java 17 is a small update, but Java 11 (previous LTS) to Java 17 is a significant update.

Production ready:

Some features have been moved from preview to production ready:

Switch pattern:

Java 17 introduces switch pattern.

Syntax:

switch(variable) {
    case Type castvariable -> ...;
    case Type castvariable -> ...;
    ...
}

Java 16 code:

public class Pat16 {
    private static void dump(Object o) {
        if(o instanceof Integer) {
            Integer oi = (Integer)o;
            System.out.println("Integer: " + oi);
        } else if(o instanceof Double) {
            Double od = (Double)o;
            System.out.println("Double: " + od);
        } else if(o instanceof String) {
            String os = (String)o;
            System.out.println("String: " + os);
        } else {
            System.out.println("Unknown: " + o);
        }
    }
    public static void main(String[] args) {
        dump(123);
        dump(123.456);
        dump("ABC");
    }
}

Java 17 code:

public class Pat17 {
    private static void dump(Object o) {
        switch(o) {
            case Integer oi -> System.out.println("Integer: " + oi);
            case Double od -> System.out.println("Double: " + od);
            case String os -> System.out.println("String: " + os);
            default -> System.out.println("Unknown: " + o);
        }
    }
    public static void main(String[] args) {
        dump(123);
        dump(123.456);
        dump("ABC");
    }
}

I think it is an useful feature that will be used.

Random number generator:

Java 17 introduces a new RNG (Random Number Generation) framework.

The new functionality is in package java.util.random and the key types are the interface RandomGenerator and the class RandomGeneratorFactory.

The new framework is compatible with the old java.util.Random class.

Java 16 code:

import java.util.Random;

public class RNG16 {
    public static void main(String[] args) {
        Random rng = new Random(123567);
        for(int i = 0; i < 10; i++) {
            System.out.println(rng.nextInt(100));
        }
    }
}

Java 17 code:

import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

public class RNG17 {
    public static void main(String[] args) {
        RandomGenerator rng = RandomGeneratorFactory.of("Random").create(123567);
        for(int i = 0; i < 10; i++) {
            System.out.println(rng.nextInt(100));
        }
        RandomGenerator superrng = RandomGeneratorFactory.of("L128X1024MixRandom").create(123567);
        for(int i = 0; i < 10; i++) {
            System.out.println(superrng.nextInt(100));
        }
    }
}

I think it is a relevant feature, but very few will use it.

Removed:

Features removed:

Regarding jaotc (Java AOT compiler) then apperently noone is using the feature so the JDK team does not want to support it.

Other:

Java 17 enhances the deserialization filter introduced in Java 9, so that it can now be applied context specific.

Java 17 makes strictfp default.

License:

OpenJDK is still available under GPL with classpath exception. But Oracle changed the license for Oracle JDK (which is practically the same product as OpenJDK).

Oracle JDK is now available under NFTC (No Fee Terms and Conditions). Which as I read it allows development, test and internal production usage from release time until 1 year after release of next LTS version.

If you plan on using Oracle JDK, then you should get the license checked by a lawyer. Or just go for OpenJDK that does not have any problems.

Version 18:

Release:

Java 18 was released March 22nd 2022.

It is not a LTS release.

In my opinion it is an extremely small update.

Finalizers:

Java 18 officially marks finalizers for future removal.

Many Java developers may not even know what finalizers are. Java finalizers are code called by the GC just before garbage collectiong objects. But note that Java finalizers are not like destructors in C++. C++ destructors are run deterministically in a known context. Java finalizers are run at an unknown time in an unknown context.

Example:

public class F {
    public F() {
        System.out.println("an F has been created");
    }
    protected void finalize() {
        System.out.println("the GC told the finalizer that an F is ready to be GC'ed");
    }
    public static void main(String[] args) {
        System.out.println("start");
        F o = new F();
        System.out.println("after F creation");
        o = null;
        System.out.println("after nulling");
        System.gc();
        System.out.println("after GC");
        System.out.println("done");
    }
}

Example output:

start
an F has been created
after F creation
after nulling
after GC
the GC told the finalizer that an F is ready to be GC'ed
done

All Java experts has recommended not relying on finalizers to cleanup since the late 1990's!

The try with resource mechanism introduced in Java 7 removed most of the use cases for finalizers.

Java 9 formally deprecated finalizers.

And now after more than two decades they are marked for future removal.

And they can even be disabled by a command line switch:

java --finalization=disabled ...

I assume that none of the readers are using finalizers.

Incubators:

Java 18 contain new previews of:

Example of the new foreign memory API.

Java 16 code:

import jdk.incubator.foreign.MemoryAccess;
import jdk.incubator.foreign.MemorySegment;

public class Mem16 {
    public static void test(MemorySegment ms) {
        System.out.println(ms.byteSize());
        for(int i = 0; i < 3; i++) {
            MemoryAccess.setIntAtIndex(ms, i, i + 1);
        }
        for(int i = 0; i < 3; i++) {
            System.out.println(MemoryAccess.getIntAtIndex(ms, i));
        }
    }
    public static void main(String[] args) {
        try(MemorySegment ms = MemorySegment.allocateNative(12)) {
            test(ms);
        }
        byte[] b = new byte[12];
        try(MemorySegment ms = MemorySegment.ofArray(b)) {
            test(ms);
        }
        try(MemorySegment ms = MemorySegment.allocateNative(20)) {
            test(ms.asSlice(4, 12));
        }
    }
}

Java 18 code:

import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import jdk.incubator.foreign.ValueLayout;

public class Mem18 {
    public static void test(MemorySegment ms) {
        System.out.println(ms.byteSize());
        for(int i = 0; i < 3; i++) {
            ms.setAtIndex(ValueLayout.JAVA_INT, i, i + 1);
        }
        for(int i = 0; i < 3; i++) {
            System.out.println(ms.getAtIndex(ValueLayout.JAVA_INT,i));
        }
    }
    public static void main(String[] args) {
        try (ResourceScope scope = ResourceScope.newSharedScope()) {
            MemorySegment ms = MemorySegment.allocateNative(12, scope);
            test(ms);
        }
        {
            byte[] b = new byte[12];
            MemorySegment ms = MemorySegment.ofArray(b);
            test(ms);
        }
        try (ResourceScope scope = ResourceScope.newSharedScope()) {
            MemorySegment ms = MemorySegment.allocateNative(20, scope);
            test(ms.asSlice(4, 12));
        }
    }
}

Example of the new foreign function API.

Java 16 code:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.LibraryLookup;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;

public class C16 {
    public static void main(String[] args) throws Throwable {
        LibraryLookup dyn = LibraryLookup.ofLibrary("dyn");
        MethodHandle add = CLinker.getInstance().downcallHandle(dyn.lookup("add").get(),
                                                                MethodType.methodType(int.class, int.class, int.class),
                                                                FunctionDescriptor.of(CLinker.C_INT, CLinker.C_INT, CLinker.C_INT));
        MethodHandle dup = CLinker.getInstance().downcallHandle(dyn.lookup("dup").get(),
                                                                MethodType.methodType(MemoryAddress.class, MemoryAddress.class),
                                                                FunctionDescriptor.of(CLinker.C_POINTER, CLinker.C_POINTER));
        int a = 123;
        int b = 456;
        int c = (int)add.invoke(a, b);
        System.out.println(c);
        String s = "ABC";
        MemoryAddress tmp = (MemoryAddress)dup.invoke(CLinker.toCString(s).address());
        String s2 = CLinker.toJavaStringRestricted(tmp);
        CLinker.freeMemoryRestricted(tmp);
        System.out.println(s2);
    }
}

Java 18 code:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import jdk.incubator.foreign.SymbolLookup;
import jdk.incubator.foreign.ValueLayout;

public class C18 {
    public static void main(String[] args) throws Throwable {
        System.loadLibrary("dyn");
        SymbolLookup sym = SymbolLookup.loaderLookup();
        CLinker linker = CLinker.systemCLinker();
        MethodHandle add = linker.downcallHandle(sym.lookup("add").get(),
                                                 FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));
        MethodHandle dup = linker.downcallHandle(sym.lookup("dup").get(),
                                                 FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS));
        int a = 123;
        int b = 456;
        int c = (int)add.invoke(a, b);
        System.out.println(c);
        String s = "ABC";
        MemorySegment itmp = MemorySegment.allocateNative(4, ResourceScope.newSharedScope());  
        itmp.setUtf8String(0, s);
        MemoryAddress tmp = (MemoryAddress)dup.invoke(itmp);
        MemorySegment otmp = MemorySegment.ofAddress(tmp, 7, ResourceScope.newSharedScope()); 
        String s2 = otmp.getUtf8String(0);
        System.out.println(s2);
    }
}

I don't think I will comment on the API until it is final. They keep changing it.

Web server:

Java 18 comes with a simple web server to serve files.

Start command:

jwebserver -p <port number> -d <absolute path of dir to server> 

It is obviously not intended for production usage.

I do not see any point in this addition.

Version 19:

Release:

Java 19 was released September 20th 2022.

It is not a LTS release.

It is yet another insignificant release. It is almost entirely preview and incubator functionality.

New previews and incubators:

Several features in preview or incubator status got enhancements:

No examples will be shown - I am waiting for final version of the features.

Virtual threads:

Work has been ongoing for this feature for many years under the name "Project Loom".

The basic concept is very simple: a new lightweigt thread where a large number of those can be mapped to a much smaller number of OS threads.

It solves the wellknown problem that creation of a huge number of threads (tens of thousands or hundreds of thousands or millions) kill the OS scheduler - all the CPU get spend administering the threads instead of running the threads.

Real threads:

public class T18 {
    public static void something() {
        try {
            Thread.sleep(1); // simulate work taking time
        } catch(InterruptedException ex) {
            ex.printStackTrace();
        }
    }
    private static final int NTASKS = 1 << 16;
    public static void testR(int nthreads) throws InterruptedException {
        long t1 = System.currentTimeMillis();
        Thread[] t = new Thread[nthreads];
        for(int i = 0; i < t.length; i++) {
            t[i] = new Thread(() -> { for(int j = 0; j < NTASKS / nthreads; j++) something(); });
            t[i].start();
        }
        for(int i = 0; i < t.length; i++) {
            t[i].join();
        }
        long t2 = System.currentTimeMillis();
        System.out.printf("Real threads (%d) : %d\n", nthreads, t2 - t1);
    }
    public static void main(String[] args) throws InterruptedException {
        for(int i = 2; i < 16; i++) {
            testR(1 << i);
        }
    }
}

Virtual threads:

public class T19 {
    public static void something() {
        try {
            Thread.sleep(1); // simulate work taking time
        } catch(InterruptedException ex) {
            ex.printStackTrace();
        }
    }
    private static final int NTASKS = 1 << 16;
    public static void testV(int nthreads) throws InterruptedException {
        long t1 = System.currentTimeMillis();
        Thread[] t = new Thread[nthreads];
        for(int i = 0; i < t.length; i++) {
            t[i] = Thread.startVirtualThread(() -> { for(int j = 0; j < NTASKS / nthreads; j++) something(); });
        }
        for(int i = 0; i < t.length; i++) {
            t[i].join();
        }
        long t2 = System.currentTimeMillis();
        System.out.printf("Virtual threads (%d) : %d\n", nthreads, t2 - t1);
    }
    public static void main(String[] args) throws InterruptedException {
        for(int i = 2; i < 16; i++) {
            testV(1 << i);
        }
    }
}

Since it is a preview then it must be compiled with:

javac --release 19 --enable-preview ...

and run with:

java --enable-preview ...

The output reveals the significance:

Real threads (4) : 16437
Real threads (8) : 8210
Real threads (16) : 4099
Real threads (32) : 2051
Real threads (64) : 1029
Real threads (128) : 524
Real threads (256) : 276
Real threads (512) : 176
Real threads (1024) : 186
Real threads (2048) : 251
Real threads (4096) : 423
Real threads (8192) : 742
Real threads (16384) : 1434
Real threads (32768) : 2904

Virtual threads (4) : 163870
Virtual threads (8) : 81921
Virtual threads (16) : 40960
Virtual threads (32) : 20480
Virtual threads (64) : 10240
Virtual threads (128) : 5120
Virtual threads (256) : 2560
Virtual threads (512) : 1280
Virtual threads (1024) : 110
Virtual threads (2048) : 110
Virtual threads (4096) : 110
Virtual threads (8192) : 120
Virtual threads (16384) : 120
Virtual threads (32768) : 140

Virtual threads is a very important change that will make multi-threading much simpler.

Structured concurrency:

This is a new mechanism to bundle some tasks so it is easier to wait for all of them to be done and easier to handle error sitiations.

Basicall one create a StructuredTaskScope and call its fork method to start tasks instead of submitting directly to a thread pool.

Traditional code:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class SC18 {
    public static int sum(int[] a, int start, int end) {
        int res = 0;
        for(int i = start; i <= end; i++) {
            res += a[i];
            try {
                Thread.sleep(10); // simulate work taking time
            } catch(InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        return res;
    }
    private static final int SIZ = 1000;
    private static final int NTASK = 10;
    private static final int TASKSIZ = SIZ / NTASK;
    private static <T> T getWithoutException(Future<T> f) {
        try {
            return f.get();
        } catch(InterruptedException ex) {
            throw new RuntimeException(ex);
        } catch(ExecutionException ex) {
            throw new RuntimeException(ex);
        }
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        final int[] a = new int[SIZ];
        for(int i = 0; i < a.length; i++) {
            a[i] = i + 1;
        }
        ExecutorService es = Executors.newFixedThreadPool(NTASK);
        List<Future<Integer>> flst = new ArrayList<Future<Integer>>();
        for(int i = 0; i < NTASK; i++) {
            final int start = i * TASKSIZ;
            final int end = start + TASKSIZ - 1;
            flst.add(es.submit(() -> sum(a, start, end)));
        }
        int sum = flst.stream().mapToInt(f -> getWithoutException(f)).sum();
        System.out.println(sum);
        es.shutdown();
    }
}

Java 19 code:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import jdk.incubator.concurrent.StructuredTaskScope;

public class SC19 {
    public static int sum(int[] a, int start, int end) {
        int res = 0;
        for(int i = start; i <= end; i++) {
            res += a[i];
            try {
                Thread.sleep(10); // simulate work taking time
            } catch(InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        return res;
    }
    private static final int SIZ = 1000;
    private static final int NTASK = 10;
    private static final int TASKSIZ = SIZ / NTASK;
    public static void main(String[] args) throws InterruptedException {
        final int[] a = new int[SIZ];
        for(int i = 0; i < a.length; i++) {
            a[i] = i + 1;
        }
        try(StructuredTaskScope scope = new StructuredTaskScope.ShutdownOnFailure()) {
            List<Future<Integer>> flst = new ArrayList<Future<Integer>>();
            for(int i = 0; i < NTASK; i++) {
                final int start = i * TASKSIZ;
                final int end = start + TASKSIZ - 1;
                flst.add(scope.fork(() -> sum(a, start, end)));
            }
            scope.join();
            int sum = flst.stream().mapToInt(f -> f.resultNow()).sum();
            System.out.println(sum);
        }
    }
}

Since it is a preview then it must be compiled with:

javac --release 19 --enable-preview --add-modules jdk.incubator.concurrent ...

and run with:

java --enable-preview --add-modules jdk.incubator.concurrent ...

Since it is an incubator then the package name will change in final version.

Elegant solution, but I doubt it will catch on.

Version 20:

Release:

Java 20 is expected to be released March 21st 2023.

It is not a LTS release.

It is yet another insignificant release. It is just preview and incubator functionality.

Updated previews and incubators:

Many previews and incubators have been updated:

Foreign memory example now look like:

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;

public class Mem20 {
    public static void test(MemorySegment ms) {
        System.out.println(ms.byteSize());
        for(int i = 0; i < 3; i++) {
            ms.setAtIndex(ValueLayout.JAVA_INT, i, i + 1);
        }
        for(int i = 0; i < 3; i++) {
            System.out.println(ms.getAtIndex(ValueLayout.JAVA_INT,i));
        }
    }
    public static void main(String[] args) {
        try (Arena a = Arena.openShared()) {
            MemorySegment ms = MemorySegment.allocateNative(12, a.scope());
            test(ms);
        }
        {
            int[] b = new int[3];
            MemorySegment ms = MemorySegment.ofArray(b);
            test(ms);
        }
        try (Arena a = Arena.openShared()) {
            MemorySegment ms = MemorySegment.allocateNative(20, a.scope());
            test(ms.asSlice(4, 12));
        }
    }
}

Foreign function example now look like:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import java.lang.foreign.Linker;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentScope;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;

public class C20 {
    public static void main(String[] args) throws Throwable {
        System.loadLibrary("dyn");
        SymbolLookup sym = SymbolLookup.loaderLookup();
        Linker linker = Linker.nativeLinker();
        MethodHandle add = linker.downcallHandle(sym.find("add").get(),
                                                 FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));
        MethodHandle dup = linker.downcallHandle(sym.find("dup").get(),
                                                 FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
        int a = 123;
        int b = 456;
        int c = (int)add.invoke(a, b);
        System.out.println(c);
        String s = "ABC";
        MemorySegment itmp = MemorySegment.allocateNative(4, SegmentScope.global());  
        itmp.setUtf8String(0, s);
        MemorySegment otmp = MemorySegment.ofAddress((long)dup.invoke(itmp), 7);
        String s2 = otmp.getUtf8String(0);
        System.out.println(s2);
    }
}

Scoped value:

Scoped value is a new feature in Java 20.

Scoped value is a weird construct - it is basically a global value that is not global. It is a global reference that can have different values in different contexts.

The basic usage is:

ScopedValue.where(globalvariable, somevalue).run(() -> codetorunwiththatvalue());

Example:

import jdk.incubator.concurrent.ScopedValue;
import jdk.incubator.concurrent.StructuredTaskScope;

public class SV20 {
    public static final ScopedValue<Integer> GLOBAL_V = ScopedValue.newInstance();
    public static void dump() {
        System.out.printf("%d : %d\n", Thread.currentThread().getId(), SV20.GLOBAL_V.get());
    }
    public static void test() {
        for(int i = 0; i < 3; i++) {
            try {
                Thread.sleep(10);
            } catch(InterruptedException ex) {
            }
            dump();
        }
    }
    public static void main(String[] args) throws Exception {
        try(final StructuredTaskScope<Integer> scope = new StructuredTaskScope<Integer>()) {
            scope.fork(() -> { ScopedValue.where(GLOBAL_V, 123).run(() -> test()); return 0; } );
            scope.fork(() -> { ScopedValue.where(GLOBAL_V, 456).run(() -> test()); return 0; } );
            scope.fork(() -> { ScopedValue.where(GLOBAL_V, 789).run(() -> test()); return 0; } );
            scope.join();
        }
    }
}
23 : 456
21 : 123
25 : 789
23 : 456
21 : 123
25 : 789
23 : 456
21 : 123
25 : 789

This is a very advanced feature. But honestly I think it should not be used - it makes code very hard to follow similar to traditional global values.

Scoped value is stil in preview, so it must be compiled and run with:


javac --enable-preview --release 20 --add-modules jdk.incubator.concurrent ...
java --enable-preview --add-modules jdk.incubator.concurrent ...

Version 21:

Release:

Java 21 was released September 19th 2023.

It is a LTS release.

It is a medium update compared to Java 20, but a significant update compared to Java 17 (last LTS).

SequencedCollection:

Java 21 adds a new interface implemented by all ordered collections to expose methods that are utilizing the fact that they are ordered.

Java 20 code:

import java.util.ArrayList;
import java.util.List;

public class SeqCol20 {
    public static void main(String[] args) {
        List<Integer> lst = new ArrayList(List.of(1, 2, 3));
        lst.remove(0);
        lst.add(4);
        System.out.printf("lst : %d .. %d\n", lst.get(0), lst.get(lst.size() - 1));
    }
}

Java 21 code:

import java.util.ArrayList;
import java.util.List;

public class SeqCol21 {
    public static void main(String[] args) {
        List<Integer> lst = new ArrayList(List.of(1, 2, 3));
        lst.removeFirst();
        lst.addLast(4);
        System.out.printf("lst : %d .. %d\n", lst.getFirst(), lst.getLast());
    }
}

I think it is a convenient but not important feature that will be used.

Unnamed class:

Java 21 allows one to omit explicit declaring a class for main.

The argument to main also becomes optional.

This is halfway what C# did in version 9.0.

Java 20 code:

public class Main20 {
    public static void main(String[] args) {
        System.out.println("main running");
    }
}

Java 21 code:

public static void main() {
    System.out.println("main running");
}

Unnamed class is stil in preview, so it must be compiled and run with:


javac --enable-preview --release 21 ...
java --enable-preview ...

I think it is an irrelevant feature that will not be used.

Unnamed variable:

Java 21 allows the use of underscore for variables not to be used, but where the assignment is significant.

This is similar to many other newer languages.

Java 20 code:

import java.util.ArrayList;
import java.util.List;

public class UnusedVar20 {
    public static void main(String[] args) {
        List<Integer> lst = new ArrayList(List.of(1, 2, 3));
        try {
            int dummy = lst.remove(0);
            for(int i = 0; i < 2; i++) {
                System.out.println(lst.get(i));
            }
        } catch(IndexOutOfBoundsException ex) {
            System.out.println("Bad index");
        }
    }
}

Java 21 code:

import java.util.ArrayList;
import java.util.List;

public class UnusedVar21 {
    public static void main(String[] args) {
        List<Integer> lst = new ArrayList(List.of(1, 2, 3));
        try {
            int _ = lst.remove(0);
            for(int i = 0; i < 2; i++) {
                System.out.println(lst.get(i));
            }
        } catch(IndexOutOfBoundsException _) {
            System.out.println("Bad index");
        }
    }
}

Unnamed variable is stil in preview, so it must be compiled and run with:

javac --enable-preview --release 21 ...
java --enable-preview ...

I think it is an unimportant feature that will not be used much.

String template:

Java 21 introduces string templates.

At the surface that may look similar to C# 10.0 and many newer languages, but the Java implementation is way more complex.

Java 20 code:

public class StrTmpl20 {
    public static void main(String[] args) {
        double v1 = 1.0;
        double v2 = 5.0;
        String s = String.format("%f - %f", v1, v2);
        System.out.println(s);
    }
}

First some Java 21 code that looks like string templates in other languages:

public class StrTmpl21 {
    public static void main(String[] args) {
        double v1 = 1.0;
        double v2 = 5.0;
        String s = STR."\{v1} - \{v2}";
        System.out.println(s);
    }
}

That looks simple although a bit weird. Prefix the string with STR. and put embedded variables in \{}.

But STR is only one template processor. Other template processors exist. The FMT template processor supports printf style formatting. One can create a template processor based on locale. And one can create a total custom template processor.

Example:

import java.util.FormatProcessor;
import java.util.Locale;

import static java.util.FormatProcessor.FMT;

public class StrTmpl21X {
    public static class MyFormatter implements StringTemplate.Processor<String, IllegalArgumentException> {
        @Override
        public String process(StringTemplate st) throws IllegalArgumentException {
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i < st.fragments().size() + st.values().size(); i++) {
                if((i % 2) == 0) {
                    String fragment = st.fragments().get(i / 2);
                    sb.append(fragment);
                } else {
                    Object value = st.values().get(i / 2);
                    if(value instanceof Double) {
                        sb.append(value.toString());
                    } else {
                        sb.append("ERROR");
                    }
                }
            }
            return sb.toString();
        }
    }
    public static void main(String[] args) {
        double v1 = 1.0;
        double v2 = 5.0;
        String s1 = STR."\{v1} - \{v2}";
        System.out.println(s1);
        String s2 = FMT."%f\{v1} - %f\{v2}";
        System.out.println(s2);
        String s3 = FMT."%.3f\{v1} - %.3f\{v2}";
        System.out.println(s3);
        FormatProcessor dkfmt = FormatProcessor.create(new Locale("da", "dk"));
        String s4 = dkfmt."%f\{v1} - %f\{v2}";
        System.out.println(s4);
        MyFormatter myfmt = new MyFormatter();  
        String s5 = myfmt."\{v1} - \{v2}";
        System.out.println(s5);
    }
}

Note that a StringTemplate.Processor does not need to return String and throw IllegalArgumentException - it can return any type and throw any exception type.

These advanced features makes it possible to create "safe" string templates.

String concatanation in SQL has been known to be problamtic since forever:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class SqlInjectA {
    private static void test(Connection con, String f2) throws SQLException {
        try(Statement stmt = con.createStatement()) {
            try(ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM t1 WHERE f2 = '" + f2 + "'")) {
                rs.next();
                System.out.println(rs.getInt(1));
            }
        }
    }
    public static void main(String[] args) throws Exception {
        try(Connection con = DriverManager.getConnection("jdbc:mysql://localhost/Test", "root", "hemmeligt")) {
            test(con, "BB");
            test(con, "BB' OR 'X'='X");
        }
    }
}

The solution is to use PreparedStatement:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class SqlInjectB {
    private static void test(Connection con, String f2) throws SQLException {
        try(PreparedStatement pstmt = con.prepareStatement("SELECT COUNT(*) FROM t1 WHERE f2 = ?")) {
            pstmt.setString(1, f2);
            try(ResultSet rs = pstmt.executeQuery()) {
                rs.next();
                System.out.println(rs.getInt(1));
            }
        }
    }
    public static void main(String[] args) throws Exception {
        try(Connection con = DriverManager.getConnection("jdbc:mysql://localhost/Test", "root", "hemmeligt")) {
            test(con, "BB");
            test(con, "BB' OR 'X'='X");
        }
    }
}

Obviously using a simple string template is just as bad as string concatanation:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class SqlInjectC {
    private static void test(Connection con, String f2) throws SQLException {
        try(Statement stmt = con.createStatement()) {
            try(ResultSet rs = stmt.executeQuery(STR."SELECT COUNT(*) FROM t1 WHERE f2 = '\{f2}'")) {
                rs.next();
                System.out.println(rs.getInt(1));
            }
        }
    }
    public static void main(String[] args) throws Exception {
        try(Connection con = DriverManager.getConnection("jdbc:mysql://localhost/Test", "root", "hemmeligt")) {
            test(con, "BB");
            test(con, "BB' OR 'X'='X");
        }
    }
}

But one can create a template processor that uses PreparedStatement:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.FormatProcessor;

public class SqlInjectD {
    public static class PreparedStatementFormatter implements StringTemplate.Processor<PreparedStatement, SQLException> {
        private Connection con;
        public PreparedStatementFormatter(Connection con) {
            this.con = con;
        }
        @Override
        public PreparedStatement process(StringTemplate st) throws SQLException {
            PreparedStatement pstmt = con.prepareStatement(String.join("?", st.fragments()));
            for(int i = 0; i < st.values().size(); i++) {
                switch(st.values().get(i)) {
                    case Integer iv -> pstmt.setInt(i + 1, iv);
                    case Double xv -> pstmt.setDouble(i + 1, xv);
                    case String sv -> pstmt.setString(i + 1, sv);
                    default -> throw new SQLException("Unsupported type");
                }
            }
            return pstmt;
        }
    }
    private static void test(Connection con, String f2) throws SQLException {
        PreparedStatementFormatter pstmtfmt = new PreparedStatementFormatter(con);
        try(PreparedStatement pstmt = pstmtfmt."SELECT COUNT(*) FROM t1 WHERE f2 = \{f2}") {
            try(ResultSet rs = pstmt.executeQuery()) {
                rs.next();
                System.out.println(rs.getInt(1));
            }
        }
    }
    public static void main(String[] args) throws Exception {
        try(Connection con = DriverManager.getConnection("jdbc:mysql://localhost/Test", "root", "hemmeligt")) {
            test(con, "BB");
            test(con, "BB' OR 'X'='X");
        }
    }
}

String templatee is still in preview, so it must be compiled and run with:

javac --enable-preview --release 21 ...
java --enable-preview ...

I believe that the STR template processor will be used similar to other languages. I doubt that the advanced features will be used much. And maybe the advanced features will be changes before becoming final - this is way too complex.

Out of preview:

Some stuff in preview are out of preview:

For usage see examples in sections for previous Java versions.

Still in preview:

Some stuff in preview are still in preview:

No new examples until they are finalized.

Other:

Java 15 introduced ZGC garbage collector, but Java 21 add henerational ZGC garbage collector making is useful in more contexs. ZGC is characterized by that GC pause is microseconds instead of traditional milliseconds.

32 bit Windows platform will be officially depreceated.

Version 22:

Release:

Java 22 is expected to be released March 19th 2024.

It is not a LTS release.

It is yet another release with very limited real new functionality.

Statements before super:

Java 22 allows statements before super call in constructors.

Java 21 code:

public class SBS21 {
    public static class P {
        public P(double x) {
            System.out.printf("x=%f\n", x);
        }
    }
    public static class C extends P {
        public C(int v1, int v2) {
            super(v1 * 1.0 / v2);
            System.out.printf("x=%f\n", v1 * 1.0 / v2);
        }
    }
    public static void main(String[] args) {
        new C(3, 2);
    }
}

Java 22 code:

public class SBS22 {
    public static class P {
        public P(double x) {
            System.out.printf("x=%f\n", x);
        }
    }
    public static class C extends P {
        public C(int v1, int v2) {
            double x = v1 * 1.0 / v2;
            super(x);
            System.out.printf("x=%f\n", x);
        }
    }
    public static void main(String[] args) {
        new C(3, 2);
    }
}

Statements before super is still in preview, so it must be compiled and run with:

javac --enable-preview --release 22 ...
java --enable-preview ...

Not important.

Stream gatherer:

Stream gatherer is a new feature that can be quite difficult to grasp.

My interpretation of the the purpose is that their purpose is to enable stream procesessing to be more context aware. Traditional stream processing operate on a single element at a time. Stream gatherers allow one to operate on a subset "a window" of elements.

Simple example that show out-of-the-box sliding window and fixed window:

import java.util.List;
import java.util.stream.Gatherers;

public class G22 {
    public static void main(String[] args) {
        List<Integer> lst = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
        System.out.println(lst);
        lst.stream().gather(Gatherers.windowSliding(3)).forEach(win -> System.out.printf("%d %d %d\n", win.get(0), win.get(1), win.get(2)));
        System.out.println(lst.stream().gather(Gatherers.windowSliding(3)).map(win -> (win.get(0) + win.get(1) + win.get(2)) / 3.0).toList());
        lst.stream().gather(Gatherers.windowFixed(3)).forEach(win -> System.out.printf("%d %d %d\n", win.get(0), win.get(1), win.get(2)));
        System.out.println(lst.stream().gather(Gatherers.windowFixed(3)).map(win -> (win.get(0) + win.get(1) + win.get(2)) / 3.0).toList());
    }
}

output:

[1, 2, 3, 4, 5, 6, 7, 8, 9]
1 2 3
2 3 4
3 4 5
4 5 6
5 6 7
6 7 8
7 8 9
[2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
1 2 3
4 5 6
7 8 9
[2.0, 5.0, 8.0]

Explanation:

Besides the standard gatherers then it is also possible to write a custom gatherer:

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Gatherer;

// WindowCounted<T>:
//   receive one T at a time
//   use an ArrayList,T> as state
//   pass one List<T> at a time to next level
// where number of elements are in the stream itself
public class WindowCounted<T extends Number> implements Gatherer <T, ArrayList<T>, List<T>> {
    private Optional<Integer> n = Optional.empty();
    @Override
    public Supplier<ArrayList<T>> initializer() {
        return () -> new ArrayList<T>();
    }
    @Override
    public Integrator<ArrayList<T>, T, List<T>> integrator() {
        return Gatherer.Integrator.ofGreedy((win, elm, targstm) -> {
            if(n.isEmpty()) {
                n = Optional.of(elm.intValue());
                return true;
            } else {
                win.add(elm);
                if(win.size() == n.get()) {
                    List<T> temp = new ArrayList<T>(win);
                    win.clear();
                    n = Optional.empty();
                    return targstm.push(temp);
                } else {
                    return true;
                }
            }
        });
    }
}
import java.util.List;
import java.util.stream.Gatherers;

public class GX22 {
    public static void main(String[] args) {
        List<Integer> lst = List.of(1, 1, 2, 2, 3, 3, 4, 5, 6, 4, 7, 8, 9, 10);
        System.out.println(lst);
        lst.stream().gather(new WindowCounted()).forEach(win -> System.out.println(win));
     }
}

output:

[1, 1, 2, 2, 3, 3, 4, 5, 6, 4, 7, 8, 9, 10]
[1]
[2, 3]
[4, 5, 6]
[7, 8, 9, 10]

Another one:

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Gatherer;

// Sampler<T>:
//   receive one T at a time
//   no state
//   pass one T at a time to next level
// where only every n element get passed
public class Sampler<T> implements Gatherer <T, T, T> {
    private int frequency;
    private int counter;
    public Sampler(int frequency) {
        this.frequency = frequency;
        this.counter = 0;
    }
    @Override
    public Integrator<T, T, T> integrator() {
        return Gatherer.Integrator.ofGreedy((win, elm, targstm) -> {
            counter++;
            if(counter % frequency == 0) {
                return targstm.push(elm);
            } else {
                return true;
            }
        });
    }
}
import java.util.List;
import java.util.stream.Gatherers;

public class GXX22 {
    public static void main(String[] args) {
        List<Integer> lst = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        System.out.println(lst);
        System.out.println(lst.stream().gather(new Sampler(2)).toList());
        List<String> lst2 = List.of("A", "B", "C", "D");
        System.out.println(lst2);
        System.out.println(lst2.stream().gather(new Sampler(2)).toList());
        
     }
}

Output:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[2, 4, 6, 8, 10]
[A, B, C, D]
[B, D]

Stream gatherer is still in preview, so it must be compiled and run with:

javac --enable-preview --release 22 ...
java --enable-preview ...

I think that it may take some time before people start using this feature, but that they eventually will and like it. In some cases it really result in much nicer code.

Class file API:

Java 22 offers a new class file API.

Basically it provides a supported API for generating Java byte code. But note that it generated byte code, so it is like "Java assembler" not like Javassist!

It is still in preview.

This feature will be extremely valuable for code generating libraries. But not so relevant for ordinary code.

I will not show an example because it is so specialized.

Preview and incubators:

Lot of features are still in preview/incubator:

Article history:

Version Date Description
1.0 March 4th 2018 Initial version
1.1 August 14th 2018 Update with Java 11 information
1.2 October 1st 2018 Update with Java 11 release info and license note
1.3 February 25th 2019 Update with Java 12 information
1.4 March 23rd 2019 Update with Java 12 release info
1.5 October 5th 2019 Update with Java 13 information
1.6 January 11th 2020 Update with Java 14 information
1.7 September 17th 2020 Update with Java 15 information
1.8 March 16th 2021 Update with Java 16 information
1.9 July 12th 2021 Update with Java 17 information
1.10 September 16th 2021 Update with Java 17 release info
1.11 February 9th 2022 Update with Java 18 information
1.12 September 18th 2022 Update with Java 19 information
1.13 March 5th 2023 Update with Java 20 information
1.14 July 1st 2023 Update with Java 21 information
1.15 January 14th 2024 Update with Java 22 information

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj