Streams is a new type of data structure introduced to us in lecture 8. Unlike previous data structures, streams are mainly used for functional programming, and we can often manipulate the data to get what we want in a single stream pipeline.
A stream pipeline consists of a data source(e.g. generate/iterate), zero or more intermediate operations(e.g. map, filter) and a terminal operation(e.g. forEach) to end the pipeline. Once a terminal operation is called, the stream is consumed, and can no longer be used. An example of a stream pipeline:
int sum = IntStream.range(1, 10).map(x -> x * 2).filter(x -> x % 2 == 0).sum();
//IntStream.range(1, 10) is the data source
//map and filter are intermediate operations
//sum() is the terminal reduction that ends the stream pipeline
What differentiates streams from other data structures is that streams are lazy. Laziness in streams means that in a stream pipeline, intermediate operations are not evaluated until the terminal operation is called. Furthermore, the data from the data source is only generated and consumed when needed. Another example:
int sum = IntStream.iterate(1, x -> x + 1).map(x -> x * 2).filter(x -> x % 2 == 0).forEach(System.out::println);
//(x -> x * 2) is the Function<T,T> mapper
//(x -> x * 2) is the Predicate<T>
//note this program runs forever since iterate creates an infinite stream
In the above example, when we do create the source with iterate, we create a IntStream object in the heap, which captures the value of
seed and the UnaryOperator next. The rest of the values in the stream is not yet generated.
When we go to intermediate operations map and filter, we create a new IntStream object in the heap, that captures the the mapper
function that we input into the map function and a pointer to the previous IntStream object. At this point, the function mapper is not
yet applied. Likewise for the filter operation, a new IntStream object is created in the heap, capturing the Predicate
Only when we call a terminal operation, in this case forEach, we follow the pointers upstream all the way to the source, get the current element, and apply all the intermediate operations downstream up to the terminal operation. We need to go through this upstream-downstream process for each element in the stream.
As in Prof Henry's lecture 9 slides, this example illustrates the laziness in streams.
int sum = IntStream
.rangeClosed(1, 10)
.filter(
x -> {
System.out.println("filter: " + x);
return x % 2 == 0;
})
.map(
x -> {
System.out.println("map: " + x);
return 2 * x;
})
.sum();
System.out.println(sum);
This program prints:
filter: 1
filter: 2
map: 2
filter: 3
filter: 4
map: 4
filter: 5
filter: 6
map: 6
filter: 7
filter: 8
map: 8
filter: 9
filter: 10
map: 10
sum is 60
The output showing the filter and map interleaving with each other shows that the whole pipeline of intermediate operation is applied to one element at a time, as we would expect from the laziness of streams causing our evaluation process to go upstream and downstream for each element and only generating the element and applying the operations as we need. For contrast, if we were to use a List for the same program, the program would first filter the entire list, and then map the entire list, as opposed to applying the whole pipeline to one element at a time.
We can create incomplete stream pipelines with only intermediate operations and pass it around our program to be to continue adding more
intermediate operations or to be consumed at a later time.
In addition, since data elements are only generated and consumed when needed, we can create infinite streams. This is useful when we
want to do things where we don't know how many elements we need in our data structure. For example, we can write a program to find the
first 100 prime numbers. If we were to use a list, we would not know how big our initial list needs to be. Using streams, we can simply
create an infinite stream, filter with a predicate to look for prime numbers and set a limit on the number of elements we want in the
stream.
From Array to ArrayList
import java.util.Arrays;
import java.util.ArrayList;
import java.util.stream.IntStream;
import java.util.stream.Collectors;
/*
Integer[] -> ArrayList<Integer>
Works with any Object instead of Integer as well.
Does not work for primitive int[] array as Arrays.asList() method does not deal
with autoboxing and it will just create a List<int[]>
*/
Integer[] integerArray = {0, 1, 2, 3};
ArrayList<Integer> integerArrayList = new ArrayList<>(Arrays.asList(integerArray));
// int[] -> ArrayList<Integer>
int[] intArray = {0, 1, 2, 3};
// boxed method converts primitive int to Integer
ArrayList<Integer> intArrayList = IntStream.of(intArray).boxed()
.collect(Collectors.toCollection(ArrayList::new));
From Array to Stream
// Integer -> Stream<Integer>
Integer[] integerArray = {0, 1, 2, 3};
Stream<Integer> integerStream = Arrays.stream(integerArray);
// int -> IntStream
int[] intArray = {0, 1, 2, 3};
IntStream intStream = Arrays.stream(intArray);
From ArrayList to Array
// ArrayList<Integer> -> Integer[]
Integer[] integerArray = new Integer[integerArrayList.size()];
integerArray = integerArrayList.toArray(integerArray);
// ArrayList<Integer> -> int[]
int[] intArray = integerArrayList.stream().mapToInt(Integer::intValue).toArray();
// IntStream -> int[]
int[] intArray = intStream.toArray();
From ArrayList to Stream
// ArrayList<Integer> -> Stream<Integer>
Stream<Integer> integerStream = integerArrayList.stream();
// ArrayList<Integer> -> IntStream
IntStream intStream = integerArrayList.stream().mapToInt(Integer::intValue);
From Stream to Array
// Stream<Integer> -> Integer[]
Integer[] integerArray = integerStream.toArray(Integer[]::new);
// intStream -> int[]
int[] intArray = intStream.toArray();
From Stream to ArrayList
// Stream<Integer> -> ArrayList<Integer>
List<Integer> integerList = integerStream.collect(Collectors.toList());
ArrayList<Integer> integerArrayList = new ArrayList<>(integerList);
// intStream -> ArrayList<Integer>
List<Integer> integerList = intStream.boxed().collect(Collectors.toList());
ArrayList<Integer> integerArrayList = new ArrayList<>(integerList);
Here is a Summary for Streams that you can use during finals since there are so many methods for streams.
Do note that these are not exhaustive but what I feel is the most common(for this course that is).
Also, the Stream API is here and IntStream is here
Making a Stream out of individual values or an array or a list.
//From a bunch of digits
Stream<Integer> stream = Stream.of(1, 2, 3, 5, 7, 11);
//From an array
Stream<String> stream = Stream.of(myArray)
// From a list (need to import java list)
list.stream(); // or from a list
Returns an infinite sequential unordered stream where each element is generated by the provided Supplier. Do note that these are infinite Streams and thus, would need a .limit() terminal function.
// just some examples
// creating a strean of the words "test" 10 times.
Stream<String> stream = Stream.generate(() -> "test").limit(10);
// creating a stream of random numbers
Stream.generate(new Random()::nextInt).limit(5).forEach(System.out::println);
Returns an infinite sequential ordered Stream produced by iterative application of a function f to an initial element seed, producing a Stream consisting of seed, f(seed), f(f(seed)), etc. It takes in a seed and a unaryoperator
// starts from 0
Stream<Integer> integers = Stream.iterate(0, n -> n + 1);
// starts from 2
IntStream.iterate(2, x -> x + 1)
Furthermore, there is also another type of iterate, where it can iterate and create a new element, but it has to satisfy the hasNext predicate. The first element will be the supplied seed value, the next element will be the result of applying the next function to the seed value, and so on iteratively until the hasNext predicate indicates that the stream should terminate. Do note if the seed does not pass the predicate, the stream will be empty.
Stream.iterate(1, i -> i <= 20, i -> i * 2).forEach(System.out::println);
// output willl be 1,2,4,8,16
Stream.iterate(1, i -> i<0, i-> i-1).forEach(System.out::println);
// no output since there the first seed would not pass the predicate.
Stream.concat method creates a concatenated stream in which the elements are all the elements of the first stream followed by all the elements of the second stream. The resulting stream is ordered if both of the input streams are ordered.
// Creating 2 streams
Stream<String> stream1 = Stream.of("I", "hate");
Stream<String> stream2 = Stream.of("circuit", "breaker");
//Concating them and displaying the result
Stream.concat(stream1, stream2).forEach(element -> System.out.println(element));
// I
// hate
// circuit
// breaker
IntStream range(int startInclusive, int endExclusive) returns a sequential ordered IntStream from startInclusive (inclusive) to endExclusive (exclusive) by an incremental step of 1.
IntStream rangeClosed(int startInclusive, int endInclusive) returns an IntStream from startInclusive (inclusive) to endInclusive (inclusive) by an incremental step of 1.
IntStream stream = IntStream.rangeClosed(-1, 2);
// produces -1, 0, 1, 2
IntStream stream = IntStream.range(-1, 2)
// produces -1, 0, 1
Returns a stream consisting of the elements of this stream that match the given predicate.
Stream.of(1,2,3,4,5).filter(x -> x < 3);
// 1, 2
IntStream.rangeClosed(1, 10).filter(x -> x % 2 == 0).forEach(System.out::println);
// 2 4 6 8 10
Applying a function to each element
IntStream.rangeClosed(1,4).map(x -> x + 1);
// 2 3 4 5
Do note that mapToObj is only present in IntStream/LongStream but not in the primitive Stream. This is because a Stream itself is already a stream of objects. Use mapToX (mapToObj, mapToDouble, etc.) if the function yields Object, double, etc. values.
// creates a bunch of circles as per lecture notes
Stream<Circle> circles = IntStream.rangeClosed(1, 3).mapToObj(Circle::new);
// From the midterms 2, mapping all the values in the rangeClosed to a String(you cant see the difference if you just print it)
IntStream.rangeClosed(1,3).mapToObj(x ->x + "").forEach(System.out::println);
Stream flatMap(Function mapper) returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element. flatMap() is the combination of a map and a flat operation i.e, it applies a function to elements as well as flatten them.
// Present in the lecture notes as well.
IntStream.rangeClosed(1,3).flatMap(x -> IntStream.rangeClosed(1,x)).forEach(System.out::println);
//1 1 2 1 2 3
//
Similarly, there are also flatMaptoDouble/Int/Long. There isnt a flatmaptoObj but you can just do a workaround by combining mapToObj and flatMap.
Stream sorted() returns a stream consisting of the elements of this stream, sorted according to natural order. For ordered streams, the sort method is stable but for unordered streams, no stability is guaranteed. It can also take in a comparator to sort objects that are passed into Streams.
IntStream.of(7, 9, 5, 2, 8, 4, 1, 6, 10, 3).sorted().forEach(System.out::println);
//sorts the stream accordingly
Returns a stream consisting of the elements of this stream, truncated to be no longer than maxSize in length.
IntStream.iterate(2, x -> x + 2).limit(5).forEach(System.out::println);
//produces 2 4 6 8 10
distinct() returns a stream consisting of distinct elements in a stream. distinct() is the method of Stream interface. This method uses hashCode() and equals() methods to get distinct elements.
IntStream.of(1, 1, 1, 0, 0, 0, 1, 0, 0, 1).distinct().forEach(System.out::println);
// 1 0
The skip(long N) is a method of java.util.stream.Stream object. This method takes one long (N) as an argument and returns a stream after removing first N elements.
IntStream.iterate(2, x -> x + 2).skip(2).limit(5).forEach(System.out::println);
// 6 8 10 12 14 (it skips the first 2 and 4)
Stream peek(Consumer action) returns a stream consisting of the elements of this stream, additionally performing the provided action on each element as elements are consumed from the resulting stream. This is an intermediate operation i.e, it creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Therefore, using peek without any terminal operation would do nothing.
This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline.
/// no output because there is no terminal operation.
IntStream.iterate(2, x -> x + 2).limit(5).peek(System.out::println);
//
IntStream.iterate(2, x -> x + 2).limit(5).peek(System.out::println).count();
// 2 4 6 8 10
// returns 5
Performs an action for each element of this stream. It is a terminal operation.
IntStream.range(4, 9).forEach(System.out::println);
// 4 5 6 7 8
These operations return a boolean result. noneMatch returns true if none of the elements pass the given predicate allMatch returns true if every element passes the given predicate anyMatch returns true if at least one element passes the given predicate
IntStream.range(2, 5).noneMatch(x -> 5 % x == 0); // true
IntStream.range(2, 5).allMatch(x -> 5 % x == 0); // false
IntStream.range(2, 5).anyMatch(x -> 5 % x == 0); // false
// if we replace 5 with 6
// we will get false false true.
Returns the count of elements in the stream as a long. It is a terminal operation
IntStream.rangeClosed(1, 10).count();
// returns 10. Note that this 10 is of long type.
reduce(T identity, BinaryOperator
Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value. First argument to reduce is the operation’s identity value, Second argument is the lambda that receives two
values and performs the accumulator onto them.
IntStream.rangeClosed(3,6).reduce(1, (x, y) -> x + y);
// this basically sums the entire thing up. BUT it starts with the seed of 1
// so it is 1+3+4+5+6
There is another version of reduce that does not take in an identity/seed. It performs a reduction on the elements of this stream, using an associative accumulation function, and returns an Optional describing the reduced value, if any.
NOTE: that this returns an Optional. This is because the calculation will start from the first element of the Stream(the first element will be treated as the seed). But there is also
the possibility that the stream is Empty. In that case, it will return an Optional.
IntStream.rangeClosed(3,6).reduce((x, y) -> x + y);
// this will produce an optional of 3 + 4 + 5 + 6 which is Optional(18)
//However, if we input an empty stream
IntStream.rangeClosed(3,3).filter(x -> x < 2).reduce((x,y)-> x+y)
// you will get an optional empty
//If there is only 1 element
IntStream.rangeClosed(3,3).reduce((x,y)-> x+y)
// you will get the only element, optional(3)
// useful way to get the last element of the stream
IntStream.rangeClosed(3,6).reduce((x,y)->y)
//Optional[6]
Stream.min() returns the optional containing the minimum element of the stream based on the provided Comparator. You can skip using a comparator if the stream is an IntStream as it will return according to the natural ordering. max returns the largest element.
IntStream.rangeClosed(3,6).min()
// returns Optional[3]
// using a comparator for a normal stream
List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
Integer var = list.stream().min(Integer::compare).get(); //get used to get the number out of the optional.
// returns -18
The uses of collect and toArray are explained in the converting from stream to array or stream to list.