How to use the Collectors.toUnmodifiableList() and other new Collectors in Java 10

In Java 10, a significant enhancement was introduced to the java.util.stream.Collectors class: new utility methods to create unmodifiable collections such as lists and sets. One notable method is Collectors.toUnmodifiableList(). This method allows you to efficiently create immutable lists during stream processing, adding to the immutability features provided by Java 9 and earlier versions.

Here’s how you can use Collectors.toUnmodifiableList() and other similar methods introduced in Java 10:


1. Using Collectors.toUnmodifiableList()

The Collectors.toUnmodifiableList() collector creates an unmodifiable list from a stream of elements. This means the resulting list cannot be modified (no adding, removing, or updating elements). If you attempt to modify it, a runtime exception (UnsupportedOperationException) will be thrown.

Example:

package org.kodejava.util.stream;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableList {
    public static void main(String[] args) {
        // Example list using Collectors.toUnmodifiableList
        List<String> unmodifiableList = Stream.of("A", "B", "C")
                .collect(Collectors.toUnmodifiableList());

        System.out.println("Unmodifiable List: " + unmodifiableList);

        // Attempt to modify the list will throw UnsupportedOperationException
        unmodifiableList.add("D"); // This will throw a runtime exception!
    }
}

Output:

Unmodifiable List: [A, B, C]
Exception in thread "main" java.lang.UnsupportedOperationException

2. Other Collectors Introduced in Java 10

Java 10 introduced two other collectors similar to toUnmodifiableList():

  • Collectors.toUnmodifiableSet()
    • Creates an unmodifiable set from a stream of elements.
    • Duplicate elements will be removed since it’s a set.

Example:

package org.kodejava.util.stream;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableSet {
    public static void main(String[] args) {
        Set<String> unmodifiableSet = Stream.of("A", "B", "C", "A") // "A" will appear only once
                .collect(Collectors.toUnmodifiableSet());
        System.out.println("Unmodifiable Set: " + unmodifiableSet);

        unmodifiableSet.add("D"); // Throws UnsupportedOperationException
    }
}
  • Collectors.toUnmodifiableMap()
    • Creates an unmodifiable map using key-value pairs from a stream.
    • Requires a way to specify the key and value in the collector.
    • If duplicate keys are produced, it will throw an IllegalStateException.

Example:

package org.kodejava.util.stream;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class UnmodifiableMap {
    public static void main(String[] args) {
        Map<Integer, String> unmodifiableMap = Stream.of("A", "B", "C")
                .collect(Collectors.toUnmodifiableMap(
                        String::length,  // Key mapper
                        v -> v           // Value mapper
                ));

        System.out.println("Unmodifiable Map: " + unmodifiableMap);

        // Attempting to modify will throw an UnsupportedOperationException
        unmodifiableMap.put(2, "D"); // Throws UnsupportedOperationException
    }
}

3. Behavior of Unmodifiable Collections

  • These collectors guarantee that:
    • The collection cannot be modified (no add, remove, put, etc.).
    • Any attempt to modify them results in an UnsupportedOperationException.
    • They are safe to use for read-only purposes.
  • If the stream itself contains null values, a NullPointerException will be thrown.

4. Best Uses of toUnmodifiable*() Collectors

  • Ensuring immutability for collections to prevent accidental modifications.
  • Useful in multi-threaded or concurrent applications where immutability eliminates thread-safety issues.
  • Perfect for cases where only read access is required.

5. Comparison with Java 9 List.of()

Java 9 introduced factory methods like List.of(), Set.of(), and Map.of() for creating immutable collections. While those methods are concise, the new collectors offer more flexibility when working with streams.

Java 9 Example:

List<String> immutableList = List.of("A", "B", "C");

Java 10 Stream Example:

List<String> immutableList = Stream.of("A", "B", "C")
                                   .collect(Collectors.toUnmodifiableList());

Summary Table:

Collector Description Introduced in
Collectors.toUnmodifiableList() Creates an unmodifiable List Java 10
Collectors.toUnmodifiableSet() Creates an unmodifiable Set Java 10
Collectors.toUnmodifiableMap() Creates an unmodifiable Map Java 10

Conclusion

The Collectors.toUnmodifiableList() and related methods introduced in Java 10 are powerful tools for creating immutable collections directly from streams. They ensure immutability, improve code safety, and fit well into functional programming paradigms introduced with Java Streams.

How do I use Collectors.maxBy() method?

The Collectors.maxBy() method is used to find the maximum element from a stream based on a certain comparator. It returns an Optional which contains the maximum element according to the provided comparator, or an empty Optional if there are no elements in the stream.

Here’s a simple example where we have a list of integers, and we want to find the biggest integer:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class MaxByDemo {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        Optional<Integer> maxNumber = numbers.stream()
                .collect(Collectors.maxBy(Comparator.naturalOrder()));

        maxNumber.ifPresent(System.out::println);
    }
}

In this example:

  • We create a Stream from the list of integers.
  • We then use Collectors.maxBy(Comparator.naturalOrder()) to get the maximum number. Comparator.naturalOrder() is a shortcut for Comparator.comparing(Function.identity()).
  • Collectors.maxBy() returns an Optional because the stream could be empty.
  • We print the maximum number if it exists.

When you run this program, it will print “5” because 5 is the biggest number in the list.

Keep in mind that if the stream is empty, maxNumber will be an empty Optional, and nothing will be printed.

How do I use Collectors.minBy() method?

The Collectors.minBy() method in Java 8 is used to find the minimum element from a stream of elements based on a certain comparator. It returns an Optional describing the minimum element of the stream, or an empty Optional if the stream is empty.

Here’s an example of how to use Collectors.minBy(). Assume we have a list of integers, and we want to find the smallest element.

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class CollectorsMinBy {
    public static void main(String... args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        Optional<Integer> min = numbers.stream()
                .collect(Collectors.minBy(Integer::compare));

        min.ifPresent(System.out::println);
    }
}

In this code:

  • We have a list of integers.
  • We create a Stream from the list and collect the stream into an Optional that might hold the minimum value via the Collectors.minBy(Integer::compare) collector.
  • Integer::compare is a method reference that is used to instruct Collectors.minBy() on how to compare the integers.
  • min.ifPresent(System.out::println) checks if the Optional has a value. If it does, the value is passed to the System.out::println method and printed to the console.

When run, this program prints the smallest number in our list, which is “1”.

Note that if the list is empty, min will hold an empty Optional, and min.ifPresent(System.out::println) will not print anything.

Here’s another example of how you can use the Collectors.minBy() method to find the object containing the minimum value for a certain property. Let’s assume we have a Person class and a list of Person objects, and we want to find which Person has the smallest age.

package org.kodejava.stream;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class CollectorsMinByObjectProperty {
    public static void main(String... args) {
        List<Person> people = Arrays.asList(
                new Person("Rosa", 21),
                new Person("Bob", 25),
                new Person("Alice", 18),
                new Person("John", 22));

        Optional<Person> youngestPerson = people.stream()
                .collect(Collectors.minBy(Comparator.comparingInt(Person::getAge)));

        youngestPerson.ifPresent(System.out::println);
    }

    static class Person {
        String name;
        int age;

        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "Person{" +
                   "name='" + name + '\'' +
                   ", age=" + age +
                   '}';
        }
    }
}

Output:

Person{name='Alice', age=18}

In this code:

  • The Person class has two fields, name and age, and a getter for the age field.
  • We have a list of Person objects.
  • We create a Stream from the list and then use Collectors.minBy() to find the Person with the smallest age. To do this, we use Comparator.comparingInt(Person::getAge), which compares the Person objects based on their age.
  • Collectors.minBy() returns an Optional that might hold the Person with the smallest age.
  • If such a Person exists, we print that Person using System.out::println.

This program prints: Person{name='Alice', age=18}, as Alice is the person with the smallest age.

How do I use Collectors.counting() method?

The Collectors.counting() method is a terminal operation that returns the count of elements in the particular stream where it is used. This is part of the java.util.stream.Collectors in Java 8.

Here is a simple example of how to use Collectors.counting(). Suppose we have a list of strings, and we want to count the number of elements in it.

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class CollectorsCounting {
    public static void main(String... args) {
        List<String> names = Arrays.asList("Rosa", "Bob", "Alice", "Dave", "John");

        long count = names.stream()
                .collect(Collectors.counting());

        System.out.println("Count: " + count);
    }
}

Output:

Count: 5

In this code:

  • We have a list of names.
  • We create a stream from this list using the .stream() method.
  • We count the elements of the stream using .collect(Collectors.counting()), which returns the number of elements in the stream.
  • Finally, we print the count.

When we run the program, we will get the output “Count: 5”, because there are five elements in the list.

The Collectors.counting() method is often used in conjunction with other methods like Collectors.groupingBy() to perform more complex operations like counting the number of elements in each group.

How do I use Collectors.toCollection() method?

The Collectors.toCollection() method is a static method in the java.util.stream.Collectors class of Java 8. This method is used with streams when you want to convert a list to another collection type.

Here’s a simple example of how to use the method:

package org.kodejava.stream;

import java.util.*;
import java.util.stream.Collectors;

public class CollectorsToCollection {
    public static void main(String[] args) {
        List<String> list = 
                Arrays.asList("Java", "Kotlin", "Python", "Scala", "Kotlin");

        // Convert List to TreeSet
        TreeSet<String> treeSet = list.stream()
                .collect(Collectors.toCollection(TreeSet::new));

        System.out.println(treeSet);
    }
}

Output:

[Java, Kotlin, Python, Scala]

In this code:

  • We have a List of Strings.
  • We convert this list into a TreeSet.
  • Collectors.toCollection(TreeSet::new) is the collector that collects the data from the stream into a new TreeSet.
  • The method referenced by TreeSet::new is a constructor reference that creates a new empty TreeSet.

The output of the program will be the TreeSet containing the elements of the list.

Keep in mind that a TreeSet automatically orders its elements (in this case, alphabetically since the elements are Strings) and does not allow duplicates. So, if the list had duplicate values, and you wanted to maintain them in your new collection, you would need to choose a different type of Set or use a List.

How do I use Collectors.partitioningBy() method?

Collectors.partitioningBy() is a special case of a grouping collector in Java’s Stream API. It partitions or divides the input elements into two groups, based on the result of a Predicate function. One group for which the Predicate function returns true, and the other where it returns false.

Each group is a List of elements, and the method returns a Map where the keys are Boolean values (true and false), and the values are the resulting groups.

Here’s a simple example:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class OddEvenNumberPartitioning {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Map<Boolean, List<Integer>> isEven = numbers.stream()
                .collect(Collectors.partitioningBy(num -> num % 2 == 0));

        System.out.println("Partition of Even Numbers: " + isEven.get(true));
        System.out.println("Partition of Odd Numbers: " + isEven.get(false));
    }
}

Output:

Partition of Even Numbers: [2, 4, 6, 8, 10]
Partition of Odd Numbers: [1, 3, 5, 7, 9]

In this example, we’re partitioning a list of integers into even and odd numbers. The Predicate function num -> num % 2 == 0 returns true for even numbers and false for odd numbers.

The value that partitioningBy() method returns is a Map where the key true maps to a list of numbers for which the Predicate was true (even numbers), and the key false maps to a list of numbers for which the Predicate was false (odd numbers).

You can use partitioningBy() to easily categorize elements of a stream where the categorization criterion can be represented with a boolean value (i.e., you have a binary condition to divide your elements).

How do I use Collectors.groupingBy() method?

In Java 8, the Collectors.groupingBy() method in the Stream API is used to group elements of the stream into a `Map“ based on a categorization function.

Here’s a basic example:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class CollectorsGroupingBy {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Alice", "Rosa", "Tom", "John");

        Map<String, List<String>> groupedByName = names.stream()
                .collect(Collectors.groupingBy(Function.identity()));

        groupedByName.forEach((name, nameList) -> {
            System.out.println("Name : " + name + " Count : " + nameList.size());
        });
    }
}

Output:

Name : Tom Count : 1
Name : Alice Count : 1
Name : John Count : 2
Name : Rosa Count : 1

In this case, elements of the names list are grouped by their identity (Function.identity() returns a function that returns its input). So the resulting Map has the name as a key, and a list of names as the value. If a name appears more than once in the list, it will appear more than once in the corresponding list in the Map.

groupingBy() can also be used with more complex streams. For example, if you have a stream of Employee objects, and you want to group employees by their department, you can do it like:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class EmployeeGroupingBy {
    public static void main(String[] args) {
        // Create a list of employees
        List<Employee> employees = Arrays.asList(
                new Employee("John", 25, "Finance"),
                new Employee("Sarah", 28, "Marketing"),
                new Employee("Tom", 35, "IT"),
                new Employee("Rosa", 30, "Finance"),
                new Employee("Sam", 24, "IT"));

        // Group the employees by their department
        Map<String, List<Employee>> employeesByDepartment = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment));

        System.out.println(employeesByDepartment);
    }
}

class Employee {
    private final String name;
    private final int age;
    private final String department;

    public Employee(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getDepartment() {
        return department;
    }

    @Override
    public String toString() {
        return "name='" + name + "'";
    }
}

Output:

{Finance=[name='John', name='Rosa'], IT=[name='Tom', name='Sam'], Marketing=[name='Sarah']}

Collectors.groupingBy() is very flexible and can be used with additional parameters to provide more control over how the grouping is done, including changing the type of Map returned, modifying how the values are collected, or using a secondary groupingBy() call to create a multi-level Map.

In the above example, the groupingBy() method groups the employees by their department. The department field of the Employee object is used as the key of the Map and the value is the list of employees in that department.

The Employee::getDepartment in the groupingBy() is a method reference in Java. It’s equivalent to writing employee -> employee.getDepartment(). The :: is used to reference a method or a constructor in the Java class.

Here, Employee::getDepartment is used as a classifier function that applies to each element in the stream. So groupingBy() distributes elements of the stream into groups according to the value returned by this function.

How do I use Collectors.summarizingInt() method?

The Collectors.summarizingInt method is actually similar to the summaryStatistics we discussed in the previous example. It returns a special class (IntSummaryStatistics) which encapsulates a set of summary statistical values for a stream of integers.

Here’s an example:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;

public class SummarizingIntExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        IntSummaryStatistics stats = numbers.stream()
                .collect(Collectors.summarizingInt(Integer::intValue));

        System.out.println("Highest number in List : " + stats.getMax());
        System.out.println("Lowest number in List  : " + stats.getMin());
        System.out.println("Sum of all numbers     : " + stats.getSum());
        System.out.println("Average of all numbers : " + stats.getAverage());
        System.out.println("Total numbers in List  : " + stats.getCount());
    }
}

Output:

Highest number in List : 10
Lowest number in List  : 1
Sum of all numbers     : 55
Average of all numbers : 5.5
Total numbers in List  : 10

In this case, Collectors.summarizingInt is used as the collector for the stream.collect method. The Integer::intValue method reference is provided to tell it how to convert the stream elements (which are Integer objects in this case) to int values. The stream.collect method aggregates the input elements into a single summary result (the IntSummaryStatistics object).

Similar to summaryStatistics(), Collectors.summarizingInt() also provides corresponding methods for Long (Collectors.summarizingLong) and Double (Collectors.summarizingDouble) values.

How do I use averaging collectors in Java Stream API?

In Java Stream API, there are several collectors you can use to calculate the average of numbers. The purpose of averaging methods or collectors is to aggregate or combine the elements of the stream into a single summary result, which is the average of the elements.

A key feature of the Stream API is its ability to execute computation in parallel, maximizing the use of multicore architectures. In this context, the Collectors.averagingInt(), Collectors.averagingLong(), and Collectors.averagingDouble() methods provide a way to calculate the average in a thread-safe manner, taking full advantage of parallel processing if a parallel stream is used.

Using these methods, you can calculate the average of a large set of numbers without having to manually implement the average calculation or worrying about synchronization in a parallel computation environment.

Here is how you can do it:

  • Average of int numbers

To calculate the average of int numbers, you can use Collectors.averagingInt().

Assuming we have a list of integers named numbers:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

We can calculate the average using the following code:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class AveragingInt {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        Double average = numbers.stream()
                .collect(Collectors.averagingInt(Integer::intValue));

        System.out.println("Average value: " + average);
    }
}
  • Average of long numbers

To calculate the average of long numbers, you can use Collectors.averagingLong().

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class AveragingLong {
    public static void main(String[] args) {
        List<Long> longNumbers = Arrays.asList(1L, 2L, 3L, 4L, 5L);

        Double longAverage = longNumbers.stream()
                .collect(Collectors.averagingLong(Long::longValue));

        System.out.println("Average value: " + longAverage);
    }
}
  • Average of double numbers

To calculate the average of double numbers, you can use Collectors.averagingDouble().

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class AveragingDouble {
    public static void main(String[] args) {
        List<Double> doubleNumbers = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);

        Double doubleAverage = doubleNumbers.stream()
                .collect(Collectors.averagingDouble(Double::doubleValue));

        System.out.println("Average value: " + doubleAverage);
    }
}

The result of these averaging collectors will be a Double variable holding the average value of the numbers.

Here is another example:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ProductPriceAveraging {
    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
                new Product("Product 1", 10.0),
                new Product("Product 2", 20.0),
                new Product("Product 3", 30.0));

        double averagePrice = products.stream()
                .collect(Collectors.averagingDouble(Product::getPrice));

        System.out.println("Average price: " + averagePrice);
    }

}

class Product {
    private final String name;
    private final Double price;

    public Product(String name, Double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public Double getPrice() {
        return price;
    }
}

In this example, the averagingDouble() collector is used to find the average price of all products in the list. The Product::getPrice method reference is used to tell the collector to use the getPrice() method of the Product class to retrieve the value for each product.

Overall, the averaging collectors in Java Stream API provide a concise, thread-safe, and efficient way to calculate averages over stream elements.

How do I use Collectors.joining() method?

The Collectors.joining() method is a handy utility in the java.util.stream.Collectors class that provides a Collector which concatenates input elements from a stream into a String.

Here are three versions of Collectors.joining():

  • joining(): Concatenates the input elements, separated by the empty string.
  • joining(CharSequence delimiter): Concatenates the input elements, separated by the specified delimiter.
  • joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix): Concatenates the input elements, separated by the delimiter, with the specified prefix and suffix.

Let’s see an example of each:

package org.kodejava.stream;

import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CollectorsJoining {
    public static void main(String[] args) {
        // Using joining()
        String joined1 = Stream.of("Hello", "world")
                .collect(Collectors.joining());
        System.out.println(joined1);

        // Using joining(CharSequence delimiter)
        String joined2 = Stream.of("Hello", "world")
                .collect(Collectors.joining(" "));
        System.out.println(joined2);

        // Using joining(CharSequence delimiter, CharSequence prefix,
        // CharSequence suffix)
        String joined3 = Stream.of("Hello", "world")
                .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(joined3);
    }
}

Output:

Helloworld
Hello world
[Hello, world]

In these examples, we use Stream.of() to create a stream of strings, and Collectors.joining() to concatenate them together, with or without delimiters, prefix, and suffix as needed.

Now, let’s consider we have a Person class and a list of Person objects. We want to join the names of all persons. Here is how to do that:

package org.kodejava.stream;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class CollectorsJoiningObjectProperty {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
                new Person("John"),
                new Person("Rosa"),
                new Person("Doe")
        );

        String names = people.stream()
                .map(Person::getName)
                .collect(Collectors.joining(", "));

        System.out.println(names);
    }
}

Output:

John, Rosa, Doe

In the above example, we start with a List of Person objects. We create a stream from the list, then use the map() function to transform each Person into a String (their name). The collect() method is then used with Collectors.joining(), which joins all the names together into one String, separated by commas.