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:

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>>:

 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:

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:

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:

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:

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:

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:

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:

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:

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():

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()

 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()

1
map.values().forEach(l -> l.forEach(stringValues::add));

Stream flatMap()

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:

 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:

 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.

priceproductNumber
1.99107-001
1.99109-001
1.02108-001

The following uses comparator chaining:

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:

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.

 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:

1
Map<String, Double> indDistance = new LinkedHashMap<String, Double>();

The comparator:

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.:

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)

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:

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)

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…

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:

 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
}
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:

 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:

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:

1
yourMap.entrySet().removeIf(k -> someRemovalCheck());

…compared to this:

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);