Java 16 API Additions: Stream.toList, mapMulti and More

Introduction Alongside records and pattern matching, Java 16Click to read tutorial: Overview of New Features in Java 16 added several methods and classes to the core libraries. This tutorial explains the Java 16 API additions with examples. We will start with two new Stream methods, toList() and mapMulti(). We will then use Unix-domain socket channels for fast communication between processes on the same machine, format times with day periods such as "in the morning", and lastly look at the new long index checks in Objects. Every example is compiled for Java 16. Stream.toList() Collecting a stream into a list is so common that .collect(Collectors.toList()) appears in almost every codebase. Java 16 added a shortcut, stream.toList(). There is one important difference: toList() returns an unmodifiable list, while Collectors.toList() in practice returns a modifiable ArrayList -
Stream.toList() vs Collectors.toList()
//ToListDemo.java
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ToListDemo {
  public static void main(String[] args) {
    List<String> upper = Stream.of("tom", "harry", "sam").map(String::toUpperCase).toList();
    System.out.println(upper);

    List<String> modifiable = Stream.of("a", "b").collect(Collectors.toList());
    modifiable.add("c");
    System.out.println("Collectors.toList() allowed add: " + modifiable);
    try {
      upper.add("ANN");
    } catch (UnsupportedOperationException e) {
      System.out.println("Stream.toList() is unmodifiable: UnsupportedOperationException");
    }
    List<String> withNull = Stream.of("x", null).toList();      // unlike List.of(), nulls are allowed
    System.out.println("Nulls allowed: " + withNull);
  }
}
 OUTPUT of the above code
[TOM, HARRY, SAM]
Collectors.toList() allowed add: [a, b, c]
Stream.toList() is unmodifiable: UnsupportedOperationException
Nulls allowed: [x, null]
Unlike List.of() and Collectors.toUnmodifiableList(), toList() accepts null elements. When replacing collect(Collectors.toList()) in existing code, check that nothing adds to or sorts the returned list afterwards.
Stream.mapMulti() Definition of Stream.mapMulti()
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
mapMulti() replaces each element with zero, one or more elements, like flatMap()Click to read: flatMap(). But instead of returning a new stream for every element, the mapper is given a Consumer and passes each output element to it. This avoids creating many small streams, and is often clearer when the logic is imperative, such as filtering and transforming in one step -
Replacing each element with zero or more elements
//MapMultiDemo.java
import java.util.List;

public class MapMultiDemo {
  record Order(String customer, List<String> items) { }

  public static void main(String[] args) {
    List<Order> orders = List.of(
        new Order("Tom", List.of("Book", "Pen")),
        new Order("Harry", List.of()),
        new Order("Sam", List.of("Laptop")));

    List<String> lines = orders.stream()
        .<String>mapMulti((order, downstream) -> {
          for (String item : order.items()) {
            downstream.accept(order.customer() + " bought " + item);   // emit one element per item
          }
        })
        .toList();
    lines.forEach(System.out::println);

    // Filter and convert in one step: keep only Integers among mixed objects
    List<Integer> numbers = List.<Object>of(1, "two", 3, 4.0, 5).stream()
        .<Integer>mapMulti((obj, downstream) -> {
          if (obj instanceof Integer i) downstream.accept(i * 10);
        })
        .toList();
    System.out.println(numbers);
  }
}
 OUTPUT of the above code
Tom bought Book
Tom bought Pen
Sam bought Laptop
[10, 30, 50]
Harry's order has no items, so it emits nothing. The type witness .<String>mapMulti tells the compiler the type of the output elements, which it cannot infer from the lambda. Unix-domain socket channels Unix-domain sockets connect processes on the same machine through a file system path instead of an IP address and port. They are faster than TCP over the loopback interface, and access can be controlled with file permissions. Java 16 (JEP 380) added support for them to the NIO SocketChannel and ServerSocketChannel classes, on Linux, macOS and Windows 10 or later -
Server and client over a Unix-domain socket
//UnixSocketDemo.java
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class UnixSocketDemo {
  public static void main(String[] args) throws Exception {
    Path socketFile = Files.createTempDirectory("uds").resolve("demo.sock");
    UnixDomainSocketAddress address = UnixDomainSocketAddress.of(socketFile);

    try (ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
      server.bind(address);
      Thread client = new Thread(() -> {
        try (SocketChannel channel = SocketChannel.open(address)) {
          channel.write(ByteBuffer.wrap("Hello over a Unix-domain socket".getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) { e.printStackTrace(); }
      });
      client.start();
      try (SocketChannel connection = server.accept()) {
        ByteBuffer buffer = ByteBuffer.allocate(100);
        connection.read(buffer);
        buffer.flip();
        System.out.println("Server received: " + StandardCharsets.UTF_8.decode(buffer));
      }
      client.join();
    } finally {
      Files.deleteIfExists(socketFile);
    }
    System.out.println("Address type: " + address.getClass().getSimpleName());
  }
}
 OUTPUT of the above code
Server received: Hello over a Unix-domain socket
Address type: UnixDomainSocketAddress
The only differences from TCP are opening the channels with StandardProtocolFamily.UNIX and using a UnixDomainSocketAddress. Binding creates the socket file, which must be deleted afterwards. Unix-domain sockets are commonly used to talk to local services such as Docker and databases, and between containers sharing a volume.
Day periods in date formatting Java 16 added the pattern letter B to DateTimeFormatter, which formats a day period - "in the morning", "at night" - using the locale's own conventions from the Unicode CLDR data -
Formatting with the day period pattern B
//DayPeriodDemo.java
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class DayPeriodDemo {
  public static void main(String[] args) {
    DateTimeFormatter f = DateTimeFormatter.ofPattern("h:mm B", Locale.US);
    for (LocalTime t : new LocalTime[] {LocalTime.of(8, 15), LocalTime.of(12, 0), LocalTime.of(15, 30), LocalTime.of(23, 5)}) {
      System.out.println(t + " -> " + t.format(f));
    }
  }
}
 OUTPUT of the above code
08:15 -> 8:15 in the morning
12:00 -> 12:00 noon
15:30 -> 3:30 in the afternoon
23:05 -> 11:05 at night
Long index checks in Objects Objects.checkIndex(), checkFromToIndex() and checkFromIndexSize(), added in Java 9 for int indexes, got long versions in Java 16, for code which works with large files or off-heap memory. Each returns the index if it is valid, and throws an IndexOutOfBoundsException with a clear message otherwise -
Checking long indexes
//IndexCheckDemo.java
import java.util.Objects;

public class IndexCheckDemo {
  public static void main(String[] args) {
    long fileSize = 5_000_000_000L;
    System.out.println("Valid index: " + Objects.checkIndex(4_999_999_999L, fileSize));
    try {
      Objects.checkFromIndexSize(4_999_999_000L, 2_000L, fileSize);
    } catch (IndexOutOfBoundsException e) {
      System.out.println(e.getMessage());
    }
  }
}
 OUTPUT of the above code
Valid index: 4999999999
Range [4999999000, 4999999000 + 2000) out of bounds for length 5000000000
Summary In this tutorial we went through the API additions in Java 16. We collected streams with the shorter, unmodifiable Stream.toList(), and replaced elements with zero or more elements using mapMulti(). We exchanged data between a server and a client over a Unix-domain socket channel, formatted times with the day period pattern B, and checked long indexes with Objects. Next, read the jpackage tutorialClick to read tutorial: jpackage Tutorial: Packaging Java Apps (Java 16).