Skip to content

Commit ea90b21

Browse files
committed
more streams
1 parent 7e9bd96 commit ea90b21

File tree

7 files changed

+177
-7
lines changed

7 files changed

+177
-7
lines changed

src/SUMMARY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -945,13 +945,13 @@ TODO: Wait for hermetic java
945945

946946
# Data Types X
947947

948-
- [Streams 🚧](./streams.md)
949-
- [Purpose](./streams/purpose.md)
948+
- [Streams](./streams.md)
950949
- [stream](./streams/stream.md)
951950
- [map](./streams/map.md)
952951
- [filter](./streams/filter.md)
952+
- [Terminal Operations](./streams/terminal_operations.md)
953953
- [Collectors](./streams/collectors.md)
954-
- [toList](./streams/toList.md)
954+
- [Purpose](./streams/purpose.md)
955955
- [Challenges](./streams/challenges.md)
956956
<!-- - [Regular Expressions 🚧]()
957957
- [Strings III 🚧]()

src/streams/collectors.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,80 @@
11
# Collectors
2+
3+
The most common kind of terminal operation you will perform on a stream
4+
is "collecting" elements into a new collection.
5+
6+
For this you use the `.collect` method along with an implemention of the
7+
`Collector` interface.
8+
9+
```java
10+
~void main() {
11+
List<String> roles = List.of("seer", "clown", "nightmare");
12+
13+
Function<String, Integer> countVowels = s -> {
14+
int vowels = 0;
15+
for (int i = 0; i < s.length(); i++) {
16+
char c = s.charAt(i);
17+
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
18+
vowels++;
19+
}
20+
}
21+
return vowels;
22+
};
23+
24+
List<Integer> vowelCounts = roles.stream()
25+
.map(countVowels)
26+
.collect(Collectors.toList());
27+
28+
IO.println(vowelCounts);
29+
~}
30+
```
31+
32+
There are implementations available as static methods on the `Collectors` class for
33+
collecting into `List`s, `Set`s, and even `Map`s.
34+
35+
Because collecting into specifically a `List` is so common, there is
36+
also a `.toList()` method directly on `Stream` that serves as a shortcut
37+
for `.collect(Collectors.toUnmodifiableList())`.
38+
39+
```java
40+
~void main() {
41+
List<String> roles = List.of("seer", "clown", "nightmare");
42+
43+
Function<String, Integer> countVowels = s -> {
44+
int vowels = 0;
45+
for (int i = 0; i < s.length(); i++) {
46+
char c = s.charAt(i);
47+
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
48+
vowels++;
49+
}
50+
}
51+
return vowels;
52+
};
53+
54+
// There is also Collectors.toUnmodifiableList
55+
List<Integer> vowelCountsList = roles.stream()
56+
.map(countVowels)
57+
.collect(Collectors.toList());
58+
59+
IO.println(vowelCountsList);
60+
61+
vowelCountsList = roles.stream()
62+
.map(countVowels)
63+
.toList();
64+
IO.println(vowelCountsList);
65+
66+
// ...and Collectors.toUnmodifiableSet()
67+
Set<Integer> vowelCountsSet = roles.stream()
68+
.map(countVowels)
69+
.collect(Collectors.toSet());
70+
IO.println(vowelCountsSet);
71+
72+
// ...and Collectors.toUnmodifiableMap
73+
Map<String, Integer> vowelCountsMap = roles.stream()
74+
.collect(Collectors.toMap(
75+
s -> s,
76+
s -> countVowels.apply(s)
77+
));
78+
IO.println(vowelCountsMap);
79+
~}
80+
```

src/streams/filter.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
# filter
22

3-
To drop elements from a `Stream` you can use `.filter`.
3+
With a stream of elements you can also drop the
4+
elements of the stream as they flow by[^metaphor] with `.filter`.
45

56
`.filter` will test each element with a predicate. If the
67
predicate returns `true` the element will be retained.
78
If it returns `false` the element will be dropped.
89

10+
11+
```java,no_run
12+
var numbers = List.of("1", "2", "3");
13+
14+
Stream<Integer> numberStream = numbers.stream()
15+
.map(Integer::parseInt) // 1, 2, 3
16+
.filter(x -> x % 2 == 1); // 1, 3
17+
```
18+
19+
[^metaphor]: In the real life stream metaphor, this is akin to rocks stuck along the way and not
20+
continuing to go with the flow of water.

src/streams/map.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ elements of the stream as they flow by[^metaphor] with `.map`.
66
`.map` applies a `Function` to the elements of the stream
77
one by one and returns you a new `Stream` containing the new elements.
88

9-
```java
9+
```java,no_run
1010
var numbers = List.of("1", "2", "3");
1111
1212
Stream<Integer> numberStream = numbers.stream()

src/streams/purpose.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,42 @@
22

33
The purpose of streams is to "de-couple"
44
the source of data, the transformations to apply on that
5-
data, and the final shape you want that data in.
5+
data, and the final shape you want that data in.
6+
7+
This serves one somewhat holistic goal and one practical one.
8+
9+
The holistic goal is that you are "declaring what you want done"
10+
as opposed to "expressing how you want something done."
11+
This is often referred to as "declarative" vs "imperative" programming.
12+
13+
Compare these two bits of code. They both add one to every number in a list.
14+
15+
```java,no_run
16+
List<Double> doubles = List.of(1.5, 2.5, 3.9);
17+
18+
List<Double> newDoubles = new ArrayList<>();
19+
for (double d : doubles) {
20+
newDoubles.add(d + 1);
21+
}
22+
```
23+
24+
```java
25+
List<Double> doubles = List.of(1.5, 2.5, 3.9);
26+
27+
List<Double> newDoubles = doubles.stream()
28+
.map(d -> d + 1)
29+
.toList();
30+
```
31+
32+
You can argue that the second code snippet more transparently "reflects the intent"
33+
than the first one. The mechanics of looping and adding to a new list are
34+
hidden and the only things on screen are `doubles.stream()` (using this collection as a source),
35+
`.map(d -> d + 1)` (apply this transformation), and `.toList()` (put the results in a list.)
36+
37+
This has its upsides and downsides. Part of the trouble is that there isn't a good rule of
38+
thumb as to when you should use a regular loop or use a stream - it often comes down to
39+
personal taste and other "soft" factors.
40+
41+
The mechanical reason for streams is that, because the operations to perform are separated somewhat
42+
from how to perform them, there are opportunities for Java to be "smart" about how it does them.
43+
The usual example given for this is "parallel streams," which we can get into eventually.

src/streams/terminal_operations.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Terminal Operations
2+
3+
We call `.map`, `.filter`, and methods like them "intermediate operations."
4+
This is because they run "in the middle" of the entire process.
5+
6+
For consuming a stream you use "terminal operations." Terminal operations
7+
"consume" the stream and produce some result.
8+
9+
The simplest terminal operation is `.forEach`. It consumes the entire stream and does
10+
something for each element in the flow.
11+
12+
```java,no_run
13+
~void main() {
14+
List<String> cities = List.of(
15+
"St. Louis", "Dallas", "London", "Tokyo"
16+
);
17+
18+
cities.stream()
19+
.filter(city -> !city.startsWith("S"))
20+
.forEach(IO::println); // Dallas, London, Tokyo
21+
~}
22+
```
23+
24+
Once a terminal operation has been performed the stream is no longer
25+
usable.
26+
27+
```java
28+
~void main() {
29+
List<String> cities = List.of(
30+
"St. Louis", "Dallas", "London", "Tokyo"
31+
);
32+
33+
Stream<String> citiesStream = cities.stream()
34+
.filter(city -> !city.startsWith("S"));
35+
36+
// Dallas, London, Tokyo
37+
citiesStream.forEach(IO::println);
38+
39+
// java.lang.IllegalStateException: stream has already been operated upon or closed
40+
citiesStream.forEach(IO::println);
41+
~}
42+
```

src/streams/toList.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)