Java 8 Creating, Combining and Sorting Streams

Introduction

This tutorial covers three everyday stream tasks in Java 8 that the other tutorials in the series touch only in passing: creating a stream from different sources, combining several streams into one, and sorting a stream. We will start with an overview of the ways to create a stream - from values, arrays, strings, regular expressions and builders. We will then compare Stream.concat() with flatMap() for joining streams, and look at sorted() with and without a ComparatorClick to read: Comparator, including why it behaves differently from most other operations. Lastly, we will run one example covering all of these and list the common pitfalls.

(For the basics of streams, see the Java 8 Streams API tutorialClick to read: Java 8 Streams API tutorial; infinite streams have their own iterate and generate tutorialClick to read: iterate and generate tutorial.)

Ways to create a stream

  • Stream.of(T... values): a stream of the given values. Stream.empty() gives a stream with none.
  • Arrays.stream(array) and Arrays.stream(array, from, to): a stream over an array, or a part of it. For int[], long[] and double[] it returns a primitive stream.
  • collection.stream(): the most common source - see also parallelStream()Click to read tutorial: Java 8 Parallel Streams Tutorial with Examples.
  • "text".chars(): an IntStream of the characters of a string.
  • Pattern.splitAsStream(text): splits text around a regular expression, lazily - new in Java 8.
  • Stream.builder(): add elements one at a time, then build(), when the elements are produced by code rather than held in a collection.
  • Stream.iterate() and Stream.generate(): infinite streams; IntStream.range(): number ranges (see range and rangeClosedClick to read: range and rangeClosed).
  • Files.lines(path) and BufferedReader.lines(): the lines of a file - see the word count exampleClick to read: word count example.
Ways to create a Java 8 stream, and joining streams with Stream.concat versus flatMap

Combining streams - concat() or flatMap()

Definition of Stream.concat() in java.util.stream.Stream -
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
Where,
  • The result contains all elements of a followed by all elements of b, lazily - nothing is copied up front.
  • It takes exactly two streams. The JDK documentation warns that building a long chain of nested concat() calls can cause deep call stacks and even a StackOverflowError.
  • For any number of streams or collections, put them in a stream and flatten it with flatMap()Click to read: flatMap(): Stream.of(a, b, c).flatMap(Function.identity()), or lists.stream().flatMap(List::stream).

Sorting - sorted() and sorted(Comparator)

sorted() sorts by natural order - the elements must implement Comparable, or the terminal operation throws a ClassCastException. sorted(Comparator) sorts with the given comparator, which can be built with Comparator.comparing(), reversed and chained with thenComparing(). For ordered streams the sort is stable: equal elements keep their original order.

Unlike filter() or map(), which pass each element on as soon as they have processed it, sorted() is a stateful operation: it cannot emit the smallest element until it has seen the last one.
Java 8 Stream sorted is a stateful barrier operation that buffers every element before emitting the first

Java 8 code example of creating, combining and sorting streams

The example uses a small Employee class with a name and a salary.
Employee.java
//Employee.java
public class Employee {
  private final String name;
  private final double salary;

  public Employee(String name, double salary) {
    this.name = name;
    this.salary = salary;
  }

  public String getName() { return name; }
  public double getSalary() { return salary; }

  @Override
  public String toString() { return name + " (" + salary + ")"; }
}
Java 8 code showing stream sources, Stream.concat, flatMap and sorted
//StreamSources.java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamSources {

  public static void main(String[] args) {
    // 1. Creating streams from values, arrays, strings and builders
    Stream<String> fromValues = Stream.of("Tom", "Harry", "Nancy");
    String[] departments = {"Finance", "Sales", "IT", "HR"};
    Stream<String> fromArrayPart = Arrays.stream(departments, 1, 3);     // elements 1 and 2
    Stream<String> fromPattern = Pattern.compile(",\\s*").splitAsStream("Ethan, Deborah,Tom");
    IntStream fromChars = "JAVA".chars();
    Stream<String> built = Stream.<String>builder().add("first").add("second").build();
    System.out.println("1. of: " + fromValues.collect(Collectors.toList())
        + ", array part: " + fromArrayPart.collect(Collectors.toList())
        + ", splitAsStream: " + fromPattern.collect(Collectors.toList()));
    System.out.println("   chars: " + fromChars.mapToObj(c -> String.valueOf((char) c))
        .collect(Collectors.joining("-"))
        + ", builder: " + built.collect(Collectors.toList())
        + ", empty: " + Stream.empty().count() + " elements");

    // 2. Combining two streams with concat
    Stream<String> finance = Stream.of("Tom Jones", "Harry Major");
    Stream<String> sales = Stream.of("Ethan Hardy", "Nancy Smith");
    List<String> both = Stream.concat(finance, sales).collect(Collectors.toList());
    System.out.println("2. concat: " + both);

    // 3. Combining many streams with flatMap
    List<List<String>> teams = Arrays.asList(
        Arrays.asList("Tom Jones", "Harry Major"),
        Arrays.asList("Ethan Hardy"),
        Arrays.asList("Nancy Smith", "Deborah Sprightly"));
    List<String> everyone = teams.stream()
        .flatMap(List::stream)
        .collect(Collectors.toList());
    System.out.println("3. flatMap of 3 teams: " + everyone);

    // 4. Sorting: natural order, a Comparator, reversed and multi-level
    List<Employee> staff = Arrays.asList(new Employee("Tom Jones", 5200),
        new Employee("Harry Major", 4100), new Employee("Nancy Smith", 5200),
        new Employee("Ethan Hardy", 6300));
    System.out.println("4. names sorted: "
        + staff.stream().map(Employee::getName).sorted().collect(Collectors.toList()));
    System.out.println("   by salary, highest first, then name: " + staff.stream()
        .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()
            .thenComparing(Employee::getName))
        .map(Employee::getName)
        .collect(Collectors.toList()));

    // 5. sorted() is stateful: it must see every element before passing one on
    StringBuilder trace = new StringBuilder();
    Stream.of(3, 1, 2)
        .peek(n -> trace.append("in:").append(n).append(' '))
        .sorted()
        .forEach(n -> trace.append("out:").append(n).append(' '));
    System.out.println("5. " + trace.toString().trim());
  }
}
 OUTPUT of the above code
1. of: [Tom, Harry, Nancy], array part: [Sales, IT], splitAsStream: [Ethan, Deborah, Tom]
   chars: J-A-V-A, builder: [first, second], empty: 0 elements
2. concat: [Tom Jones, Harry Major, Ethan Hardy, Nancy Smith]
3. flatMap of 3 teams: [Tom Jones, Harry Major, Ethan Hardy, Nancy Smith, Deborah Sprightly]
4. names sorted: [Ethan Hardy, Harry Major, Nancy Smith, Tom Jones]
   by salary, highest first, then name: [Ethan Hardy, Nancy Smith, Tom Jones, Harry Major]
5. in:3 in:1 in:2 out:1 out:2 out:3

Explanation of the code

  • Part 1: Arrays.stream(departments, 1, 3) takes the elements at index 1 and 2 (the end index is exclusive). splitAsStream() splits on a comma followed by any spaces. chars() gives int code points, turned back into strings with mapToObj(). Stream.<String>builder() needs the type witness because nothing else tells the compiler the element type.
  • Part 2: concat() joins the Finance stream and the Sales stream, keeping their order. Each input stream is consumed, so neither can be used again.
  • Part 3: three team lists are flattened into one list of five names with flatMap(List::stream) - the way to combine any number of sources.
  • Part 4: sorted() on the names uses String's natural order. The second pipeline sorts by salary, highest first, and breaks the tie between Nancy and Tom (both 5200) by name.
  • Part 5: the trace shows sorted() as a barrier: all three elements pass peek() ("in") before any reaches forEach() ("out"), and they come out in sorted order.

Common pitfalls

  • Sorting an infinite stream: Stream.iterate(1, n -> n + 1).sorted().limit(5) never finishes, because sorted() waits for an end that never comes. Put limit() before sorted().
  • Sorting what is not Comparable: sorted() on objects without a natural order compiles but throws ClassCastException when the stream runs. Pass a Comparator.
  • Reusing a stream: a stream can be consumed only once. After concat(a, b), both a and b are used up; create new streams if you need them again.
  • Arrays.asList(intArray).stream(): for an int[], this is a stream of one element - the array itself. Use Arrays.stream(intArray) or IntStream.of().
  • Losing the order after sorting: collecting a sorted stream into a HashSet or HashMap discards the order - see how to sort a mapClick to read tutorial: Java 8 - How to Sort a Map by Key or Value.

Summary

In this tutorial we first listed the ways to create a stream in Java 8: Stream.of(), Arrays.stream(), collections, chars(), splitAsStream(), builders, generators and files. Next we compared Stream.concat(), for exactly two streams, with flatMap(), for any number. We then saw sorted() and sorted(Comparator), and why sorting is a stateful barrier. We then ran an example of all of these, including a trace of sorted(). Lastly, we listed the common pitfalls, from sorting infinite streams to reusing consumed ones.