This is the Java Stream API, introduced in Java 8 - not the I/O package streams.
Not a tutorial - just some examples.
Filter Map Reduce
Example (taken from the package documentation):
Here we use widgets, a Collection<Widget>
, as a source for a stream, and then perform a filter-map-reduce on the stream to obtain the sum of the weights of the red widgets:
Java
1
2
3
4
|
int sum = widgets.stream()
.filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
|
Group by a Key
Convert a vararg list of Map<Integer, MyObject>
s to Map<Integer, Set<MyObject>>
:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.Collectors;
...
Map<Integer, Set<String>> collected = Stream.of(
Map.entry(123, "foo"),
Map.entry(123, "bar"),
Map.entry(234, "baz"))
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(
Map.Entry::getValue, Collectors.toSet()
)
));
|
The classifier in the above groupingBy
is simply the key of the source map: Map.Entry::getKey
. This determines which group each element will belong to.
Then there is a downstream mapping: Collectors.mapping(...)
. This controls how the elements are transformed before being saved: each input value is placed in a set.
The result:
{234=[baz], 123=[bar, foo]}
The above example used groupingBy()
. There is also groupingByConcurrent()
which performs the same grouping but collects its data into a ConcurrentHashMap
instead of HashMap
:
Not at all relevant in my small example above - but could be, for larger data sets where concurrent/parallel execution across multiple threads may make a performance difference.
Group by Two Fields
Nested groupings populate the nested map:
Java
1
2
3
4
|
final Map<Employee.Gender, Map<Integer, List<Employee>>> groupByGenderAndAge = employees.stream()
.collect(groupingBy(Employee::getGender,
groupingBy(Employee::getAge))
);
|
Sort a List/Array
Natural ordering is assumed (no comparator used).
For a list of objects:
Java
1
2
3
4
5
6
|
public static <T> List<T> sortList(List<T> unsortedList) {
return unsortedList
.stream()
.sorted()
.collect(Collectors.toList());
}
|
For an array of primitives:
Java
1
2
3
4
|
return Arrays.stream(unsortedArray)
.boxed()
.sorted()
.collect(Collectors.toList());
|
Count Items in a List
This includes distinct()
, so it provides a count of unique values:
Java
1
|
items.stream().distinct().count();
|
Get List of Fields from a List
We start with a list of objects - in this case courses, where each course has a name and a class room number:
Java
1
2
3
4
|
List<Course> courses = new ArrayList<>();
courses.add(new Course("Math", "123"));
courses.add(new Course("Physics", "456"));
courses.add(new Course("Chemistry", "789"));
|
Then we build a new list containing just the room numbers. In this example, we then count the number of distinct room numbers:
Java
1
2
3
4
5
6
7
8
|
import static java.util.stream.Collectors.toList;
...
long roomsCount = courses.stream()
.map(Course::getClassRoom) // get the room name from each course
.collect(toList()) // build a list of these room names
.stream().distinct().count(); // count the number of unique room names
|
Stringify a List
Create a comma-separated list of items in a String, from data in a List:
Java
1
2
|
String sorted = myList.stream().map(item -> item.getItemName())
.collect(Collectors.joining(", "));
|
Check Items in a Collection
Three examples of the same basic thing:
Traditional for loop
The traditional way using a for()
loop:
Java
1
2
3
4
5
6
7
8
|
boolean nameMatch = Boolean.FALSE;
for (ContentType ct : contentTypes) {
if (ct.getContentTypeName().equals("movie")) {
nameMatch = Boolean.TRUE;
break;
}
}
assertThat(nameMatch).isTrue();
|
Stream forEachOrdered
Using a filter .forEach()
:
Java
1
2
3
4
5
6
|
contentTypes
.stream()
.filter(ct -> ct.equals(expectedType))
.forEachOrdered((ct) -> {
assertThat(ct.getContentTypeName()).isEqualTo("movie");
});
|
Stream ifPresentOrElse
Using a filter .ifPresentOrElse()
Java
1
2
3
4
5
6
7
8
9
10
11
12
|
contentTypes
.stream()
.filter(ct -> ct.equals(expectedType))
.findFirst()
.ifPresentOrElse(
value -> {
assertThat(value.getContentTypeName())
.isEqualTo("movie");
},
() -> {
assertThat(false);
});
|
At this point I am missing the simplicity of the traditional syntax.
Map of Lists to Set
Convert a Map<String, List<String>>
to Set<String>
.
Traditional forEach()
Java
1
|
map.values().forEach(l -> l.forEach(stringValues::add));
|
Stream flatMap()
Java
1
2
3
4
|
Set<String> stringValues = map.values()
.stream()
.flatMap(List::stream)
.collect(Collectors.toCollection(HashSet::new));
|
Here, flatMap()
flattens the nested list inside the map.
Process Map of Maps
Convert text in the following structure to upper case:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Map<String,Map<String,String>> input = Map
.of("abc",
Map.of("Def", "Ghi"),
"jkl",
Map.of("MNO", "PQR", "stu", "vwx")
);
Map<String,Map<String,String>> output = input.entrySet().stream()
.collect(Collectors.toMap(
e1 -> e1.getKey().toUpperCase(),
e1 -> e1.getValue().entrySet().stream().collect(Collectors.toMap(
e2 -> e2.getKey().toUpperCase(),
e2 -> e2.getValue().toUpperCase()))));
System.out.println(input);
System.out.println(output);
|
Results:
{abc={Def=Ghi}, jkl={stu=vwx, MNO=PQR}}
{ABC={DEF=GHI}, JKL={STU=VWX, MNO=PQR}}
Note: The code will fail with IllegalStateException: Duplicate key if there are 2 keys that will become the same when uppercased.
Custom Sorting
Start with this:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
|
List<Map<String,String>> list = new ArrayList<>();
Map<String,String> map = new HashMap<>();
Map<String,String> map1 = new HashMap<>();
Map<String,String> map2 = new HashMap<>();
map.put("productNumber", "107-001");
map1.put("productNumber", "108-001");
map2.put("productNumber", "109-001");
map.put("price", "1.99");
map1.put("price", "1.02");
map2.put("price", "1.99");
list.add(map);
list.add(map1);
list.add(map2);
|
Objective: Sort by key1 in descending order, key2 ascending order.
price |
productNumber |
1.99 |
107-001 |
1.99 |
109-001 |
1.02 |
108-001 |
The following uses comparator chaining:
Java
1
2
3
4
|
Comparator<Map<String, String>> comp = Comparator
.comparing(m ->Double.parseDouble(m.get("price"))
,Comparator.reverseOrder());
comp = comp.thenComparing(m -> m.get("productNumber"));
|
Apply it as follows:
Java
1
2
3
4
5
6
|
List<Map<String, String>> formattedResult =
list.stream().sorted(comp)
.collect(Collectors.toList())
formattedResult.forEach(m -> System.out.println(
m.get("price") + " : " + m.get("productNumber")));
|
Compute If Absent
Assume we have a set of client IDs. Each ID can have many invoices, each invoice generated at a specific datetime.
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
private void doComputeIfAbsent() {
int id = 0;
Invoice invoice = new Invoice();
LocalDateTime date = LocalDateTime.now();
HashMap<Integer, HashMap<LocalDateTime, Invoice>> allInvoicesAllClients = new HashMap();
HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.get(id);
// sub-optimal:
if (allInvoices != null) {
allInvoices.put(date, invoice); //<---REPEATED CODE
} else {
allInvoices = new HashMap<>();
allInvoices.put(date, invoice); //<---REPEATED CODE
allInvoicesAllClients.put(id, allInvoices);
}
// If id isn't present as a key in allInvoicesAllClients, then
// it'll create mapping from id to a new HashMap and return the
// new HashMap. If id is present as a key, then it'll return the
// existing HashMap. See:
// https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Map.html#computeIfAbsent(K,java.util.function.Function)
allInvoicesAllClients.computeIfAbsent( id, k -> new HashMap() ).put(date, invoice);
}
|
Get Last n Entries
Sort a linked hash map in reverse order by value, in order to get the last (i.e. now the first) n entries:
Java
1
|
Map<String, Double> indDistance = new LinkedHashMap<String, Double>();
|
The comparator:
Java
1
2
3
4
5
|
public class ReverseDoubleComparator implements java.util.Comparator<Double> {
public int compare(Double d1, Double d2) {
return (d1.compareTo(d2)) * (-1);
}
}
|
Now use this Comparator in sorted() method, i.e.:
Java
1
2
3
4
|
Map sortedInds =
indDistance.entrySet().stream()
.sorted(Entry.comparingByValue(new ReverseDoubleComparator()))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
|
Frequency Count (1)
Java
1
2
3
4
5
6
7
|
List<String> wordsList = Arrays.asList("foo", "bar", "foo", "baz", "bar");
Map<String, Long> freqMap = wordsList.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting())
);
System.out.println(freqMap);
|
Output: {bar=2, foo=2, baz=1}
Notes:
In the above groupingBy
:
- the first parameter is a function - in this case
identity()
, so that the input is returned as the output. In other words, each word in the list is what we group by.
- the second parameter is the “classifier” - it maps the input elements to keys.
The classifier here is Collectors.counting()
, which is a special case of Collectors.reducing()
- specifically:
Java
1
|
Collectors.reducing(0L, e -> 1L, Long::sum)
|
This maps each element e
to the number 1
and then sums it (with 0
as the default).
Frequency Count (2)
Java
1
2
3
4
5
6
|
List<String> wordsList = Arrays.asList("foo", "bar", "foo", "baz", "bar");
Map<String, Integer> freqMap2 = new HashMap<>();
wordsList.forEach(word
-> freqMap2.compute(word, (k, v) -> v != null ? v + 1 : 1)
);
System.out.println(freqMap2);
|
This uses a more generic way of counting, using Map.compute()
. In this example, the following…
Java
1
|
(k, v) -> v != null ? v + 1 : 1
|
…is the “remapping” function, used to compute each value in the end-result map.
The output is the same as for the previous frequency count example: {bar=2, foo=2, baz=1}
Group a List, Get the Min of Each Group
Test data:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import java.time.Instant;
public class CarDetails {
private final String model;
private final Instant createdDate;
public CarDetails(String model, Instant createdDate) {
this.model = model;
this.createdDate = createdDate;
}
// getters
}
|
Java
1
2
3
4
5
6
7
8
9
|
List<CarDetails> cars = new ArrayList<>();
CarDetails cd1 = new CarDetails("Ford", Instant.ofEpochSecond(1680742800));
CarDetails cd2 = new CarDetails("Ford", Instant.ofEpochSecond(1680746400));
CarDetails cd3 = new CarDetails("Honda", Instant.ofEpochSecond(1680746400));
CarDetails cd4 = new CarDetails("Honda", Instant.ofEpochSecond(1680742800));
cars.add(cd1);
cars.add(cd2);
cars.add(cd3);
cars.add(cd4);
|
The streams:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Map<String, CarDetails> reducedCars = cars.stream()
.collect(Collectors.groupingBy(CarDetails::getModel))
.entrySet().stream().collect(
Collectors.toMap(
(entry) -> entry.getKey(),
(entry) -> entry.getValue().stream()
.min(Comparator.comparing(CarDetails::getCreatedDate))
.orElseThrow(NoSuchElementException::new)
));
for (Map.Entry<String, CarDetails> entry : reducedCars.entrySet()) {
System.out.println(entry.getKey() + " - " + entry.getValue().getCreatedDate());
}
|
In the above code, we use orElseThrow(...)
. If we did not include this step, we would need to use Optional
:
Java
1
|
Map<String, Optional<CarDetails>> reducedCars
|
Results:
Ford - 2023-04-06T01:00:00Z
Honda - 2023-04-06T01:00:00Z
To Stream or Not to Stream
When not to use a Stream - when it’s simpler:
Java
1
|
yourMap.entrySet().removeIf(k -> someRemovalCheck());
|
…compared to this:
Java
1
2
3
4
|
Map<Integer, String> map = new HashMap<>(Map.of(1, "One", 2, "Two", 3, "Three"));
System.out.println(map);
map.values().removeIf("Three"::equals);
System.out.println(map);
|