Home | Send Feedback

Small changes in Java 9 to 21

Published: 12. May 2020  •  Updated: 20. September 2023  •  java

In this blog post, I look at the more minor changes in Java 9 to 21. A smaller change is, for example, a new method added to an existing class or smaller language changes. I don't cover in this article major changes like Local-variable type inference (var) or the new HTTP client, or the new switch expression.

I also focus on useful features for my daily programming life as an application developer. I'm personally less interested in changes in low-level APIs like Reflection and low-level IO. I also omit in this blog post changes to the Java virtual machine (JVM), like new Garbage Collectors (GC).

I'm starting with Java 9, the release that started the new six-month release cycle.

I'm sure I missed a few of these smaller features. Send me a message if there is an interesting new feature that you use a lot. I will add it to this list.

Java 9 (September 2017)

The big thing in this release is the introduction of the module system (Project Jigsaw, JSR 376). This release also added JShell (JEP 222) and introduced the new java.util.concurrent.Flow class to support the Reactive Streams publish-subscribe pattern (JEP 266).


Private methods in interfaces

It is now possible to add private methods to interfaces. Like any other private method, they can't be abstract and overridden and can only be called from the same source file. The use case for private methods in interfaces is to share code between default methods, a feature introduced in Java 8.

public interface Writable {

  String content();

  default void writeToFile(File file) {
    write(file.toPath());
  }

  default void writeToPath(Path path) {
    write(path);
  }

  private void write(Path path) {
    try {
      Files.writeString(path, content());
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}

public class WritableImpl implements Writable {

  @Override
  public String content() {
    return "Hello World";
  }

  @Override
  public void writeToPath(Path path) {
    // can't access write(Path) 
    System.out.println(content());
  }
}

Diamond Operator refinement

Java 7 introduced the diamond operator (<>) to reduce verbosity.

// Java 6
List<Integer> result1 = new ArrayList<Integer>();

// Java 7
List<Integer> result2 = new ArrayList<>();

This feature did not work with anonymous inner classes.

With Java 9, this is now possible.

List<Integer> numbers = new ArrayList<>() {
  public boolean add(Integer e) {
    System.out.println("calling add");
    return super.add(e);
  }
};

numbers.add(1);

Compiling the code above with Java 7 or 8 results in this compiler error message.

error: cannot infer type arguments for ArrayList<E>
    List<Integer> numbers = new ArrayList<>() {
                                         ^
  reason: cannot use '<>' with anonymous inner classes

With Java 9+, this use case of the diamond operator is now supported.


try-with-resources statement refinement

Until Java 9, the try-with-resources statement requires new variables to be declared for being managed by the statement. In the following example, the method work() receives references to two AutoCloseable implementations, but the method has to create new variables inside try(...) so that the try-with-resources statement manages them.

class DbResource implements AutoCloseable {
  @Override
  public void close() {
    System.out.println("closing resource");
  }
}

private void doSomeWork() {
  work(getConnection(), getConnection());
}

private void work(DbResource dbResource1, DbResource dbResource2) {
  try (DbResource db1 = dbResource1; 
       DbResource db2 = dbResource2) {
    System.out.println("do some work");
  }   
  // do some work
  // closing resource
  // closing resource
}

private DbResource getConnection() {
  return new DbResource();
}

Java 9 introduced a refinement to the try-with-resources statement. The statement can now also manage resources referenced by final or effectively final variables without declaring new variables.

With this change the following syntax is valid and the try-with-resources statement auto closes the managed resources.

private void work(DbResource dbResource1, DbResource dbResource2) {
  try (dbResource1; dbResource2) {
    System.out.println("do some work");
  } 
}

You don't need to add the modifier final to a variable. The Java compiler checks if a variable is changed after initialization. If not, the variable is effectively final and can be used in a try-with-resources statement.

On the other hand, when a new value is assigned to a variable, it is no longer effectively final and can't be used in a try-with-resources statement.
The following code throws a compiler error: "Local variable dbResource1 defined in an enclosing scope must be final or effectively final".

    try (dbResource1; dbResource2) {

      dbResource1 = getConnection();
    }

Collection Factories

Java 9 introduced static of methods to the List, Set and Map interface. These methods return unmodifiable collections with the given arguments as elements. An unmodifiable collection cannot store null values, and calling mutator methods on such a collection throws UnsupportedOperationException.

List.of and Set.of are available as overloaded methods to take zero to nine arguments. Another overloaded method takes variable arguments (vararg), so these two methods work with any number of arguments.

Set<Integer> numbers = Set.of(1, 2, 3);
// [1, 2, 3]

List<String> cities = List.of("London", "Paris");
// [London, Paris]


numbers.add(4);
// java.lang.UnsupportedOperationException

List<Boolean> flags = List.of(false, true, null);
// java.lang.NullPointerException

It's important to note when given duplicate values to Set.of it throws IllegalArgumentException.

Set<Long> numbers = Set.of(1L, 2L, 3L, 3L);
// java.lang.IllegalArgumentException: duplicate element: 3

Maps store key/value pairs, and so the Map.of() expects keys and values in alternating order.

Map<String, String> countryCapital = Map.of("FR", "Paris", "GB", "London", "ES", "Madrid");
String capitalOfSpain = countryCapital.get("ES");
// "Madrid"

Keys and values have to be non-null. Passing null throws NullPointerException. Passing a duplicate key throws IllegalArgumentException.

Map<Integer, String> users = Map.of(1, null);
// java.lang.NullPointerException

Map<String, String> countryCapital = Map.of("FR", "Paris", "FR", "Paris", 
                                            "GB", "London", "ES", "Madrid");
// java.lang.IllegalArgumentException: duplicate key: FR    

The Map.of method is available as an overloaded method to take zero to nine key/value pairs. If an application needs to create a map with more than nine key/value pairs it has to use the Map.ofEntries(Map.Entry...) method that expects a variable number of Map.Entry instances as arguments.

Map<String, String> capitals = 
  Map.ofEntries(
      Map.entry("FR", "Paris"), Map.entry("GB", "London"), 
      Map.entry("ES", "Madrid"), Map.entry("IT", "Rome"),
      Map.entry("JP", "Tokyo"), Map.entry("IE", "Dublin"), 
      Map.entry("CH", "Bern"), Map.entry("DE", "Berlin"),
      Map.entry("AT", "Vienna"), Map.entry("PT", "Lisbon"), 
      Map.entry("LI", "Vaduz"), Map.entry("AD", "Andorra la Vella"));  

String capitalOfAustria = capitals.get("AT");
// "Vienna"

of creates unmodifiable collections, which means that elements can't be added, removed, or updated. But if elements are mutable and an element is modified, that change is reflected in the unmodifiable collections.

class User {
  private String name;
  User(String name) {
    this.name = name;
  }
  @Override
  public String toString() { return this.name; }
}

User userA = new User("Adam");
User userB = new User("Sarah");
List<User> users = List.of(userA, userB);
// [Adam, Sarah]


// changing name of userA    
userA.name = "John";
String firstUserName = users.get(0).name;
// John
// users contains: [John, Sarah] 

java.lang.Process

Java 9 added new methods to the classes java.lang.ProcessBuilder, java.lang.Process and added a new interface java.lang.ProcessHandle.

java.lang.ProcessHandle provides three static factory methods to get handles for all processes (allProcesses()), to get the current process handle (current()), and to get a handle for a specific process (of(long)).

ProcessHandle.allProcesses().forEach(ph -> {
  System.out.printf("PID: %d Info: %s\n", ph.pid(), ph.info());
});

long myPid = ProcessHandle.current().pid();

ProcessHandle.of(myPid).ifPresent(ph -> System.out.println(ph.info()));

With the java.lang.ProcessHandle an application can access the process id (pid()), can check for liveness (isAlive()), and can stop the process with destroy() and destroyForcibly(). parent() returns the handle of the parent process of this process, children() and descendants() return the process handles of all direct children resp. all descendants (children of children) of this process. A CompletableFuture available from onExit() can be used to wait for process termination.

info() returns an instance of ProcessHandle.Info which contains the following information:


The java.lang.Process class also got most of these methods. An application can call toHandle() to get the java.lang.ProcessHandle of the process.

The difference between java.lang.ProcessHandle and java.lang.Process is that the latter represents processes started by the current process and additionally provide access to the process input, output, and error stream.


The java.lang.ProcessBuilder got a new method startPipeline to create a pipeline of new processes that send the output of each process directly to the following process.

The following example creates a sample text file and then invokes the three commands.

cat sample | grep -v a | sort - r

These commands list the file, remove every word containing `a', and reverse sort the file.

    Path sampleFile = Paths.get("./sample.txt");
    Files.writeString(sampleFile, "Bat\nGoat\nApple\nDog\nFirst\nEat\nHide");
    File workingDir = Paths.get(".").toFile();
    ProcessBuilder cat = new ProcessBuilder()
        .command("cat", "sample.txt")
        .directory(workingDir);
    
    ProcessBuilder grep = new ProcessBuilder()
        .command("grep", "-v", "a")
        .directory(workingDir);
    
    ProcessBuilder sort = new ProcessBuilder()
        .command("sort", "-r")
        .directory(workingDir);
    
    List<Process> commands = ProcessBuilder.startPipeline(List.of(cat, grep, sort));

    Process last = commands.get(2);
    last.onExit().thenAccept(process -> {
      BufferedReader lineReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
      lineReader.lines().forEach(System.out::println);
    }).join();

    /*
    Hide 
    First
    Dog  
    Apple
     */    

java.util.Optional

Java 9 added three new methods to the java.util.Optional class.

ifPresentOrElse(Consumer,Runnable) performs the Consumer passed as first argument if the Optional has a value, otherwise performs the Runnable passed as second argument.
Similar to the ifPresent(Consumer) method but with one additional parameter for the no value case.

Optional<Boolean> flag = Optional.of(true);

flag.ifPresentOrElse(b -> System.out.printf("value is %b", b),
    () -> System.out.println("Optional has no value"));
// value is true

or(Supplier) returns the Optional if a value is present, otherwise returns an Optional produced by the Supplier.

Optional<String> city1 = Optional.of("London");
Optional<String> city2 = Optional.of("Paris");

Optional<String> nextTrip = city1.or(() -> city2);
// Optional[London]

Optional<String> city3 = Optional.ofNullable(null);
nextTrip = city3.or(() -> city2);
// Optional[Paris]

stream() returns a stream with one value when the Optional has a value, otherwise returns an empty stream.

  class User {
    private final String name;
    User(String name) {
      this.name = name;
    }
  }

  Optional<User> result = Optional.of(new User("Adam"));
  String name = result.stream().map(u -> u.name).collect(Collectors.joining());
  // "Adam"

  Optional<User> resultN = Optional.ofNullable(null);
  String nameN = resultN.stream().map(u -> u.name).collect(Collectors.joining());
  // ""

java.util.stream.Stream

The java.util.stream.Stream interface got four new methods

ofNullable(T) is a static method that returns a sequential stream with one element if the argument is non-null, otherwise returns an empty stream.

String input = "John";
String output = Stream.ofNullable(input).map(String::toUpperCase).findFirst().orElse("");
// "John"

input = null;
output = Stream.ofNullable(input).map(String::toUpperCase).findFirst().orElse("");
// ""

The Stream interface has a iterate(T, UnaryOperator) method that creates an infinite streams. The first argument is the initial element, and the second argument is a function applied to the previous element to return a new element.

Java 9 added a new overloaded iterate(T,Predicate,UnaryOperator) method that takes three arguments, the initial element (1st argument), the function that produces the new element (3rd argument) and a Predicate that determines when the stream must terminate (2nd argument).

//Infinite stream
Stream.iterate(1, i -> i += 1).limit(3).forEach(System.out::println);
// 1 2 3

//Java 9 iterate
Stream.iterate(1, i -> i < 4, i -> i += 1).forEach(System.out::println);
// 1 2 3

dropWhile(Predicate) drops all elements from the stream until the given Predicate returns false for the first time. Including this element, dropWhile lets all other elements pass.

takeWhile(Predicate) returns elements from the beginning of the stream until the Predicate returns false for the first time. At that point, takeWhile terminates the stream.

Stream.of(1, 2, 20, 3, 4, 15).dropWhile(n -> n < 10).forEach(System.out::println);
// 20 3 4 15

Stream.of(1, 2, 20, 3, 4, 15).takeWhile(n -> n < 10).forEach(System.out::println);
// 1 2

In the first example, we see that dropWhile removes 1 and 2 from the stream because they meet the criteria of the Predicate. The number 20 is the first element of the stream where the Predicate returns false, so dropWhile returns all elements starting with 20. Also, the elements are no longer tested against the Predicate from this point.

The second example with takeWhile only returns 1 and 2 because they meet the criteria of the Predicate. 20 is the first element where the Predicate returns false. takeWhile terminates the stream at this point and does not include 20 to the resulting stream.


Another example with dropWhile and takeWhile. Our task is to write code that reads a file and extracts the text between :body: and :footer:.

:header:
  some header data
:body:
  the body
  more content
:footer:
  footer information
Path input = Paths.get("./test.txt");
List<String> body = Files.lines(input)
          .dropWhile(line -> !line.strip().equals(":body:"))
          .skip(1)
          .takeWhile(line -> !line.strip().equals(":footer:"))
          .map(String::strip)
          .collect(Collectors.toUnmodifiableList());
// ["the body", "more content"]   

java.util.Arrays

Java 9 introduced equals, compare and mismatch to the java.util.Arrays class. These methods are available for each primitive data type long, int, short, char, byte, boolean, double, float, Object. And there is also a generic variant for custom classes. Each method is also overloaded to take from and to indexes.

equals

equals compares two arrays and returns true if both arrays contain the same elements in the same order.

int[] array1 = {1,2,3};
int[] array2 = {3,2,1};
int[] array3 = {1,2,3,4};

boolean isEqual = Arrays.equals(array1, array2);
// false

isEqual = Arrays.equals(array1, new int[]{1,2,3});
// true

isEqual = Arrays.equals(array1, array3);
// false

isEqual = Arrays.equals(array1, null);
// false

From and to indexes can be passed to the methods. Note that from index is inclusive and to index is exclusive.

int[] array1 = {1,2,3};
int[] array3 = {1,2,3,4};

boolean isEqual = Arrays.equals(array1, 0, 3, array3, 0, 3);
// true

When comparing custom objects, a Comparator needs to be passed as the third argument.

class User {
  private final String name;
  User(String name) {
    this.name = name;
  }
  String getName() { return this.name; }
}

User[] users1 = new User[]{new User("John"), new User("Sarah")};
User[] users2 = new User[]{new User("John"), new User("Sarah")};

boolean isEqual = Arrays.equals(users1, users2, Comparator.comparing(User::getName));
// true

compare

compare returns 0 if the two arrays contain the same elements in the same order. returns a value less than 0 if the first array is lexicographically less than the second array, and a value greater than 0 if the first array is lexicographically greater than the second array.

List<byte[]> numbers = new ArrayList<>();

numbers.add(new byte[]{9});
numbers.add(new byte[]{101});
numbers.add(new byte[]{1,2,3,4});
numbers.add(null);
numbers.add(new byte[]{7,7,7});
numbers.add(new byte[]{8,9});

numbers.sort((a, b) -> Arrays.compare(a, b));
/*
null
[1, 2, 3, 4]
[7, 7, 7]
[8, 9]
[9]
[101]
  */

For byte, short, int and long there is a compareUnsigned method available. Compares the numerical elements as unsigned. See Integer.compareUnsigned(int,int) for an example how this works internally.


Custom objects need to implement the Comparable interface.

Comparator<String> nullSafeStringComparator = Comparator
    .nullsFirst(String::compareToIgnoreCase);

class User implements Comparable<User> {
  private final String name;

  User(String name) {
    this.name = name;
  }

  String getName() {
    return this.name;
  }

  @Override
  public String toString() {
    return this.name;
  }

  @Override
  public int compareTo(User o) {
    return nullSafeStringComparator.compare(this.name, o.name);
  }
}

List<User[]> users = new ArrayList<>();

users.add(new User[] { new User("Walter"), new User("Sarah") });
users.add(new User[] { new User("Adam"), null, new User("Jane") });
users.add(new User[] { new User("Michael") });
users.add(null);

users.sort((a, b) -> Arrays.compare(a, b));

/*
null
[Adam, null, Jane]
[Michael]
[Walter, Sarah]
*/

mismatch

mismatch finds and returns the index of the first mismatch between two arrays. If there is no mismatch between the two arrays, the method returns -1.

long[] numbers1 = new long[] { 1, 2, 3, 4 };
long[] numbers2 = new long[] { 1, 2, 5 };
int mismatchIndex = Arrays.mismatch(numbers1, numbers2);
// 2

mismatchIndex = Arrays.mismatch(new short[] { 1, 2 }, new short[] { 1, 2 });
// -1

// from and to index
mismatchIndex = Arrays.mismatch(
    new float[] { 1.1f, 2.2f, 3.3f }, 1, 3,
    new float[] { 5.5f, 2.2f, 3.3f }, 1, 3);
// -1

A Comparator needs to be passed as the third argument when custom objects are used.

class User {
  private final String name;
  User(String name) {
    this.name = name;
  }
  String getName() { return this.name; }
}

User[] users1 = new User[]{new User("John"), new User("Sarah")};
User[] users2 = new User[]{new User("John"), new User("Anna")};

int mismatchIndex = Arrays.mismatch(users1, users2, Comparator.comparing(User::getName));
// 1

java.util.Objects

java.util.Objects got a bunch of new methods

requireNonNullElse(T, T) returns the first argument if it is non-null, otherwise returns the second argument. Note when the first and second arguments are null, the method throws NullPointerException.

String userName = "Adam";
String output = Objects.requireNonNullElse(userName, "");
// "Adam"

userName = null;
output = Objects.requireNonNullElse(userName, "");
// ""

userName = "Sara";
output = Objects.requireNonNullElse(userName, null);
// "Sara"

userName = null;
output = Objects.requireNonNullElse(userName, null);
// java.lang.NullPointerException: defaultObj

requireNonNullElseGet(T, Supplier) returns the first argument if it is non-null, like the method before. If the first argument is null, the method calls the Supplier function passed as the second argument and returns the value that this function produces. The method throws NullPointerException when the first argument is null, and either the Supplier is null, or the produced value of the Supplier is null.

String userName = "Adam";
String output = Objects.requireNonNullElseGet(userName, () -> "<default>");
// "Adam"

userName = null;
output = Objects.requireNonNullElseGet(userName, () -> "<default>");
// <default>

output = Objects.requireNonNullElseGet(userName, () -> null);
// java.lang.NullPointerException: supplier.get()

output = Objects.requireNonNullElseGet(userName, null);
// java.lang.NullPointerException: supplier

checkIndex(int, int) checks if the first argument is within 0 (inclusive) and the second argument (exclusive). The method returns the first argument if it is within the bounds, otherwise throws IndexOutOfBoundsException.

int index = 13;
int upperBound = 100;
index = Objects.checkIndex(index, upperBound);
// 13

index = Objects.checkIndex(-1, upperBound);
// java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 100

index = Objects.checkIndex(100, upperBound);
// java.lang.IndexOutOfBoundsException: Index 100 out of bounds for length 100

index = Objects.checkIndex(99, upperBound);
// 99

checkFromIndexSize(int, int, int) checks if the range from first argument (inclusive) to first argument + second argument (exclusive) is within 0 (inclusive) and the third argument (exclusive). The method returns the first argument if the range is within the bounds, otherwise throws IndexOutOfBoundsException.

int upperBound = 100;
int index = Objects.checkFromIndexSize(13, 20, upperBound);
// 13

index = Objects.checkFromIndexSize(-1, 20, upperBound);
// java.lang.IndexOutOfBoundsException: Range [-1, -1 + 20) out of bounds for length 100

index = Objects.checkFromIndexSize(100, 1, upperBound);
// java.lang.IndexOutOfBoundsException: Range [100, 100 + 1) out of bounds for length 100

index = Objects.checkFromIndexSize(90, 10, upperBound);
// 90

checkFromToIndex(int, int, int) checks if the range from first argument (inclusive) to second argument (exclusive) is within 0 (inclusive) and the third argument (exclusive). The method returns the first argument if the range is within the bounds. Otherwise, it throws IndexOutOfBoundsException.

int upperBound = 100;
int index = Objects.checkFromToIndex(13, 20, upperBound);
// 13

index = Objects.checkFromToIndex(-1, 20, upperBound);
// java.lang.IndexOutOfBoundsException: Range [-1, 20) out of bounds for length 100

index = Objects.checkFromToIndex(100, 101, upperBound);
// java.lang.IndexOutOfBoundsException: Range [100, 101) out of bounds for length 100

index = Objects.checkFromToIndex(90, 100, upperBound);
// 90

Java 10 (March 2018)

The significant visible change for developers is the introduction of the reserved type name var for local-variable type inference (JEP 286)

Unmodifiable copies of collections

Java 10 added the static copyOf() method to List, Set and Map. These methods take a collection as input and return an unmodifiable snapshot of that collection. An unmodifiable collection cannot store null values and calling mutator methods on such a collection always throws UnsupportedOperationException.

List<String> source = new ArrayList<>();
source.add("one");
source.add("two");

List<String> copy = List.copyOf(source);
// [one, two]

copy.add("three");
// java.lang.UnsupportedOperationException

Note that changing the source collection does not affect the copy.

Map<Integer, String> source = new HashMap<>();
source.put(1, "one");
source.put(2, "two");
    
Map<Integer, String> copy = Map.copyOf(source);

source.put(3, "three");
source.remove(1);
    
// source: {2=two, 3=three}
// copy: {2=two, 1=one}

If elements are mutable, and an element is modified, that change appears in the original collection and the copy.

class Counter {
    private int count;
    private Counter(int startCount) {
      this.count = startCount;
    }
    private void inc() { this.count++; }
    @Override
    public String toString() {
      return String.valueOf(this.count);
    }
}

List<Counter> source = new ArrayList<>();
Counter counter = new Counter(0);
source.add(counter);
    
List<Counter> copy = List.copyOf(source);
// [0]

counter.inc();

// source.get(0).count == 1
// copy.get(0).count == 1

Null elements are not allowed. copyOf() throws NullPointerException when a program tries to copy a collection with null elements.

Set<String> source = new HashSet<>();
source.add("one");
source.add(null);
   
Set<String> copy = Set.copyOf(source);
// java.lang.NullPointerException

List.copyOf(Collection) and Set.copyOf(Collection) can create copies of any Collection implementation. Only Map.copyOf(Map) expects a Map instance as argument.

Set<Integer> numbers = new HashSet<>();
numbers.add(1);
numbers.add(2);    
List<Integer> copy = List.copyOf(numbers);
// [1, 2]

Deque<Long> deque = new ArrayDeque();
deque.add(100L);
deque.add(200L);
Set<Long> copyOfDeque = Set.copyOf(deque);
// [100, 200]

Unmodifiable collection Collectors

Java 10 added four new Collectors to java.util.stream.Collectors.

These four new Collectors have in common that they return an unmodifiable collection. As mentioned before, these kinds of collections disallow null elements, and calling mutator methods on such a collection always throws UnsupportedOperationException.

List<Integer> result = Stream.of(1, 2, 3, 4, 5).filter(n -> n % 2 == 0)
        .collect(Collectors.toUnmodifiableList());
// [2, 4]

result.add(6); // throws java.lang.UnsupportedOperationException

Set<Integer> numbers = Stream.of(1,2,2,3,3,4,4,4).filter(n -> n % 2 == 0)
        .collect(Collectors.toUnmodifiableSet());
// [4, 2]

Ensure that custom objects stored in a Set implement equals(Object) and hashCode() properly.


toUnmodifiableMap(Function,Function) expects two functions as arguments. The first argument is the key mapper, a function producing keys for the resulting map, and the second argument is the value mapper, a function producing the values. Both functions must return non-null values.

This example creates a map with the length of the string as key and the string itself as the value.

Map<Integer,String> result = Stream.of("Paris", "London", "Rome")
        .collect(Collectors.toUnmodifiableMap(String::length, Function.identity()));
// {5=Paris, 6=London, 4=Rom}

The difference of this Collector to toUnmodifiableMap(Function,Function,BinaryOperator) is how duplicate keys are handled. If toUnmodifiableMap(Function,Function) produces duplicate keys an IllegalStateException is thrown.

Map<Integer,String> result = Stream.of("Paris", "London", "Rome", "Madrid")
        .collect(Collectors.toUnmodifiableMap(String::length, Function.identity()));
// java.lang.IllegalStateException: 
// Duplicate key 6 (attempted merging values London and Madrid)

If a duplicate key occurs with the toUnmodifiableMap(Function,Function,BinaryOperator) variant, the function passed as the third argument is called. This function is the merge function and receives the existing and new values as parameters. The merge function has to resolve this collision by returning a new value.

The following example creates a multimap where the Map's values are a collection. The value mapper (2nd argument) wraps the stream items in a HashSet. The merge function (3rd argument) is called when a duplicate key occurs and adds the new value to the existing value. Both values passed to the merge function are an instance of HashSet.

    Map<Integer, Set<String>> result = Stream.of("Paris", "London", "Rome", "Madrid")
        .collect(Collectors.toUnmodifiableMap(
            String::length,
            item -> new HashSet<>(Set.of(item)), 
            (existing, newValue) -> {
              existing.addAll(newValue);
              return existing;
            }));
    System.out.println(result);
    // {4=[Rom], 5=[Paris], 6=[Madrid, London]}

It's important to note that mutable objects stored in these collections can still be changed and the unmodifiable collection reflect the change.

class Counter {
    private int count;
    private Counter(int startCount) {
      this.count = startCount;
    }
    private void inc() { this.count++; }
    @Override
    public String toString() {
      return String.valueOf(this.count);
    }
}

Counter zeroCounter = new Counter(0);
List<Counter> result = Stream.of(zeroCounter).collect(Collectors.toUnmodifiableList());
// [0]
zeroCounter.inc();
int c = result.get(0).count;  // 1

java.io.Reader

The class java.io.Reader got a new transferTo(Writer) method. The method reads all characters from this reader and writes the characters to the given writer. This is the Reader equivalent of the InputStream.transferTo(OutputStream) method introduced in Java 9.

Here an example that copies all the characters from the file test1.txt to the file test2.txt

    FileReader fr = new FileReader("test1.txt");
    FileWriter fw = new FileWriter("test2.txt");
    try (fr; fw) {
      fr.transferTo(fw);
    }

It is worth noting that this particular use case is easier to implement with the Files.copy(Path, Path) method.

java.nio.file.Files.copy(Paths.get("test1.txt"), Paths.get("test2.txt"));

java.util.Optional

Java 10 added one new method to java.util.Optional. orElseThrow() returns the wrapped value if it's not null, otherwise throws NoSuchElementException exception.

Optional<String> result = Optional.of("HelloWorld");
String value = result.orElseThrow(); // HelloWorld
   
result = Optional.ofNullable(null);
value = result.orElseThrow();
// java.util.NoSuchElementException: No value present

Java 11 (September 2018)

The major new addition to the standard Java library is the HTTP client (JEP 321). I also wrote a blog post about this topic.


java.util.Collection

The java.util.Collection interface got the new default method toArray(IntFunction).

List<String> cities = List.of("Rome", "Paris", "London");   
String[] citiesArray1 = cities.toArray(new String[cities.size()]);
Object[] citiesArray2 = cities.toArray();
   
//new in Java 11
String[] citiesArray3 = cities.toArray(String[]::new);

The new toArray(IntFunction) method returns an array containing all of the elements in this collection, using the provided generator function to allocate the returned array. A IntFunction is a function that accepts an int value as an argument and returns a result. The default implementation of toArray(IntFunction) passes 0 to the IntFunction.

According to the JavaDoc, this method acts as a bridge between array-based and collection-based APIs. It allows the creation of an array of a particular runtime type. Use toArray() to create an array whose runtime type is Object[], or use toArray(T[]) to reuse an existing array.


java.util.function.Predicate

Java 11 introduced the new static method not(Predicate) to the java.util.function.Predicate interface. not(Predicate) takes a Predicate and returns the negation of it.

java.util.function.Predicate<Integer> isEven = n -> n % 2 == 0;
java.util.function.Predicate<Integer> isOdd = Predicate.not(isEven);
    
List<Integer> evenNumbers = Stream.of(1,2,3,4,5,6).filter(isEven).collect(Collectors.toUnmodifiableList());
// [2, 4, 6]
    
List<Integer> oddNumbers = Stream.of(1,2,3,4,5,6).filter(isOdd).collect(Collectors.toUnmodifiableList());
// [1, 3, 5]

java.util.Optional

The java.util.Optional class got one new method. isEmpty() returns true when the value of the Optional is null, otherwise false

Optional<Integer> result = java.util.Optional.of(10);
boolean present = result.isEmpty(); // false

The new method is the opposite of isPresent() which returns true when the value is NOT null.


java.lang.String

Six new methods where added to the java.lang.String class: isBlank(), lines(), repeat(int), stripLeading(), stripTrailing(), strip()

isBlank() tests if the string is empty or contains only white space codepoints.

boolean b = "Hello World".isBlank(); // false
b = "   \n   \t  ".isBlank(); // true

lines() returns a stream of lines extracted from this string. A line is either a sequence of zero or more characters followed by "\n", "\r" or "\r\n", or it is a sequence of one or more characters followed by the end of the string. The lines that lines() extracts do not include "\n", "\r" and "\r\n".

List<String> tokens = "Line 1\nLine 2\nLine 3".lines().collect(Collectors.toUnmodifiableList());
[Line 1, Line 2, Line 3]

repeat(int) repeats this string n times and concatenates the repetitions together.

String www = "World".repeat(3); // WorldWorldWorld

stripLeading() removes all leading white space code points, stripTrailing() all trailing code points, and strip() all leading and trailing white space code points.

String st = "\t\tHelloWorld\n\n".stripLeading(); // "HelloWorld\n\n"
st = "\t\tHelloWorld\n\n".stripTrailing(); // "\t\tHelloWorld"
st = "\t\tHelloWorld\n\n".strip(); // "HelloWorld"

Java already has a method that removes white space from both ends: trim(). The difference is that strip() uses the method Character.isWhitespace(int) to determine what white space is. trim() on the other hand defines all codepoints less than or equal to U+0020 as a whitespace character. It is recommended to use strip() because it uses the Unicode standard.


java.nio.file.Files

Java 11 introduced four new methods to the java.nio.file.Files class. writeString(Path,CharSequence,Charset,OpenOption...) and readString(Path, Charset) both methods with two overloaded variants writeString(Path,CharSequence,OpenOption...) and readString(Path). These methods do what their names suggest, write a string to a file and read a string from a file.

Path file1 = Paths.get("./test1.txt");
Files.writeString(file1, "Hello World");

Path file2 = Paths.get("./test2.txt");
Files.writeString(file2, "Hello World", StandardCharsets.UTF_8);
    
String input1 = Files.readString(file1); // Hello World
String input2 = Files.readString(file2, StandardCharsets.UTF_8); // Hello World

The method that does not take a java.nio.charset.Charset argument uses the UTF-8 charset. Hence the writeString() and readString() calls in the example above are equivalent.


Local-Variable Syntax for Lambda Parameters

A small change to the language is that the reserved type name var introduced in Java 10 can now be used when declaring the formal parameters of implicitly typed lambda expressions.

The first two forms are valid syntax, and the third syntax was not allowed in Java 10, but now it is since Java 11.

Predicate<Integer> predicate1 = n -> n % 2 == 0;
Predicate<Integer> predicate2 = (Integer n) -> n % 2 == 0;
Predicate<Integer> predicate3 = (var n) -> n % 2 == 0;

Adding annotations to parameters is a reason for specifying the type or var.

Predicate<Integer> predicate3 = (@Nonnull var n) -> n % 2 == 0;

Java 12 (March 2019)

A significant change in this release is the addition of JMH (Java Microbenchmark Harness JEP 230). JMH is a library that existed before, but starting with Java 12, it is now part of the standard library.

CompactNumberFormat

This is an extension to the existing number formatting API (java.text.NumberFormat). This addition provides the functionality to format numbers into more compact and, for humans, easier to grasp strings.

To obtain a compact number formatter call the method java.text.NumberFormat.getCompactNumberInstance(Locale, NumberFormatStyle) and pass the locale and the style. Alternatively call NumberFormat.getCompactNumberInstance() which returns a compact number format for the default locale and with the SHORT format style.

The following code converts the number 1_234 into "1K" and "1 thousand".

java.text.NumberFormat shortFormat = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
java.text.NumberFormat longFormat = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
    
String shortOutput = shortFormat.format(1_234); // 1K
String longOutput = longFormat.format(1_234); // 1 thousand

By default, the rounding mode half even is used but can be changed with the setRoundingMode(RoundingMode) method.

NumberFormat longFormat = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
longFormat.setRoundingMode(RoundingMode.UP);
String longOutput = longFormat.format(1_234); // 2 thousand

The default formatting behavior returns a formatted string with no fractional digits, however calling the setMinimumFractionDigits(int) method includes the fractional part. Note that the result is rounded according to the configured rounding mode.

NumberFormat shortFormat = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
NumberFormat longFormat = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);

shortFormat.setMinimumFractionDigits(2);
longFormat.setMinimumFractionDigits(1);
    
String shortOutput = shortFormat.format(1_256); // 1.26K
String longOutput = longFormat.format(1_256); // 1.3 thousand

Parsing strings that contain numbers in a compact format is also supported.

NumberFormat longFormat = NumberFormat.getCompactNumberInstance(Locale.GERMAN, NumberFormat.Style.LONG);
        
String input1 = "1,11 Tausend";
String input2 = "1,22 Million";
String input3 = "1,33 Milliarde";
    
Number n1 = longFormat.parse(input1); // 1110
Number n2 = longFormat.parse(input2); // 1220000
Number n3 = longFormat.parse(input3); // 1330000000

teeing Stream Collector

A stream can only be terminated with one collector. This is an issue if multiple streams' results need to be acquired. The new Collectors.teeing(Collector, Collector, BiFunction) collector can apply two collectors on the items of a stream.

The teeing collector expects three arguments. Two Collectors and a BiFunction. The teeing collector applies the two collectors on the items. The result of each collector is then passed as arguments to the BiFunction, which produces the final result of the teeing collector.

The following example loops over a list of strings, transforms them to upper case, and returns the number of elements and the list with the modified city names.

List<String> cities = List.of("Paris", "London", "Tokyo", "Madrid", "Rome");
List<Object> result = cities.stream().map(String::toUpperCase)
                        .collect(java.util.stream.Collectors.teeing(
                                    Collectors.counting(), 
                                    Collectors.toUnmodifiableList(), 
                                    (count, list) -> List.of(count, list))
                                );
    
long count = (long) result.get(0); // 5
List<String> citiesUC = (List<String>) result.get(1); // [PARIS, LONDON, TOKYO, MADRID, ROME]

Multiple teeing collectors can be combined to get more than just two results. The following example returns the shortest name, longest name, the number of cities, and the list with the transformed cities themselves.

List<String> cities = List.of("Paris", "London", "Tokyo", "Madrid", "Rome");

var stringLengthComparator = Comparator.comparing(String::length);

var minMaxCollector = Collectors.teeing(Collectors.minBy(stringLengthComparator),
        Collectors.maxBy(stringLengthComparator),
        (min, max) -> List.of(min.get(), max.get()));

var countListCollector = Collectors.teeing(Collectors.counting(),
        Collectors.toUnmodifiableList(), (count, list) -> List.of(count, list));

var result = cities.stream().map(String::toUpperCase).collect(
        Collectors.teeing(minMaxCollector, countListCollector, (minMax, countList) -> List
            .of(minMax.get(0), minMax.get(1), countList.get(0), countList.get(1))));

String min = (String) result.get(0); // ROME
String max = (String) result.get(1); // LONDON
long count = (long) result.get(2); // 5
List<String> list = (List<String>) result.get(3); // [PARIS, LONDON, TOKYO, MADRID, ROME]

java.nio.file.Files.mismatch(Path, Path)

The new java.nio.file.Files.mismatch(Path, Path) method finds and returns the position of the first mismatched byte in the content of two file, or -1L if there is no difference.

// file1.txt:Hello World
// file2.txt:Hello world

java.nio.file.Path file1 = java.nio.file.Paths.get("file1.txt");
java.nio.file.Path file2 = java.nio.file.Paths.get("file2.txt");
long diffPos = java.nio.file.Files.mismatch(file1, file2); // 6

java.lang.String

The new String.indent(int) method adds spaces to the beginning of a string. If there are multiple lines in the string, terminated with either "\n", "\r" or "\r\n", the methods adds spaces to each line.

String basic = "10 PRINT \"Hello, World!\"\n" + 
               "20 END";
String indented = basic.indent(3);
/*
   10 PRINT "Hello, World!"
   20 END
*/    

It's important to note that the indent(int) method also normalizes the line termination characters. It always ends a line with "\n" regardless of what line termination characters the input uses. Even passing 0 to the method (indent(0)), which does not indent the string, the method still changes the line termination characters.

It is possible to pass a negative value to the method. In that case, indent(int) removes the given number of white space characters from the beginning of each line. If there are not sufficient white space characters in one line, then all leading white space characters are removed.

String basic = " \t\t 10 PRINT \"Hello, World!\"\n" + 
                "\t20 END";
String indented = basic.indent(-3);
/*
 10 PRINT "Hello, World!"
20 END
*/

With the new String.transform(Function) method, a function can be applied to the string. The function should expect the string as an argument and return a result. The result can be another string or something different.

String hw = "Hello World";
int len2 = hw.transform(str -> str.length() * 2); // 22

String cities = "Tokyo;Madrid;London;Paris";
List<String> citiesList = cities.transform(str -> List.of(str.split(";")).stream()
                              .sorted().collect(Collectors.toUnmodifiableList()));
// [London, Madrid, Paris, Tokyo]

String text = "this is line 1\nthe second line";
String transformed = text.transform(str -> 
                      str.lines()
                      .map(line -> line.substring(0, 1).toUpperCase() + line.substring(1))
                      .collect(Collectors.joining("\n")));
//This is line 1\nThe second line

Java 13 (September 2019)

This release did not introduce major changes visible to the developer. A notable significant change under the surface is the re-implementation of java.net.Socket and java.net.ServerSocket (JEP 353)

I didn't find a small change that qualifies for this article.

Java 14 (March 2020)

With Java 14, we get a new language feature: Switch Expressions (JEP 361)

Exact Arithmetic

When we do arithmetic in Java, we need to be aware that datatypes like int and long silently overflow.

int i = 2_147_483_647; // max value of Integer
i = i + 10;  // i == -2_147_483_639

This can lead to subtle bug. Java 8 introduced methods like java.lang.StrictMath.addExact, which throw java.lang.ArithmeticException if an overflow occurs.

int i = StrictMath.addExact(2_147_483_647, 10);
// java.lang.ArithmeticException: integer overflow

In Java 14, we get six more of these *Exact methods. Three for Integers and three for Longs.

int i = 2_147_483_647;
i = StrictMath.decrementExact(i); // i--
i = StrictMath.incrementExact(i); // i++
i = StrictMath.negateExact(i); // i = -i

See JavaDoc of StrictMath for more information


Plural Support in CompactNumberFormat

java.text.CompactNumberFormat introduced in Java 12 is now capable of dealing with plural forms. Here is an example in French that has plural forms for "number" words.

// Java 13
NumberFormat cnf = NumberFormat.getCompactNumberInstance(Locale.FRENCH, Style.LONG);
String output = cnf.format(1_000_000); // 1 million
output = cnf.format(2_000_000); // 2 million

// Java 14
NumberFormat cnf = NumberFormat.getCompactNumberInstance(Locale.FRENCH, Style.LONG);
String output = cnf.format(1_000_000); // 1 million
output = cnf.format(2_000_000); // 2 millions  <=======

Java 15 (September 2020)

Java 15 introduces Text Blocks (JEP 378).

Helpful NullPointerExceptions (JEP 358)

Not a new language feature or API change, nonetheless a useful change for developers. When a NullPointException occurs, the error message is more detailed and shows which part of the code caused the exception. Note this feature is for security, performance, and compatibility reasons disabled by default.

Let's assume we have this Java application.

   class Address {
     private final String city;
     private Address(String city) {
       this.city = city;
     }
   }
   class Person {
     private final Address address;
     private Person(Address address) {
       this.address = address;
     }     
   }
   Address a = new Address(null);
   Person p = new Person(a);
   String cityUpperCase = p.address.city.toUpperCase();    

When we run the application with a JDK prior to Java 15, the JVM prints out this error message.

Exception in thread "main" java.lang.NullPointerException
        at JavaTest.main(JavaTest.java:19)

Just from the error message alone, we don't know what caused the exception. In this application it could be because p or p.address or p.address.city is null.

The same code running on Java 15 gives us a more detailed message, and we immediately see what caused the error.

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "<local2>.address.city" is null
        at JavaTest.main(JavaTest.java:19)

Java 16 (March 2021)

Java 16 adds Record (JEP 395) classes and pattern matching for instanceof (JEP 394 to the language.

Stream.toList convenient method

When you work with streams in Java, you often use a collector to collect all the elements from the stream into a container. The most commonly used collector is .collect(Collectors.toList())

		List<String> fruits = List.of("apple", "pear", "pineapple");
		List<String> upperCaseFruits = fruits.stream().map(String::toUpperCase)
				.collect(Collectors.toList());

Because this collector is so prevalent the JDK developers added a new convenient method to Stream interface: toList() With this addition, we can simplify the code above.

		List<String> upperCaseFruitsTl = fruits.stream().map(String::toUpperCase)
				.toList();

Be aware that .toList() is not an exact equivalent of .collect(Collectors.toList())). The latter returns a modifiable list whereas .toList() returns an unmodifiable list. It behaves similar to .collect(Collectors.toUnmodifiableList())

		List<String> fruits = List.of("apple", "pear", "pineapple");
		List<String> upperCaseFruits = fruits.stream().map(String::toUpperCase)
				.collect(Collectors.toList());
		upperCaseFruits.add("ORANGE"); // ok
		
		List<String> upperCaseFruitsTl = fruits.stream().map(String::toUpperCase)
				.toList();
		upperCaseFruitsTl.add("ORANGE"); // throws java.lang.UnsupportedOperationException

Stream.mapMulti

mapMulti is a new method of the Stream interface. This method is an intermediate operation and replaces each stream element with zero or more elements.

The method is similar to flatMap in that it applies a one-to-many transformation and flattens the result. According to the JavaDoc, mapMulti is preferable to flatMap when an application replaces each element with a small (possibly zero) number of elements. mapMulti avoids the overhead of creating a new Stream instance as flatMap requires. mapMulti also uses an imperative approach which might be easier to implement for certain use cases than returning the result as a Stream.

		List<String> input = List.of("AA", "AB", "AC", "AD");

		List<Character> output = input.stream()
				.flatMap(elem -> Stream.of(elem.charAt(0), elem.charAt(1))).toList();
		System.out.println(output);
		// [A, A, A, B, A, C, A, D]

		List<Character> outputMapMulti = input.stream()
				.<Character>mapMulti((elem, consumer) -> {
					consumer.accept(elem.charAt(0));
					consumer.accept(elem.charAt(1));
				}).toList();
		System.out.println(outputMapMulti);
		// [A, A, A, B, A, C, A, D]

DateTimeFormatter pattern B

In the date-time formatter, we can use the new pattern B to get the period of the day in human-readable form.

		DateTimeFormatter dtf = DateTimeFormatter.ofPattern("B", Locale.ENGLISH);
		System.out.println(dtf.format(LocalTime.of(8, 0))); // in the morning
		System.out.println(dtf.format(LocalTime.of(13, 0))); // in the afternoon
		System.out.println(dtf.format(LocalTime.of(20, 0))); // in the evening
		System.out.println(dtf.format(LocalTime.of(23, 0))); // at night
		System.out.println(dtf.format(LocalTime.of(0, 0))); // midnight
		
		dtf = DateTimeFormatter.ofPattern("B", Locale.FRENCH);
		System.out.println(dtf.format(LocalTime.of(8, 0))); // du matin
		System.out.println(dtf.format(LocalTime.of(13, 0))); // de l’après-midi
		System.out.println(dtf.format(LocalTime.of(20, 0))); // du soir
		System.out.println(dtf.format(LocalTime.of(23, 0))); // du soir
		System.out.println(dtf.format(LocalTime.of(0, 0))); // minuit

The implementation of the B pattern follows the Unicode standard.

Java 17 (September 2021)

This release adds one new language feature to Java: Sealed Classes (JEP 409)

HexFormat

The new java.util.HexFormat class handles conversions from byte array to hexadecimal strings and vice versa. To use the new class, you need to create an instance with either of() or ofDelimiter(String). The returned instance is thread-safe

		HexFormat hexFormat = HexFormat.of();
		byte[] input = new byte[] { 0, 1, 2, 3, 11, 12 };
		String inputHex = hexFormat.formatHex(input); // 000102030b0c    

    byte[] output = hexFormat.parseHex(inputHex);
		// [0, 1, 2, 3, 11, 12]

HexFormat also provides a method to convert an byte, short, int, long and char into a hexadecimal string

String hex =  HexFormat.of().toHexDigits(44)); // 0000002c

By default, HexFormat returns the hexadecimal string with lowercase characters. You can change this with withUpperCase(). Additionally, Hexformat allows adding prefix, suffix, and delimiter to control the output of the hexadecimal string even further.

		byte[] input = new byte[] { 0, 1, 2, 3, 11, 12 };
		String hex1 = HexFormat.of().withUpperCase().formatHex(input)); 
    // 000102030B0C

		String hex2 = HexFormat.ofDelimiter(":").formatHex(input)); 
    // 00:01:02:03:0b:0c

		String hex3 = HexFormat.of().withPrefix("<").withSuffix(">").withDelimiter("-").formatHex(input);
		// <00>-<01>-<02>-<03>-<0b>-<0c>

Enhanced Pseudo-Random Number Generators (JEP 356)

JEP 356 provides new interfaces and implementations for pseudo-random number generators (PRNGs).

java.util.random.RandomGenerator is a new interface for objects that return random values.

The existing classes java.util.Random, [java.util.SplittableRandom](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/SplittableRandom.html), and [java.security.SecureRandom](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/SecureRandom.html), now implement the RandomGenerator interface, but apart from that they behave the same as before.

java.util.random.RandomGeneratorFactory is the entry point to the implementations of the RandomGenerator interface. all() returns all implementations, of(String) selects a specific implementation.

		RandomGeneratorFactory.all().map(r -> r.name()).forEach(System.out::println);
		
		RandomGenerator rnd = RandomGeneratorFactory.of("L64X128StarStarRandom").create();
		int r = rnd.nextInt(1000);
		System.out.println(r);

Check out this article for more information about the new pseudo-random number generators:
https://mbien.dev/blog/entry/enhanced-pseudo-random-number-generators

Java 18 (March 2022)

This release changes the default charset encoding of the platform to UTF-8 (JEP 400). It also brings a simple webserver to the core library (JEP 408). It is also now easier to add code snippets to the JavaDoc (JEP 413).

In Java 18 java.lang.reflect.Method, Constructor, and Field were reimplemented on top of java.lang.invoke method handles (JEP 416) and lastly, hostname and address resolution got a service-provider interface (SPI) so that java.net.InetAddress can use resolvers other than the platform's built-in resolver.

New Math and StrictMath methods

This release adds a bunch of new methods to the Math and StrichtMath class.

Java 19 (September 2022)

Apart from a lot of Incubator and Preview features, this release introduces a port for Linux/RISC-V (JEP 422).

Factory methods for creating Map and Set

Java 19 introduces new factory methods for HashMap, HashSet, LinkedHashMap, LinkedHashSet, and WeakHashMap.

These methods create a new empty Map or Set for the expected number of mappings or elements. The initial capacity is generally large enough so that the expected number of mappings or elements can be added without resizing the map or set.

parallelMultiply

The BigInteger gets a new parallelMultiply(BigInteger val) to multiply numbers.

Compared to multiply(BigInteger val), parallelMultiply uses a parallel multiplication algorithm that typically uses more CPU resources and memory to compute the result faster. Note that an application might only benefit of the parallel algorithm when both numbers are large, typically in the thousands of bits. This method returns the same mathematical result as multiply(BigInteger val).

Java 20 (March 2023)

Like Java 19, this release brings a lot of Incubator and Preview features. Java 20 now supports Unicode 15.

Regarding minor changes, there are new methods for named groups in regular expressions.

Java 21 (September 2023)

The significant addition in this release is the support for Virtual Threads (JEP 444). Java 21 adds two new enhancements to the Java language. Deconstruction of record values with Record patterns (JEP 440) and Pattern Matching for switch (JEP 441) introduces pattern matching in switch expressions and statements.

The Z Garbage Collector (ZGC) got an update with (JEP 439).

The Windows 32-bit x86 port of Java is now deprecated and might be removed in a future release (JEP 449). Java 21 now warns when agents are loaded dynamically into a running JVM as a preparation for a future release that disallows dynamic loading of agents by default (JEP 451).

JEP 452 Introduces the Key Encapsulation Mechanism API, an encryption technique for securing symmetric keys using public key cryptography.

Sequenced Collections JEP 431

Java 21 introduces three new interfaces to the collections framework.

Collections that implement these interfaces store their elements in a defined encounter order. Each such collection has a well-defined first, second, and so forth element.

The Javadoc for each new interface shows which existing collections implement the new interfaces. The most obvious example of a sequenced collection is ArrayList, which stores the elements in an array. But also, LinkedHashMap and LinkedHashSet store their elements in a defined encounter order.

The SequencedCollection<E> interface defines the following methods:

reversed() returns a reverse-ordered view of the collection.

SequencedSet<E> is a sub-interface of SequencedCollection<E> and adds the definition of the following method: SequencedSet<E> reversed()

Note that TreeSet implements the SequencedSet<E> interface, but the addFirst and addLast methods throw UnsupportedOperationException. The comparison method determines the encounter order of elements; therefore, explicit positioning is not supported.

Sequenced maps do not implement SequencedCollection<E> instead they implement SequencedMap<K,V>, which defines the following methods:

TreeMap implements SequencedCollection<E>, but the putFirst and putLast methods throw UnsupportedOperationException. The comparison method determines the encounter order of mappings; therefore, explicit positioning is not supported.

  List<String> abc = new ArrayList<>();
  abc.add("b");
  abc.add("c");
  abc.addFirst("a");
  abc.addLast("d");

  System.out.println(abc.getFirst()); // "a"
  System.out.println(abc.getLast()); // "d"

  
  abc.removeFirst();
  abc.removeLast();
  System.out.println(abc.getFirst()); // "b"
  System.out.println(abc.getLast()); // "c"

  for (String s : abc.reversed()) {
      System.out.println(s);
  }
  // c
  // d
  var map = new LinkedHashMap<>();
  map.put(1, "one");
  map.put(2, "two");
  map.putFirst(0, "zero");
  map.putLast(3, "three");

  System.out.println(map.firstEntry()); // 0=zero
  System.out.println(map.lastEntry());  // 3=three

  for (var entry : map.reversed().entrySet()) {
      System.out.println(entry);
  }
  // 3=three
  // 2=two
  // 1=one
  // 0=zero

  var first = map.pollFirstEntry();
  System.out.println(first); // 0=zero

  var last = map.pollLastEntry();
  System.out.println(last);  // 3=three

  System.out.println(map);   // {1=one, 2=two}

clamp

The Math and StrictMath classes got new clamp() methods to conveniently clamp the numeric value between the specified minimum and maximum values. Each class got four overloads of these methods to support int, long, float, and double.

  int i = 12;
  double d = -12.0;
  float f = 23.5f;
  long l = 1234567890L;

  System.out.println(Math.clamp(i, 0, 10)); // 10
  System.out.println(Math.clamp(d, 0, 10)); // 0.0
  System.out.println(Math.clamp(f, 0, 10)); // 10.0
  System.out.println(Math.clamp(l, 0, 10)); // 10

indexOf

The String class gets two new methods: indexOf(int ch, int beginIndex, int endIndex) and indexOf(String str, int beginIndex, int endIndex). The existing indexOf methods search in the whole string or from a given start index. The two new methods limit the search range further by specifying an end index.

Besides complete control over the search range, they are safer to use than indexOf(int ch, int fromIndex) and indexOf(String str, int fromIndex), because they throw an exception on illegal search ranges.

  String test = "Hello World";

  int pos = test.indexOf("lo", 2, 5);
  System.out.println(pos);
  // 3

  pos = test.indexOf('l', 6, 10);
  System.out.println(pos);
  // 9
  
  // existing indexOf does not check range
  pos = test.indexOf('l', 12);
  System.out.println(pos);
  // -1

  // new indexOf throws exception
  pos = test.indexOf('l', 6, 12);
  // Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [6, 12) out of bounds for length 11

repeat

repeat(int codePoint, int count) and repeat(CharSequence cs, int count) have been added to java.lang.StringBuilder and java.lang.StringBuffer to simplify the appending of multiple copies of characters or strings.

  StringBuilder sb1 = new StringBuilder();
  sb1.repeat("Hello ", 5);
  System.out.println(sb1);
  // Hello Hello Hello Hello Hello

  StringBuilder sb2 = new StringBuilder();
  sb2.repeat('-', 5);
  sb2.append("HELLO");
  sb2.repeat('-', 5);
  System.out.println(sb2);
  // -----HELLO-----

splitWithDelimiters

New splitWithDelimiters() methods added to String and java.util.regex.Pattern.

Unlike the split() method, splitWithDelimiters() returns an alternation of strings and matching delimiters rather than just the strings.

  String input = "1,us,10.0";

  String[] parts = input.split(",");
  System.out.println(parts.length); // 3
  for (String part : parts) {
      System.out.println(part);
  }
  // 1
  // us
  // 10.0



  String[] partsWithDelimiter = input.splitWithDelimiters(",", 0);
  System.out.println(partsWithDelimiter.length); // 5
  for (String part : partsWithDelimiter) {
      System.out.println(part);
  }
  // 1
  // ,
  // us
  // ,
  // 10.0


  Pattern p = Pattern.compile(",");
  String[] items = p.splitWithDelimiters(input, 0);
  System.out.println(items.length); // 5

Emoji support

The following six new methods are added to java.lang.Character for obtaining Emoji character properties, which are defined in the Unicode Emoji Technical Standard (UTS #51):

The names of these methods can also be used in a regular expression pattern with the `p{IsXXX} construct.

  Pattern isEmoji = Pattern.compile("\\p{IsEmoji}");
  var matcher = isEmoji.matcher("\uD83D\uDE0D");
  boolean matches = matcher.matches(); // true

HttpClient AutoCloseable

The java.net.http.HttpClient is now auto-closable, and the the following methods have been added.

JDK Downloads

You've reached the end of this article.

Here is a list of URLs to download JDK builds based on OpenJDK.