Java Stream map operation

Introduction

We use the Java Stream map operation to map an element in a stream to a different value (of the same type or different type). This post explains the map operation on a Java Stream in detail. We will also explore the special map operations specific to the primitive specializations (i.e., for primitive int, long and double).

Java Stream map operation

The map operation on a Java Stream (of type Stream<T>) is an intermediate operation. It takes a Function<T, R> (ignoring generics) as an argument. It applies this function to each element of the stream and returns a new stream (Stream<R>) consisting of the results of applying the function.

The method signature is shown below.

<R> Stream<R> map(Function<? super T,? extends R> mapper)

Note that the mapper has to be a non-interfering and a stateless function.

Java Stream map – A simple example

Let us start with a simple example where we map an element in a stream to the same type. Then we will see an example of mapping to a different type.

Mapping a stream element to the same type

In the below code, we have a list of fruit names (Note: The List.of method was introduced in JDK 9. You can learn more about it and other factory methods added in Collections in the Convenience Factory Methods for Collections post).

From the list, we create a stream and use the map operation on it. We pass a lambda expression to convert each fruit name to a string which prefixes the fruit name with the string “Fruit Name: “. Thus the passed mapper is a Function<String, String>. Finally, we use the collect operation and using Collectors.toList we collect all the mapped (or converted) fruit names as a list.

List<String> fruits = List.of("apple", "fig", "grapes", "orange");
List<String> prefixedFruitNames = fruits.stream()
        .map(fruit -> "Fruit Name: " + fruit)
        .collect(Collectors.toList());
prefixedFruitNames.forEach(System.out::println);

The resultant list will have each fruit name prefixed with the string Fruit Name: as shown below.

Fruit Name: apple
Fruit Name: fig
Fruit Name: grapes
Fruit Name: orange

Mapping a stream element to a different type

Let us now use the same stream (Stream<String>) and map each element to a different type.

Here, from the stream created out of the fruits list, we use a method reference to map each string (fruit name) to an Integer which is the length of the string. Then we will get back a Stream of Integer (Stream<Integer>) as a result. Finally, we collect the Integer values into a list.

Note: If we want to write the method reference as a lambda expression, we would write it as fruit -> fruit.length().

The output will be a list of integer where each value is the length of the corresponding fruit at that index.

List<Integer> fruitLengths = fruits.stream()
        .map(String::length) //.map(fruit -> fruit.length())
        .collect(Collectors.toList());
System.out.println(fruitLengths); //[5, 3, 6, 6]

Java Stream map on a custom type

In this section, we will use Stream#map on a custom type. For that, we will use the below shown Student type.

Note: Since the Student type is a simple data carrier (simple POJO), I’ve modelled it as a Java Record. Records were added as a preview feature in JDK 14 and became a finalized language feature in JDK 16. If you are using a Java version lesser than 14, you can use re-write this as a simple POJO class.

public record Student(int id, String firstName, String lastName, List<Integer> marks) {

Mapping a Student instance to a String value

We will start with a list of student objects, as shown below.

List<Student> students = List.of(
        new Student(1, "John", "Smith", List.of(45, 55, 61)),
        new Student(2, "Michael", "Davis", List.of(81, 88, 91)),
        new Student(3, "Sarah", "Joe", List.of(78, 89, 93))
);

We will create a stream out of this list which will return a Stream<Student>. Then, using the map operation, we will map each student to extract their first name alone. Thus the passed mapper is a Function<Student, String>. At last, as in the previous examples, we collect the result as a list. This will give us a List<String> holding the first name of all students.

List<String> firstNames = students.stream()
        .map(Student::firstName)
        .collect(Collectors.toList());
System.out.println(firstNames); //[John, Michael, Sarah]

Mapping a Student instance to a another custom type

Now let us look at an example of mapping a Student type into another type. For that, we will use another type called SimpleStudent as shown below. It represents a student with an id and name (i.e., has fewer fields than the Student type).

public record SimpleStudent(int id, String name) {
}

We will use the same list of students seen in the previous section. From the Stream<Student>, we use the map on that stream to map each Student value to a SimpleStudent. We do this by creating a new SimpleStudent object for each Student instance. The SimpleStudent’s name will be the concatenation of the student’s first and last name fields. Thus, the mapper type here is Function<Student, SimpleStudent>.

List<SimpleStudent> simpleStudents = students.stream()
        .map(student -> new SimpleStudent(
                student.id(),
                String.join(" ", student.firstName(), student.lastName())))
        .collect(Collectors.toList());
simpleStudents.forEach(System.out::println);

The result is shown below.

Note: This is the default toString representation for a Java Record. If you have a custom POJO, you will have to implement the toString method yourself.

SimpleStudent[id=1, name=John Smith]
SimpleStudent[id=2, name=Michael Davis]
SimpleStudent[id=3, name=Sarah Joe]

More examples for Stream#map

More complex mapping logic (as Lambda Expression)

Let us look at an example where the mapping logic is more complicated.

Let us say we have the students data as a string as shown below (say it could be coming from a file). When visualizing the individual columns where the columns are separated by a space, the first column is the student id followed by the student’s name (first and last name) and the marks. Now we have to parse this string list and build a Student instance for each line.

List<String> data = List.of(
        "1 John Smith [45,55,61]",
        "2 Michael Davis [81,88,91]",
        "3 Sarah Joe [78,89,93]"
);

Once we have a stream created out of this list, the main logic is within the map operation. First, we split each line on space (” “) to get the individual components from this data as a String[]. Then the first element (0th index) is the id. The third index (4th component) has the marks data. We remove the opening and closing brackets (’[’ and ‘]’) from the string. Then, we split this resultant string on comma (,) and pass it (of type String[]) to Arrays.stream. We use a map on this stream to map each string value into an integer using the method reference Integer::parseInt before collecting them into a list. Finally, we build the Student object with all the processed components.

List<Student> students = data.stream()
          .map(line -> {
              String[] parts = line.split(" ");
              int id = Integer.parseInt(parts[0]);
              String marksAsString = parts[3].substring(1, parts[3].length() - 1); // remove '[' and ']'

              List<Integer> marks = Arrays.stream(marksAsString.split(","))
                      .map(Integer::parseInt)
                      .collect(Collectors.toList());
              return new Student(id, parts[1], parts[2], marks);
          })
          .collect(Collectors.toList());
students.forEach(System.out::println);

When we print the built Student objects list, we get the below result.

Student[id=1, firstName=John, lastName=Smith, marks=[45, 55, 61]]
Student[id=2, firstName=Michael, lastName=Davis, marks=[81, 88, 91]]
Student[id=3, firstName=Sarah, lastName=Joe, marks=[78, 89, 93]]

Abstracting the complex mapping logic (with a helper method)

In the above example, since the mapping logic (the lambda expression) is big, we can move it to a private helper method to make the code more readable.

private Student buildStudent(String line) {
    String[] parts = line.split(" ");
    int id = Integer.parseInt(parts[0]);
    String marksAsString = parts[3].substring(1, parts[3].length() - 1); // remove '[' and ']'

    List<Integer> marks = Arrays.stream(marksAsString.split(","))
            .map(Integer::parseInt)
            .collect(Collectors.toList());
    return new Student(id, parts[1], parts[2], marks);
}

Now we can just call the helper method to map each line from the input (i.e., each element in the input stream) to a Student object using this method.

List<String> data = List.of(
        .... //sample data as we had before
);
List<Student> students = data.stream()
        .map(this::buildStudent)
        .collect(Collectors.toList());

students.forEach(System.out::println);

Here, we have the method reference this::buildStudent to make use of the helper method to map each line (String) to a Student object. When written as a lambda expression, it would look like – line -> buildStudent(line). The final result will be the same as seen in the previous section.

Java Stream#map specialization for primitives

Let us now look into special map operations which are specific to the three primitives i.e., primitive int, long and double. These are mapToInt, mapToLong and mapToDouble.

Stream#mapToInt

The mapToInt takes a ToIntFunction which is a function to convert a value (of any type) to a primitive integer. It applies the passed function (ToIntFunction) to the elements of the stream and returns an IntStream which has the results of applying the mapping function.

The method signature is,

IntStream mapToInt(ToIntFunction<? super T> mapper);

Let us look at a couple of examples.

In one of the earlier examples, we used the map operation to map each fruit name to its length. Since this mapping operation returns a primitive int value, we can use mapToInt to return an IntStream.

In the below example, from the stream created out of the list, we use the Stream#mapToInt. The function is a lambda expression which converts each string to its length (a primitive int). The result is thus an IntStream. We call the sum() on this IntStream to get the sum of lengths of all elements in the stream.

List<String> fruits = List.of("apple", "fig", "grapes", "orange");
int sum = fruits.stream()
        .mapToInt(String::length)
        .sum();
System.out.println(sum); //20

Let us look at another example. We have a list of integers (List<Integer>). Let us use mapToInt to unbox each Integer object to primitive int to get back an IntStream. On this IntStream, we call the average() which returns an OptionalDouble. Using orElse on an OptionalDouble will give us the double value (the average).

List<Integer> integers = List.of(45, 52, 81, 83);
double avg = integers.stream()
        .mapToInt(Integer::intValue)
        .average()
        .orElse(0);
System.out.println(avg); //65.25

Stream#mapToLong

The Stream#mapToLong takes a ToLongFunction (which is a function or mapper of any value to primitive long) and returns a LongStream.

Here, we use mapToLong with Integer::longValue method reference to map each Integer to a primitive long value.

List<Integer> integers = List.of(45, 52, 81, 83);
integers.stream()
        .mapToLong(Integer::longValue)
        .forEach(System.out::println);

Prints,

45
52
81
83

Stream#mapToDouble

The Stream#mapToDouble takes a ToDoubleFunction (which is a mapper of any value to primitive double) and returns a DoubleStream.

In the below example, we have a list of students data. In the Stream<Student>, for each student it computes the average of all scores for that student. For this, it uses mapToDouble to map each Student to a primitive double value to get back a DoubleStream. Note that within the logic (lambda expression) of the mapToDouble, it uses mapToInt to find the average. Finally, it prints each of the values of the resultant DoubleStream (i.e., average score of each student).

List<Student> students = List.of(
        new Student(1, "John", "Smith", List.of(45, 55, 61)),
        new Student(2, "Michael", "Davis", List.of(81, 88, 91)),
        new Student(3, "Sarah", "Joe", List.of(78, 89, 93))
);
students.stream()
        .mapToDouble(student -> student.marks()
                .stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0))
        .forEach(System.out::println);

Prints,

53.666666666666664
86.66666666666667
86.66666666666667

Conclusion

In this post, we learnt about the Java Stream map operation with examples. We also looked at three special map operations for operating on primitive values (mapToInt, mapToLong and mapToDouble).

Leave a Reply