Java Streams Introduction

Java Streams were introduced in Java 8 and contains classes for processing a sequence of elements. A stream provides a number of methods that can be chained together to produce a transformation on such elements.

The Streams API is integrated with the Java collections framework, and can be used to process a Java collection when a stream is created.

We normally use the type Stream when working with the Streams API. Now let’s look at how we can create streams in Java.

Stream Creation

As mentioned streams can be created from a collection.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void create_FromCollection() {
    List<String> listOfStrings = new LinkedList<String>();
    listOfStrings.add("one");
    listOfStrings.add("two");
    listOfStrings.add("three");

    Stream<String> stream = listOfStrings.stream();
    assertNotNull(stream);
}

Above we use the stream method to get a Stream from a Collection. This is because the java.util.Collection interface contains a default method to return a Stream.

1
2
3
4
5
public interface Collection<E> extends Iterable<E> {
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}

We can also create a Stream from an Array.

1
2
3
4
5
6
@Test
public void create_FromArray() {
    String[] arrayOfString = new String[]{"one", "two", "three"};
    Stream<String> stream = Arrays.stream(arrayOfString);
    assertNotNull(stream);
}

We can also create a stream using a builder.

1
2
3
4
5
6
7
8
9
@Test
public void create_FromStreamBuilder() {
    Stream<Object> stream = Stream.builder()
            .add("one")
            .add("two")
            .build();

    assertNotNull(stream);
}

Stream Parallel Processing

We can also create a parallel stream for executing stream operations over multiple threads.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void parallel_FromCollection() {
    List<String> listOfStrings = new LinkedList<String>();
    listOfStrings.add("one");
    listOfStrings.add("two");
    listOfStrings.add("three");

    Stream<String> stream = listOfStrings.parallelStream(); 
    assertNotNull(stream);
}

When we call an operation on the stream such as:

1
stream.forEach((s) -> //do some work);

It will run the operation in parallel for each element in the parallel stream.

Stream Operations

Now let’s look at some of the common operations on streams. Stream operations can be categorized into two main types of operations.

  • Intermediate Operations: These operations work on the stream in some way and normally return a Stream.
  • Terminal Operations: These operations convert the stream to some result type.

Stream operations are also functional programming friendly.

  • They don’t change the original sequence.
  • Operations are composable(chained)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Test
public void operations() {
    List<Integer> integerList = new LinkedList<>();
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);

    List<Integer> expected = new LinkedList<>();
    expected.add(2);

    List<Integer> evenIntegers = integerList.stream()
            .filter((i) -> i % 2 ==0)
            .collect(Collectors.toList());

    assertThat(evenIntegers, is(expected));
}

For example in the above code the filter method is an intermediate operation, that returns a Stream<Integer> of even numbers. Finally, the call to collect is the terminal operation that converts the stream back to a List<Integer>.

Iterating

We can replace the traditional, for, while loop etc with more concise operations provided by the Stream API.

For example, we can replace the traditional for loop:

1
2
3
4
5
6
7
8
9
List<String> listOfStrings = new LinkedList<String>();
listOfStrings.add("one");
listOfStrings.add("two");
listOfStrings.add("three");


for(String str : listOfStrings) {
    System.out.println(str);
}

With the forEach method.

1
listOfStrings.forEach((s) -> System.out.println(s));

Filtering

We can use the filter method to filter the items of a stream based on some predicate. For example, consider we want to find the even numbers in a list of integers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
List<Integer> integerList = new LinkedList<>();
integerList.add(1);
integerList.add(2);
integerList.add(3);

List<Integer> expected = new LinkedList<>();
expected.add(2);

List<Integer> evenIntegers = integerList.stream()
        .filter((i) -> i % 2 ==0)
        .collect(Collectors.toList());

assertThat(evenIntegers, is(expected));

The above code passes the predicate as a lambda expression to the filter method of the Stream. After all the even numbers are found we apply the collect method to the resulting stream to convert the result back to a List of Integers.

Collecting

The collect method of a Stream can be used to convert a Stream back to some result type such as a Map, Set, List etc.

1
2
3
List<Integer> evenIntegers = integerList.stream()
        .filter((i) -> i % 2 ==0)
        .collect(Collectors.toList());

Above we use the utility class Collectors to convert a stream to a List. Moreover, we can convert a Stream to a Map.

1
2
3
Map<Integer,String> evenIntegerMap = integerList.stream()
        .filter((i) -> i % 2 ==0)
        .collect(Collectors.toMap((k) -> k, (v) -> v.toString()));

Above we use the Collectors.toMap() method to convert the List to a Map<Integer,String>.

Mapping

The Stream API provides methods for converting elements from one type to another. For example, below we convert a List of Integers to a List of Strings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Test
public void operations_map() {
    List<Integer> integerList = new LinkedList<>();
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);

    List<String> expected = new LinkedList<>();
    expected.add("1");
    expected.add("2");
    expected.add("3");

    List<String> integerStringList = integerList.stream()
            .map((i) -> i.toString())
            .collect(Collectors.toList());

    assertThat(integerStringList, is(expected));
}

The flatMap method can also be used to flatten a list of sequences to a single sequence.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Test
public void operations_flatMap() {
    Object[][] arraryOfArrays = new Object[][] {
            new Object[] {1,2,3},
            new Object[] {4,5,6}
    };


    Object[] expected = new Object[] {1,2,3,4,5,6};
    Object[] actual = Arrays.stream(arraryOfArrays).flatMap((o) -> Arrays.stream(o)).toArray();

    assertArrayEquals(expected,actual);
}

Above we convert a multidimensional array to an array with a single dimension.

Reducing

The Stream API provides the reduce method that allows us to reduce a sequence of elements to a single value. For example, let’s say we want to sum a list of integers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Test
public void operations_reduce() {
    List<Integer> integerList = new LinkedList<>();
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);

    Integer expected = 6;
    Integer result = integerList.stream()
            .reduce(0,(x,y) -> x + y);

    assertEquals(expected, result);
}

Above we use the reduce method passing an initial value of 0, and a lambda expression to sum each of the integers in a List.

Conclusion

In this tutorial we introduced Streams in Java. We also provided a brief overview of some of the main operations that can be performed with streams.