-
Notifications
You must be signed in to change notification settings - Fork 4
Streams
The stream data structure was introduced in java 8 and is used in functional programming, where data is manipulated in a self contained data pipeline. Streams aim to replace the for loop and are considered a type of declarative programming as opposed to imperative programming, where we write code that tells the program what we want but not how to get it instead of telling the program step by step how to get what we want.
Thus, in streams we completely remove the need for a for loop and avoid needing state management which can cause mutable data structures, leading to bugs (that's why all streams are self-contained).
Lets take a look at some examples
// Imperative Approach
List<Integer> intList = List.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
/**
* Here we have to take note of the "state" of evenList.
* Right now its empty, but it will change its state overtime*
*/
List<Integer> evenList = new ArrayList<>();
for (Integer i : intList) { // we instruct the program to loop through the list
if (i % 2 == 0) { // if the number is even, add to evenList
evenList.add(i);
}
}
System.out.println(evenList.toString());
// Declarative Approach
List<Integer> intList = List.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
/**
* Here we tell the program to give us everything that is even.
* Note that the stream is self-contained, and there's no "state" to be found
*/
List <Integer> evenList =
intList.
stream().
filter((x) -> x % 2 == 0).
collect(Collectors.toList());
System.out.println(evenList.toString());
Here is a non-exhaustive table of some useful stream operations. Feel free to add more to the table and add examples of how these stream operations can be used!
Starting a Stream | Intermediate Operations | Terminal Operations |
---|---|---|
Stream.<T>of(...) Stream.<T>of(T t)
|
map(Function<? super T, ? extends U> mapper) .map((x) -> x.method())
|
collect(Collectors<? super T, U, V> collector) .collect(Collectors.toList())
|
Stream.<T>concat(Stream<? extends T> a, Stream<? extends T> b) |
filter(Predicate<? super T> pred) .filter((x) -> { if x % 2 == 0 return true;})
|
forEach(Consumer<? super T> action) .forEach((x) -> System.out.println(x)) .forEach(System.out::println)
|
Stream.<T>iterate(T a, Predicate limiter, Function increment) Stream.<T>iterate(T seed, UnaryOperator<T> next)
|
flatMap(Function<? super T, ? extends Stream<? extends U>> mapper) .flatMap((x) -> x.stream())
|
noneMatch(Predicate<? super T> pred) |
IntStream.range(1, n) IntStream.rangeClosed(1, n)
|
peek(Consumer<? super T> action> .peek((x) -> System.print.ln(x))
|
allMatch(Predicate<? super T? pred) |
Stream.<T>generate(Supplier<? extends T> s) |
limit(long maximumSize) .limit(5)
|
anyMatch(Predicate<? super T> pred) |
min(Comparator<? super T> comparator) max(Comparator<? super T> comparator)
|
||
.sum() .count()
|
||
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner) .reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
|
||
To start of an empty stream, we can use Stream.iterate
to return an infinite sequential ordered Stream by applying a function f to an initial element seed, creating a stream which consists of an initial seed Ex: f(seed), f(f(seed)), etc...
There are 2 types of Stream.iterate
implementations, the first one starts off with an initial seed, and a function to apply to that seed for the next value and so on
/**
* Initial Seed/starting value is 0
* Subsequently + 1 to each value
**/
Stream<Integer> integers = Stream.iterate(0, (x) -> x + 1);
// 0, 1, 2, 3 ...
// demostrate using IntStream
IntStream.iterate(1, (x) -> x + 1)
The second one starts off with an initial seed, a predicate that decides whether the stream will continue applying the function f, and the function to apply to the seed and subsequent values. Do note if the seed does not pass the predicate, the stream will be empty. If the predicate check is false, the stream will end.
Stream.iterate(1, (x) -> x <= 20, (x) -> x * 2)
.forEach((x) -> System.print.ln(x));
// stream will end when 20 is reached
// 1, 2, 4, 8, 16
Stream.iterate(5, (x) -> x <= 0, (x) -> x - 1)
.forEach((x) -> System.print.ln(x));
// no output since there the first seed would not pass the predicate.
// returns an empty stream
Another way to start a stream is to use generate
along with a Supplier
function. This will create an infinite stream based on the values returned from the Supplier
's get()
method.
For example, generating an infinite stream of 5
s:
Stream<Integer> stream = Stream.<Integer>.generate(() -> 5);
Or generating an infinite stream of random integers from 1 to 10:
Stream<Integer> stream = Stream.<Integer>generate(() -> (int) Math.ceil(Math.random() * 10));
Since the stream generated is infinite, limit()
should be used when terminating the stream:
stream.limit(20).forEach(System.out::println)
Note that the method signature is Stream<T> generate(Supplier<? extends T> s)
, meaning the supplier can return any subclass of T
. For instance, the following methods are valid:
Stream<Number> stream = Stream.<Number>generate(() -> 5);
Stream<Object> stream = Stream.<Object>generate(() -> "s");
Peek is an intermediate operation, which means that its best used together with a terminal operation for it to work. Generally, the peek operation is usually used to debug the stream, by printing out the elements past a certain point in the data pipeline.
An example of using peek()
wrongly would be put the peek()
at the end of the stream pipeline without a terminal operation like so:
Stream<String> nameStream = Stream.<String>of("Russell", "Henry", "Alvin");
nameStream.peek((x) -> System.out.println(x));
The above code will not output anything. To actually print something, you would need the terminal operation forEach()
Here's an example of using peek to debug a stream pipeline:
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
peek()
can also be used to alter the inner state of the element without completely replacing the element like map()
. For example:
Stream<User> userStream = Stream.of(new User("Russell"), new User("Henry"), new User("Alvin"));
userStream.peek(u -> u.setName(u.getName().toLowerCase())) // useful for HashMaps too!
.forEach((user) -> System.out.println(user));
keep in mind that ultimately peek()
was mainly designed for debugging and should be used as such.
Returns a stream consisting of the elements of this stream passes the predicate. Uses the SAM test
on each element
IntStream
.rangeClosed(1, 10)
.filter(x -> x % 2 == 0)
.forEach((x) -> System.out.println(x));
// 2, 4, 6, 8, 10
Applys the given function on each element, expects the returned type to be the same. Uses the SAM apply
on each element
IntStream
.rangeClose(1, 5)
.map((x) -> x + 1)
.forEach((x) -> System.out.println(x));
// 2, 3, 4, 5, 6
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.
IntStream
.rangeClosed(1, 3)
.flatMap((x) -> IntStream. // must return a Stream
rangeClosed(1, x))
.forEach((x) -> System.out.println(x));
// 1, 1, 2, 1, 2, 3
Returns a stream consisting of the elements of this stream, where size of the stream will be no larger than the inputed MaxSize in limit()
.
IntStream
.iterate(2, x -> x + 2)
.limit(5)
.forEach((x) -> System.out.println(x));
// 2, 4, 6, 8, 10 -> 5 elements
Feel free to add in more methods on how to convert list and arrays to streams so that stream operations can be applied on it
- Array -> ArrayList
int[] intArray = {0, 1, 2, 3}; // primitive
ArrayList<Integer> intArrayList = IntStream.of(intArray)
.boxed() // boxed method converts int -> into Integer
.collect(Collectors.toCollection(() -> new ArrayList<Integer>()));
Integer[] integerArray = {0, 1, 2, 3};
ArrayList<Integer> integerArrayList= new ArrayList<>(Arrays.asList(integerArray));
- Array -> List
int[] intArray = {0, 1, 2, 3}; // primitive
ArrayList<Integer> intArrayList = IntStream.of(intArray)
.boxed() // boxed method converts int -> into Integer
.collect(Collectors.toList());
Integer[] integerArray = {0, 1, 2, 3};
List<Integer> integerArrayList= new ArrayList<>(Arrays.asList(integerArray));
- Stream -> List
Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
List<Integer> integerList = stream.collect(Collectors.toList());
- Stream -> ArrayList
Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
List<Integer> integerArrayList = stream.collect(Collectors.toCollection(() -> new ArrayList<Integer>()));
List<Integer> integerArrayList = stream.collect(Collectors.toCollection(ArrayList::new)); // alternative
- ArrayList -> Array
List<Double> floatingNumbers = List.<Double>of(1.00, 2.00, 3.00, 4.00);
double[] doubleArray = floatingNumbers.stream().mapToDouble((x) -> x.doubleValue()).toArray();
- Stream -> Array
Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
stream.toArray(); // ==> Object[4] {1, 2, 3, 4}
Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4);
stream.toArray(Integer[]::new); // ==> Integer[4]{1, 2, 3, 4}
- ArrayList -> Stream
List<Integer> intArrayList = List.<Integer>of(1, 2, 3, 4, 5);
Stream<Integer> intStream = intArrayList.stream();
- Array -> Stream
int[] intArray = new int[] {1, 2, 3, 4, 5};
Stream<Integer> intStream = Arrays.stream(intArray);
Peer Learning
Guides
Setting Up Checkstyle
Setting Up Java
Setting Up MacVim
Setting Up Stu
Setting Up Unix For Mac
Setting Up Unix For Windows
Setting Up Vim
Setting up SSH Config
SSH Without Password
Copying Files From PE Nodes
Using tmux
CS2030 Contents
Lecture 1 SummaryLecture 2 Summary
Access Modifiers
Lecture 3 Summary (Polymorphism)
Compile Time Type VS Runtime Type
Abstraction, Encapsulation, Inheritance, and Polymorphism
SOLID Principles
Class VS Abstract Class VS Interface
Comparable VS Comparator
Generic Types T
HashMap
Raw Types with Generic
Lambda expression
PECS (Producer Extends Consumer Super)
Optional
Streams
Parallel Streams
Monad
Functors and Monads with Category Theory