Home | Send Feedback | Share on Bluesky |

Small changes in Java 9 to 11

Published: 12. May 2020  •  Updated: 31. August 2025  •  java

In this blog post we take a look at the smaller changes in Java 9 to 11. 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 a new language feature or a major new package like the new HTTP client or the Class File API.

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).

Here are the other parts of this series:

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 or 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 required 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 interfaces. 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 or 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 provides 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 reversely 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 the first argument if the Optional has a value; otherwise, it performs the Runnable passed as the 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, it 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, it 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, it 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 an iterate(T, UnaryOperator) method that creates 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 in 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 the from index is inclusive, and the 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. It 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. It compares the numerical elements as unsigned. See Integer.compareUnsigned(int,int) for an example of 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 object 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 the 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 reflects 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 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;