This is an article on December 20th of Works Human Intelligence Advent Calendar 2020 (https://qiita.com/advent-calendar/2020/whi), which embodies the Advent Calendar [Develop fun!] Of Works Human Intelligence Co., Ltd. If you don't mind, please take a look at other articles.
The Stream API is a mechanism added from Java 8 that conveniently handles iterative processing for collections. The Oracle documentation explains:
** Description of package java.util.stream ** Map to collection-A class that supports functional operations on a stream of elements, such as reduce transforms. https://docs.oracle.com/javase/jp/8/docs/api/java/util/stream/package-summary.html
The Stream API allows you to write complex iterations in several steps, allowing you to write readable and maintainable code. For example, compare the simple example of "summing a sequence of numbers from 1 to 100".
int sum = 0;
for (int i = 1; i <= 100; ++i) {
sum += i;
}
int sum = IntStream.rangeClosed(1, 100).sum();
The important thing is not that the code is in one line, but that the process is divided into stages. Coloring green as a sequence, yellow as a result, and so on:
As you can see, in the good old for, yellow is scattered in two places, but in Stream, it is gathered in one place. This means that the process is organized step by step.
Normally, the process of processing the generated collection is also included, so there will be more code between green and yellow. The more complex the content of the iteration, the more meaningful it is to separate collection generation, processing, and result generation.
In this way, the Stream API is a mechanism that incorporates the good points of functional styles into Java and provides iterative processing that is easy to read and write.
Also, because there are mechanisms similar to the Stream API in different languages, what you've learned here can be applied and used in different languages in the same way.
Reference: Why do we stubbornly avoid for
A for statement is, so to speak, a "god of iteration" that can express any iteration. And the Stream API is the result of refactoring this iterative god.
The following figure shows the process by which the repetitive force is divided.
goto (Unconditional jump)
+-> if (Conditional branch)
+-> try-catch-finally (Exception handling)
+-> for (Iterative processing= while,Recursive function)
+-> unfold (Deployment)
+-> Stream.iterate
+-> Stream.generate
+-> Stream.of
+-> Stream.empty
+-> IntStream.rangeClosed
...
+-> Stream.reduce (Convolution)
+-> IntStream.sum
+-> Stream.max
+-> Stream.allMatch
+-> Stream.count
+-> Stream.flatMap
+-> Stream.map
+-> Stream.peek
+-> Stream.filter
...
Just as you can use goto for conditional bifurcation and iterative processing, you can use for to cover all of the Stream API. On the flip side, the Stream API is that much smaller iteration.
In addition to iterating with for (while, do-while), you can also iterate using recursive functions.
Iterative syntax and recursive functions look completely different, but they actually have exactly the same functionality. In other words, everything that can be written with for can be written with a recursive function, and vice versa.
However, for and recursive functions have different properties, and in practice it is necessary to use them properly.
In the previous chapter, we explained that the Stream API is a for for each function. However, there are some relatively strong functions within this Stream API as well.
First of all, iterative processing can be roughly divided into two. The process of creating a collection such as a sequence (expansion, unfold) and the process of aggregating a collection into some value (convolution, fold ≒ reduce).
In other words, this unfold and fold are the pillars of the Stream API's functional division [^ 1]. Stream API functions basically belong to either of these.
[^ 1]: It is different from the distinction between intermediate operation and terminal operation.
reduce
The reduce
in the Stream API is like a" god of convolution ", and you can use them to perform most convolution operations.
The range of convolution operations is wide, from the basics provided by the Stream API to such things! ?? You can even implement functions that you think are reduce
.
intstream.sum(); // IntStream<Integer>Fold into an Int
list.size(); // List<T>Fold into an Int
list.flatMap(f); // List<T>List<U>Fold in
insertionSort(list); // List<Integer>List<Integer>Fold in
flatMap
flatMap
is a relatively strong function because it can implement map
and filter
in combination with singleton
.
In addition to this, flatMap
has the property of being able to perform multiple loops.
In most cases, you will want to use flatMap
in practice.
var list = List.of(1, 2, 3);
var result = list.stream().flatMap(x -> {
return list.stream().map(y -> {
return x * y;
});
//map flatMap+Example of writing in singleton
// return list.stream().flatMap(y -> {
// return Collections.singletonList(x * y);
// });
})
.collect(Collectors.toList());
This characteristic of flatMap
is noticed in functional languages such as Haskell, and the interface with only flatMap
and singleton
is called ** monad ** and is cherished.
Writing Java-like pseudo code looks like this.
interface Monad<T> {
<U> Monad<U> flatMap(Function<T, Monad<U>> f);
static Monad<T> singleton(T a);
}
By the way, isn't this interface similar to the JavaScript Promise
?
Looking up at the sky, closing my eyes, thinking of a JavaScript Promise
, turning to the monitor and opening a thin eye, I feel that flatMap
looks like then
and Monad.singleton
looks like Promise.resolve
. Would you like to ...?
Promise
…… You …… Monad ……?
Promise.resolve(value); //Creating a promise from the value
p.then(value => { //Passing a function that takes a value and returns a Promise
console.log(value);
return Promise.resolve(value);
});
Unfolding in programming is the operation of creating a recursive structure from a single value, as opposed to convolution.
Unlike convolution (reduce
), the Stream API doesn't have a function called smashing, but if it did, it would look like this:
public static <T, U> Stream<U> iterate(
T seed, //seed
Predicate<? super T> hasNext, //Whether you can get the next seed and value from a seed
UnaryOperator<T> next, //Get the next seed from a seed
Function<? super T, ? extends U> mapper //Get value from seed
);
A function that allows you to pass a mapper
to the iterate
overload added in Java 11.
It is used as follows.
iterate(1,
n -> true,
n -> n + 1,
n -> fizzbuzz(n))
.limit(15)
.forEach(System.out::println);
The final Stream is the collection of these values.
In the fizzbuzz example, * species * is a sequence of numbers greater than or equal to 1, and the value is a collection of integers converted by fizzbuzz.
The above program produces the following output (supplementing the fizzbuzz
function):
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
The range of expansion operations is wide, and roughly speaking, all functions with the following types are expanded.
<T, R> Stream<R> someunfold(T seed);
The simplest ones are Stream.of
and Stream.empty
.
Stream.generate
and IntStream.rangeClosed
are also classified into this.
The point is that the operation of creating a Stream or collection is an expansion.
Like you, I always want to replace the for statement with the Stream API.
However, in reality, for is still active, and unfortunately it is not the case when asked if we should replace everything with the Stream API.
Here, I'd like to take a side trip and point out some problems with Java's Stream API.
This is the hardest part of using the Stream API personally.
I think there are many people who seemed to be able to write neatly using Stream, but were worried about rewriting for because they wanted to call a function with throws
in the middle.
However, it is not beautiful to wrap it in RuntimeException
once and peel it off later.
There is a technique called Sneaky throw [^ 3] as a solution, but this is also a little hesitant to use. [^ 4]
[^ 3]: Reference: "Sneaky Throws" in Java https://www.baeldung.com/java-sneaky-throws [^ 4]: You can also use Lombok annotations if you are allowed to use them https://projectlombok.org/features/SneakyThrows
public static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
throw (E) e;
}
Streams are nested when trying to run multiple loops in Stream. Originally, multiple loops are a complicated process, and nesting itself is the same for for, but the difficulty of reading when a Stream is nested is considerable.
var list = List.of(1, 2, 3);
var result = list.stream().flatMap(x -> {
return list.stream().map(y -> {
return x * y;
});
})
.collect(Collectors.toList());
Do you know what you are doing at a glance? In this example, for may seem rather simple.
var list = List.of(1, 2, 3);
var result = new ArrayList<Integer>();
for (var x : list) {
for (var y : list) {
result.add(x * y);
}
}
This issue isn't a Java or Stream API issue, it's just that it's hard to read if higher-order functions are nested. There is no solution to this in Java today, and it seems reasonable to use for as before when multiple loops are needed.
I mentioned earlier that flatMap
is deeply linked to multiple loops, and that flatMap
is important in functional languages.
And problems that are difficult to read when higher-order functions are nested seem to occur regardless of paradigm.
So how does a functional language solve this problem?
In fact, functional languages solve this problem using syntactic sugar. For example, in Scala's for statement, you can write the same multiple loop as above as below.
val list = 1 to 3
val result = for (x <- list; y <- list) yield {
x * y
}
The nest is gone.
Scala for is desalted into calls to flatMap
and map
.
That is, the Scala code above is equivalent to the code below.
val list = 1 to 3
val result = list.flatMap { x =>
list.map { y =>
x * y
}
}
You can see that this is exactly the same as the code that makes full use of flatMap
and map
written in Java's Stream API.
In this way you can (visually) eliminate nesting of higher-order functions.
In this article, I introduced the classification and properties of Stream API with the purpose of "Understanding Stream API better". I hope you can think that you have gained a new perspective and learning.
If you don't mind, please take a look at other articles. Works Human Intelligence Advent Calendar 2020 that embodies Develop fun!
Recommended Posts