Home | Send Feedback

Smaller changes in Java 9 to 14

Published: May 12, 2020  •  java

In this blog post, I take a look at the smaller changes in Java 9 to 14. 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 the more noticeable changes like Local-variable type inference (var) or the new HTTP client or the new switch expression.

I also focus on features that are or could be useful 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 methods, they can't be abstract and can't be 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");
  } 
}

Note you don't need to add the modifier final to a variable. The Java compiler checks if a variable is changed after initialization. If it's 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() expect 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 overloaded methods to take zero to nine key/value pairs. If an application needs to create a map with more than nine key/value pairs is 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 next 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 that contains a and lastly 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 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, from this point, the elements are no longer tested against the Predicate.

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 datatype 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

When custom objects are used, 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("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 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 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 major 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 value and the new value as parameters. The merge function has to resolve this collision by returning a new value.

The following example creates a multimap where the values of the Map is 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)

Java 11 is an important release because it is a long-term support (LTS) release. For example Amazon Corretto supports Java 11 at least until August 2024 and AdoptOpenJDK until October 2024

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 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 a 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 behaviour 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 results from one stream 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 itself.

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 major 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 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  <=======

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 now 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. To enable it pass the following option to the JVM: -XX:+ShowCodeDetailsInExceptionMessages.

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 Java 13 the JVM prints out this error message.

java.lang.NullPointerException
  at ch.rasc.starter.M.go(M.java:26)
  at ch.rasc.starter.M.main(M.java:7)

Just from the error message alone we don't have any clue 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 14 (with flag enabled) gives us a more detailed message and we immediately see what caused the error.

java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "p.address.city" is null
  at ch.rasc.starter.M.go(M.java:26)
  at ch.rasc.starter.M.main(M.java:7)

Next: Java 15 (September 2020)

The next release introduces Text Blocks (JEP 378) to the language. There are other changes targeted for this release like two new Garbage Collectors and the removal of the built-in JavaScript engine Nashorn.

We have to wait and see if there are also some smaller changes that will qualify for this list.

Visit the project page of Java 15 to learn more.

JDK Downloads

You reached the end of this article. Here I want to show you a list of download locations for the JDK and JRE. With all the changes going on in the Java world, Oracle is no longer the only location where you can download Java. Now a few other companies provide JDK and JRE builds based on OpenJDK, and they also support these builds often beyond what Oracle offers.