Java 10 API Additions: copyOf, toUnmodifiableList & More

Introduction Java 10 is best known for the var keyword, but it also added a few useful methods to the core libraries. This tutorial explains these Java 10 API additions with examples. We will start with List.copyOf(), Set.copyOf() and Map.copyOf(), which create unmodifiable copies of collections, and see how a copy differs from the unmodifiable view returned by Collections.unmodifiableList(). We will then collect streams into unmodifiable collections with Collectors.toUnmodifiableList(), toUnmodifiableSet() and toUnmodifiableMap(). Lastly, we will look at the new no-argument Optional.orElseThrow(), and the other smaller additions.

(Note - The collections returned by these methods are the same kind of unmodifiable collections that Java 9 introduced. To learn about them, read Java 9 immutable collections using List.of, Set.of and Map.ofClick to read tutorial: Java 9 Immutable Collections using List.of, Set.of, Map.of.) List.copyOf(), Set.copyOf() and Map.copyOf() Definitions of the copyOf() methods
static <E> List<E> copyOf(Collection<? extends E> coll)
static <E> Set<E> copyOf(Collection<? extends E> coll)
static <K,V> Map<K,V> copyOf(Map<? extends K, ? extends V> map)
Java 9 added List.of(), Set.of() and Map.of() to create unmodifiable collections from individual elements. Java 10 adds the copyOf() methods, which create an unmodifiable collection from an existing collection or map. This is the usual way to make a defensive copy - for example, when a class stores a list passed to its constructor and must not be affected if the caller changes that list later.
Unmodifiable copies with copyOf()
//CopyOfDemo.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class CopyOfDemo {
  public static void main(String[] args) {
    List<String> names = new ArrayList<>(List.of("Tom", "Harry"));

    List<String> copy = List.copyOf(names);                    // a snapshot
    List<String> view = Collections.unmodifiableList(names);   // a read-only view

    names.add("Sam");
    System.out.println("Original: " + names);
    System.out.println("copyOf:   " + copy);
    System.out.println("View:     " + view);

    try {
      copy.add("Ann");
    } catch (UnsupportedOperationException e) {
      System.out.println("copy.add() -> UnsupportedOperationException");
    }

    // Set.copyOf removes duplicates, Map.copyOf copies the entries
    Set<String> unique = Set.copyOf(List.of("Tom", "Tom", "Harry"));
    System.out.println("Set.copyOf size: " + unique.size());
    Map<String, Integer> marks = new HashMap<>(Map.of("Tom", 78));
    Map<String, Integer> marksCopy = Map.copyOf(marks);
    marks.put("Harry", 91);
    System.out.println("Map.copyOf: " + marksCopy);

    // Copying an unmodifiable collection returns the same object
    List<String> fixed = List.of("a", "b");
    System.out.println("Same instance: " + (List.copyOf(fixed) == fixed));
  }
}
 OUTPUT of the above code
Original: [Tom, Harry, Sam]
copyOf:   [Tom, Harry]
View:     [Tom, Harry, Sam]
copy.add() -> UnsupportedOperationException
Set.copyOf size: 2
Map.copyOf: {Tom=78}
Same instance: true
Explanation of the code
  • List.copyOf(names) copies the elements at the time it is called. Adding "Sam" to names afterwards does not change the copy.
  • Collections.unmodifiableList(names), by contrast, is only a read-only view of names. It cannot be changed through the view itself, but it shows every change made to the underlying list, which is why it includes "Sam". Use copyOf() when you need a collection which can never change.
  • Like all unmodifiable collections, the copy throws an UnsupportedOperationException when you try to modify it.
  • Set.copyOf() silently drops duplicate elements, so the set has 2 elements. This differs from Set.of("Tom", "Tom"), which throws an IllegalArgumentException for duplicates.
  • If the source is already an unmodifiable collection created by List.of() or copyOf(), copyOf() returns it as it is, without making a copy.
  • The collection passed to copyOf() must not contain null. A null element, key or value throws a NullPointerException.
Collectors.toUnmodifiableList(), toUnmodifiableSet() and toUnmodifiableMap() Collectors.toList() makes no promise about the type or mutability of the list it returns - in practice it is a modifiable ArrayList. When the result of a stream must not be changed, Java 10's new collectors return unmodifiable collections directly, instead of wrapping the result in Collections.unmodifiableList() using collectingAndThen()Click to read: collectingAndThen().
Collecting streams into unmodifiable collections
//ToUnmodifiableDemo.java
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class ToUnmodifiableDemo {
  public static void main(String[] args) {
    List<String> names = List.of("Tom", "Harry", "Sam", "Tom");

    List<String> upper = names.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toUnmodifiableList());
    Set<Integer> lengths = names.stream()
        .map(String::length)
        .collect(Collectors.toUnmodifiableSet());
    Map<String, Integer> nameLengths = names.stream()
        .distinct()
        .collect(Collectors.toUnmodifiableMap(n -> n, String::length));

    System.out.println(upper);
    System.out.println(lengths.size() + " distinct lengths");
    System.out.println("Harry -> " + nameLengths.get("Harry"));

    try {
      upper.set(0, "TOMMY");
    } catch (UnsupportedOperationException e) {
      System.out.println("upper.set() -> UnsupportedOperationException");
    }

    // Duplicate keys need a merge function
    try {
      names.stream().collect(Collectors.toUnmodifiableMap(n -> n, String::length));
    } catch (IllegalStateException e) {
      System.out.println("Duplicate key -> " + e.getMessage());
    }
    Map<String, Integer> counts = names.stream()
        .collect(Collectors.toUnmodifiableMap(n -> n, n -> 1, Integer::sum));
    System.out.println("Tom appears " + counts.get("Tom") + " times");
  }
}
 OUTPUT of the above code
[TOM, HARRY, SAM, TOM]
2 distinct lengths
Harry -> 5
upper.set() -> UnsupportedOperationException
Duplicate key -> Duplicate key Tom (attempted merging values 3 and 3)
Tom appears 2 times
Explanation of the code
  • toUnmodifiableList() keeps the order and the duplicates of the stream, while toUnmodifiableSet() removes duplicates - "Tom", "Sam" and "Harry" have lengths 3, 3 and 5, so there are 2 distinct lengths.
  • toUnmodifiableMap(keyMapper, valueMapper) throws an IllegalStateException when two elements map to the same key, as "Tom" does here. The three-argument version takes a merge function, Integer::sum, which combines the values of a duplicate key - here, to count the occurrences of each name.
  • As with copyOf(), these collectors throw a NullPointerException if the stream contains a null element, key or value.
Optional.orElseThrow() Definition of Optional.orElseThrow()
public T orElseThrow()
orElseThrow() returns the value if one is present, and otherwise throws a NoSuchElementException. That is exactly what Optional'sClick to read: Optional's get() method does. It was added because the name get() hides the fact that it can throw, so it was often called without checking whether the Optional had a value. orElseThrow() says it in its name, and is now the preferred alternative to get(). The same method was added to OptionalInt, OptionalLong and OptionalDouble.
Optional.orElseThrow() instead of get()
//OrElseThrowDemo.java
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;

public class OrElseThrowDemo {
  public static void main(String[] args) {
    List<String> names = List.of("Tom", "Harry", "Sam");

    String firstLong = names.stream()
        .filter(n -> n.length() > 3)
        .findFirst()
        .orElseThrow();                       // same as get(), but the name says it may throw
    System.out.println("First long name: " + firstLong);

    int maxLength = names.stream().mapToInt(String::length).max().orElseThrow();
    System.out.println("Longest name length: " + maxLength);

    try {
      Optional.empty().orElseThrow();
    } catch (NoSuchElementException e) {
      System.out.println("Empty Optional -> NoSuchElementException: " + e.getMessage());
    }
  }
}
 OUTPUT of the above code
First long name: Harry
Longest name length: 5
Empty Optional -> NoSuchElementException: No value present
Note - Use orElseThrow() when an empty Optional means a bug - when there must be a value. When the value can be legitimately missing, use orElse(), orElseGet(), or orElseThrow(Supplier) with a more meaningful exception.
Other additions in Java 10
Reader.transferTo() and Charset overloads
//OtherJava10Additions.java
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;

public class OtherJava10Additions {
  public static void main(String[] args) throws IOException {
    // Reader.transferTo(Writer) copies all characters from a reader to a writer
    StringWriter writer = new StringWriter();
    long count = new StringReader("Java 10").transferTo(writer);
    System.out.println("Transferred " + count + " characters: " + writer);

    // PrintStream(OutputStream, boolean, Charset) and ByteArrayOutputStream.toString(Charset)
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    PrintStream out = new PrintStream(bytes, true, StandardCharsets.UTF_8);
    out.print("café");
    String decoded = bytes.toString(StandardCharsets.UTF_8);
    System.out.println(bytes.size() + " bytes decoded to " + decoded.length() + " characters");
  }
}
 OUTPUT of the above code
Transferred 7 characters: Java 10
5 bytes decoded to 4 characters
  • Reader.transferTo(Writer) copies all characters from a reader to a writer and returns the number of characters copied. InputStream.transferTo(OutputStream), its byte equivalent, was added in Java 9.
  • Classes which used to take a charset only as a String name - PrintStream, PrintWriter, Formatter, Scanner and ByteArrayOutputStream.toString() - got overloads which take a Charset object. A Charset cannot be misspelled, so these overloads do not throw the checked UnsupportedEncodingException.
  • "café" (café) is 4 characters, but 5 bytes in UTF-8, since é takes 2 bytes. Decoding the bytes with the same charset gives back the 4 characters.
Summary In this tutorial we went through the API additions in Java 10. We created unmodifiable copies of collections and maps with List.copyOf(), Set.copyOf() and Map.copyOf(), and saw how a copy differs from an unmodifiable view. We then collected streams into unmodifiable collections with toUnmodifiableList(), toUnmodifiableSet() and toUnmodifiableMap(), including handling duplicate keys. Next we used Optional.orElseThrow() as a clearer replacement for get(). Lastly, we looked at Reader.transferTo() and the new Charset overloads. In the next tutorial, we will see how Application Class-Data SharingClick to read tutorial: Java 10 Application Class-Data Sharing (AppCDS) Tutorial reduces the startup time of Java applications.