Functional programming in Java: Streams

First article: zomor.hashnode.dev/functional-programming-i..

Second article: zomor.hashnode.dev/functional-programming-i..

Third article: https://zomor.hashnode.dev/functional-programming-in-java-functions

In this last article, we will talk more in-depth about one of the main things introduced in Java 8, Streams

What is "stream"?

A sequence of elements supporting sequential and parallel aggregate operations (As per the docs). In another word, stream is a way to implement a few operations on a specific collection.

Why "stream"?

Because it's generic, doesn't change its input, uses functions as inputs (mostly) and the most important thing, it is readable.

Also -as we will elaborate later- you can chain operations to run after one another, or if the sequence is not important for you, you can run parallel stream, which takes much less time.

Return type of stream

We have two main return types of streams, they either return another stream (Intermediate operations) or return something else (Terminal Operations)

I- Stream operations

A- map

When you want to implement a specific function on each element in the collection, map is the best solution for you

map is taking

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
list.stream().map( x -> x * 2 ); //Returns Stream<Integer> containing 2, 4, 6, 8
// You can also use Function as explained in previous articles
Function<Integer, String> myFunc = x -> "double is: " + (x * 2);
// Note that the return type can be different //
list.stream().map(myFunc); //Return Stream<String>

B- Filter

Filter is creating a new stream, which contains only the elements that achieve the condition you mentioned

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
list.stream().filter(x -> x % 2 == 0); //Returns Stream<Integer> 2, 4
// You can also use Predicate as explained in previous articles
Predicate<Integer> filterEven = x -> x % 2 == 0;
list.stream().filter(filterEven); //Returns the same as above

C- Sorted

As appears from the name, it's returning a sorted stream, you can use it for ascending or descending order as follows

List<Integer> list = new ArrayList<>(Arrays.asList(2, 3, 1, 4));
list.stream().sorted(); //By default it's ascending -> 1, 2, 3, 4
list.stream().sorted(Comparator.reverseOrder()); //Stream<Integer> 4, 3, 2, 1

// You can use Comparator to create your specific comparator
Comparator<Integer> reversedOrder = (x, y) -> x.compareTo(y);
list.stream().sorted(reversedOrder); //Returns the same as last one 4, 3, 2, 1

II- Terminal Operations

A- forEach

This loops through all the objects and run a (void) method for each of them

List<Integer> list = new ArrayList<>(Arrays.asList(2, 3, 1, 4));
list.stream().forEach(System.out::println); // Will return nothing
// You can also use a consumer and pass it to the forEach
Consumer<Integer> myConsumer = x -> System.out.println("The number is: " + x;
list.stream().forEach(myConsumer);

B- Reduce

To run operations over the whole array, and result in a single value (like for example the sum of the whole array), we can use reduce as following

List<Integer> list = new ArrayList<>(Arrays.asList(2, 3, 1, 4));
// reduce takes two args:
// 1- Initial valaue
// 2- BinaryOperator (Function that takes two inputs, and returns output, the three are of the same type)
list.stream().reduce(0, (accumulated, x) -> accumulated + x);
// You can also use a BinaryOperator and pass it to the reduce
BinaryOperator<Integer> sumArray = (x, y) -> Integer::sum ;
list.stream().reduce(0, sumArray); //Will return 10

C- Collectors

Collectors are a way to collect this whole stream into a specific type, there are multiple types as follows:

1- Grouping (Group by vs Partition by):

Group ByPartition ByNotes
DefinitionReturns map, where the return type of the function is the key, and the value is a list of the type of the original listReturns map, where the key is either true or false, and the value is a list of the type of the original list as per the condition match
InputFunction<T, U>PredicateT is the type of the list U is the return type of the function
Result TypeMap<U, List>Map<Boolean, List>
List<String> list = new ArrayList<>(Arrays.asList(
    "Hello",
    "World",
    "This",
    "is",
    "Beautiful"
));

//Group By//
Function<String, Integer> getLength = s -> s.length();
Map<Integer, List<String>> groupMap= list.stream().collect(Collectors.groupBy(getLength));
//Return as following
// {
//    5: ["Hello", "World"],
//    4: ["This"],
//    2: ["is"],
//    9: ["Beautiful"]
// }

//Partition By//
Predate<String> lessThanFive = s -> s.length < 5;
Map<Boolean, list<String>> partitionMap = list.stream().collect(Collectors(partitionBy(lessThanFive));
//Return as following
// {
//     true: ["This", "is"],
//     false: ["Hello", "World", "Beautiful"]
// }

2- To List

We can revert the output stream to ArrayList using the following collector

List<Integer> list = new ArrayList<>(Arrays.asList(2, 3, 1, 4));
List<Integer> new_list = list.stream().collect(Collectors.toList());
// A new method has been added since Java 16 (Avail in Java 17+)
List<Integer> other_list = list.stream().toList();

3- Count

Used to count the number of this stream, usually used after (filter) operation to count the elements that meet the condition

List<Integer> list = new ArrayList<>(Arrays.asList(2, 3, 1, 4));
Long countMoreThanTwo = list.stream().filter(x -> x > 2).collect(Collectors.counting());

4- Join

You can also join the whole stream in one long string as follows

List<String> list = new ArrayList<>(Arrays.asList(
    "Hello",
    "World",
    "This",
    "is",
    "Beautiful"
));
String joinedList = list.stream().collect(Collectors.joining(","));
//Output: "Hello,World,This,is,Beautiful"

Merging both types (Chaining)

You can chain operations as follows but do not forget that you have to use intermediate operations while in the chain and use terminal operation as the last one

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
Integer sumOfEvenSquaredList = list
                            .stream()
                            .filter(x -> x % 2 == 0)
                            .map(x -> x * x)
                            .reduce(0, Integer::sum);

Parallel Streams

Streams can run multiple threads to make faster results, it can be useful if the sequence is not important in your implementation

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
list.parallelStream().forEach(System.out::println);

Conclusion

This is the last article about functional programming in Java, as we have been discussing through the previous four articles about how Java is doing its best to keep its reputation as one of the most important programming languages. Hope that was helpful for you as a gateway to get more into functional programming in Java.

You can find more code examples on this repo

github.com/elZomor/functional_programming_w..