mapping in Java 8 streams
-
Last Updated: February 24, 2024
-
By: javahandson
-
Series

Learn mapping in Java 8 streams with clear examples of map, mapToInt, mapToLong and mapToDouble, plus pitfalls and interview questions.
Mapping in Java 8 streams is how you turn one stream into another stream of a different shape. You start with a list of employees and end with a list of names. You start with a list of orders and end with a list of totals. Nothing in the original list changes. The Stream API simply builds a fresh stream for you.
This guide walks through all four mapping methods on the Stream interface: map, mapToInt, mapToLong and mapToDouble. We will start with the simplest example possible, then build up to primitive streams, chaining, and a small report program that pulls everything together.
Before Java 8, pulling a field out of a list meant writing a loop. You created an empty list, looped over the source, called a getter, and added the result. Four lines of noise for one simple idea.
The Stream API replaced that whole ritual with a single word: map. You hand it a function, and it hands you back a new stream where every element has passed through that function. The loop disappears, and what stays behind reads almost like a sentence.
Mapping sounds abstract until you see it twice. After that, you will spot it everywhere in real code.
map method, with lambdas and with method referencesmap does nothing until a terminal operation runsmap with filter and with a second mapmapToInt, mapToLong and mapToDoubleboxed() and mapToObj()map differs from flatMapThe word mapping just means transforming one form into another. Give it a number, get back a doubled number. Give it an employee, get back a salary. Same count of elements, different content.
This point matters more than any syntax detail. The map method never edits the source collection. It builds a brand new stream and leaves your list exactly as it was.
So if you map a list of ten prices and forget to collect the result, your prices stay untouched. Nothing broke. You simply threw the new stream away.
That behaviour keeps stream pipelines safe. Two different parts of your program can map the same list without stepping on each other.
Picture a photocopier with a colour filter. You feed in a stack of colour photos. Out comes a stack of black-and-white copies, one per photo. Your original photos come back untouched.
The map method works the same way. Elements go in, transformed elements come out, and the source stays whole.
Every stream pipeline has three parts. A source starts it, intermediate operations shape it, and a terminal operation finishes it.
Stream.of(...)filter, map, sorted, distinct, limitcollect, forEach, count, reduceAll four mapping methods sit squarely in the middle group. They return a stream, so you can keep chaining. New to pipelines? Start with our introduction to streams in Java 8 and come back here.
The map method takes a Function as its argument. Java calls that function once per element and gathers the results into a new stream.
<R> Stream<R> map(Function<? super T, ? extends R> mapper) // T = the type of element going in // R = the type of element coming out // mapper = a stateless function applied to each element // returns = a new stream of R elements
Read the two type letters and the whole signature clicks. A Stream<Student> plus a function from Student to String gives you a Stream<String>.
The word stateless in the docs carries weight. Your mapper should not remember anything between calls, and it should not depend on the order Java processes elements. Break that rule and parallel streams will punish you.
Here is the smallest useful example. We take six numbers and double each one.
package com.javahandson.map;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MapElements {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> doubled = numbers.stream()
.map(number -> number * 2)
.collect(Collectors.toList());
System.out.println("Original : " + numbers);
System.out.println("Doubled : " + doubled);
}
}
// Output: Original : [1, 2, 3, 4, 5, 6]
// Output: Doubled : [2, 4, 6, 8, 10, 12]Look at the two output lines together. The original list survived. The doubled values live in a completely separate list.
Notice the element count too. Six went in, six came out. A map never adds or drops elements, which is exactly what separates it from filter.
Real code rarely maps plain numbers. Far more often you have objects and you want one field from each. Let us set up a small Student class first.
package com.javahandson.map;
public class Student {
private int rollNumber;
private String name;
private char grade;
public Student(int rollNumber, String name, char grade) {
this.rollNumber = rollNumber;
this.name = name;
this.grade = grade;
}
public int getRollNumber() {
return rollNumber;
}
public String getName() {
return name;
}
public char getGrade() {
return grade;
}
}Now we can turn a list of students into a list of names in one line.
package com.javahandson.map;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StudentNames {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student(101, "Suraj", 'C'),
new Student(102, "Iqbal", 'A'),
new Student(103, "Amar", 'B'),
new Student(104, "Amit", 'C'),
new Student(105, "Suchit", 'A'));
List<String> names = students.stream()
.map(student -> student.getName())
.collect(Collectors.toList());
System.out.println("Names : " + names);
}
}
// Output: Names : [Suraj, Iqbal, Amar, Amit, Suchit]The stream started as Stream<Student>. Our function returned a String each time, so the stream became Stream<String>. That type switch is the whole point of mapping.
When your lambda does nothing except call one method, Java offers a shorter form. Replace student -> student.getName() with Student::getName.
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.toList());
System.out.println(names); // Output: [Suraj, Iqbal, Amar, Amit, Suchit]
// Both forms compile to the same thing:
// .map(student -> student.getName())
// .map(Student::getName)Which one should you pick? Use the method reference when the lambda adds no logic of its own. Keep the lambda when you need real work, such as student -> student.getName().toUpperCase().
Our article on method reference in Java 8 covers the four flavours in depth.
Here is a fact that surprises many beginners. Calling map runs your function zero times. Java only walks the pipeline once a terminal operation asks for results.
List<Integer> numbers = Arrays.asList(1, 2, 3);
// No terminal operation, so nothing prints
numbers.stream().map(n -> {
System.out.println("mapping " + n);
return n * 2;
});
System.out.println("--- now add collect ---");
numbers.stream().map(n -> {
System.out.println("mapping " + n);
return n * 2;
}).collect(Collectors.toList());
// Output: --- now add collect ---
// Output: mapping 1
// Output: mapping 2
// Output: mapping 3The first pipeline printed nothing at all. Laziness lets Java skip work you never asked for, and it lets several operations share a single pass over the data.
A single map is handy. Combined with filter or another map, it becomes genuinely expressive.
Suppose we only want the names of grade A students. First we narrow the stream, then we transform what survives.
List<String> toppers = students.stream()
.filter(student -> student.getGrade() == 'A')
.map(Student::getName)
.collect(Collectors.toList());
System.out.println("Grade A : " + toppers);
// Output: Grade A : [Iqbal, Suchit]Read it top to bottom and it almost speaks English. Take the students, keep the A grades, take their names, gather them into a list.
Need more on narrowing a stream? See filtering in streams.
Nothing stops you from mapping twice. Each step changes the stream type again.
List<Integer> nameLengths = students.stream()
.map(Student::getName) // Stream<Student> -> Stream<String>
.map(String::length) // Stream<String> -> Stream<Integer>
.collect(Collectors.toList());
System.out.println(nameLengths); // Output: [5, 5, 4, 4, 6]Two small steps beat one clever step. Each line stays readable, and the comments show the type at every stage.
Put filter before map whenever both apply to the same data. Filtering first shrinks the stream, so the mapper runs fewer times.
filter first, then map – the mapper runs only on surviving elementsmap first, then filter – the mapper runs on every element, even the ones you throw awayWith five students the difference means nothing. With five million rows and a mapper that hits a database, the difference becomes your afternoon.
So far every example produced objects. That works, but for plain numbers it costs more than you might expect.
A Stream<Integer> cannot hold the primitive int value 42. Generics only accept objects, so Java wraps each number in an Integer. That wrapping step carries a name: boxing.
Boxing costs you twice. Each wrapper needs heap memory, and every arithmetic step needs an unboxing call to get the raw value back.
Primitive streams solve both problems. An IntStream stores real int values with no wrapper in sight.
| Method | Function type | Returns |
|---|---|---|
mapToInt | ToIntFunction<T> | IntStream |
mapToLong | ToLongFunction<T> | LongStream |
mapToDouble | ToDoubleFunction<T> | DoubleStream |
Each of those three function types is a functional interface from java.util.function. Our guide to predefined functional interfaces explains the family.
Primitive streams also unlock methods you will not find on a regular stream, such as sum(), average() and summaryStatistics(). That alone justifies learning them.
Use mapToInt whenever your mapper produces a whole number and you plan to do arithmetic with the result.
IntStream mapToInt(ToIntFunction<? super T> mapper) // mapper = a stateless function returning an int for each element // returns = an IntStream of primitive int values
package com.javahandson.map;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
public class MapToIntExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student(101, "Suraj", 'C'),
new Student(102, "Iqbal", 'A'),
new Student(103, "Amar", 'B'));
IntStream rollNumbers = students.stream()
.mapToInt(Student::getRollNumber);
rollNumbers.forEach(roll -> System.out.print(roll + " "));
}
}
// Output: 101 102 103Because getRollNumber() already returns an int, the method reference fits ToIntFunction perfectly. No wrapper objects appear anywhere in this pipeline.
Once you hold an IntStream, the maths methods come along for free.
List<String> words = Arrays.asList("Java", "C++", "Python", "Rust");
int totalChars = words.stream().mapToInt(String::length).sum();
OptionalInt longest = words.stream().mapToInt(String::length).max();
OptionalDouble avg = words.stream().mapToInt(String::length).average();
System.out.println("Total : " + totalChars); // Output: Total : 17
System.out.println("Longest : " + longest.getAsInt()); // Output: Longest : 6
System.out.println("Average : " + avg.getAsDouble()); // Output: Average : 4.25
IntSummaryStatistics stats = words.stream().mapToInt(String::length).summaryStatistics();
System.out.println(stats.getMin() + " to " + stats.getMax()); // Output: 3 to 6Why do max() and average() return optional types? Because an empty stream has no maximum and no average. The optional forces you to handle that case instead of crashing later.
One call to summaryStatistics() gives you count, sum, min, max and average together. Reach for it when you need more than one of them.
The mapToLong method mirrors mapToInt exactly, except it produces a LongStream of 64-bit values.
LongStream mapToLong(ToLongFunction<? super T> mapper) // mapper = a stateless function returning a long for each element // returns = a LongStream of primitive long values
An int stops at 2,147,483,647. Add past that and the value silently wraps around to a negative number. Summing large quantities is exactly where this bites.
List<Integer> bigValues = Arrays.asList(2_000_000_000, 2_000_000_000, 2_000_000_000);
int wrongTotal = bigValues.stream().mapToInt(Integer::intValue).sum();
long rightTotal = bigValues.stream().mapToLong(Integer::longValue).sum();
System.out.println("int sum : " + wrongTotal); // Output: int sum : 1705032704
System.out.println("long sum : " + rightTotal); // Output: long sum : 6000000000The first number looks like nonsense because it is nonsense. Three additions overflowed the int range and wrapped around. Switching to mapToLong fixed it with one word.
Rule of thumb: if the total might exceed roughly two billion, map to long.
Money, averages, percentages and measurements all want decimals. That is what mapToDouble handles.
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) // mapper = a stateless function returning a double for each element // returns = a DoubleStream of primitive double values
package com.javahandson.map;
import java.util.Arrays;
import java.util.List;
public class MapToDoubleExample {
public static void main(String[] args) {
List<String> prices = Arrays.asList("19.99", "5.49", "120.00", "8.25");
double total = prices.stream()
.mapToDouble(Double::parseDouble)
.sum();
double average = prices.stream()
.mapToDouble(Double::parseDouble)
.average()
.orElse(0.0);
System.out.printf("Total : %.2f%n", total); // Output: Total : 153.73
System.out.printf("Average : %.2f%n", average); // Output: Average : 38.43
}
}Notice orElse(0.0) on the average. An empty price list would otherwise leave us holding an empty OptionalDouble, and this gives us a sensible fallback.
A word of caution about double and money. Floating point cannot represent every decimal exactly, so financial code usually prefers BigDecimal. Use mapToDouble for reports and averages, not for balances that must match to the last paisa.
Primitive streams give up something in exchange for speed. They cannot hold objects, so collect(Collectors.toList()) does not work on them. Three methods bring you back.
The boxed() method wraps each primitive back into its object form.
List<Integer> rolls = students.stream()
.mapToInt(Student::getRollNumber)
.boxed() // IntStream -> Stream<Integer>
.collect(Collectors.toList());
System.out.println(rolls); // Output: [101, 102, 103]When you want an object other than the plain wrapper, use mapToObj. It maps each primitive to any type you like.
List<String> labels = IntStream.rangeClosed(1, 4)
.mapToObj(i -> "Chapter " + i)
.collect(Collectors.toList());
System.out.println(labels);
// Output: [Chapter 1, Chapter 2, Chapter 3, Chapter 4]Think of mapToObj as the mirror image of mapToInt. One leaves the object world, the other returns to it.
Sometimes you stay primitive but need a wider type. Two shortcut methods cover that.
asLongStream() turns an IntStream into a LongStreamasDoubleStream() turns an IntStream or LongStream into a DoubleStreamdouble avgRoll = students.stream()
.mapToInt(Student::getRollNumber)
.asDoubleStream()
.average()
.orElse(0.0);
System.out.println(avgRoll); // Output: 102.0Primitive streams carry their own map method, and it behaves a little differently. On an IntStream, map takes an IntUnaryOperator, which means int goes in and int comes out. You stay inside the primitive world.
int total = IntStream.rangeClosed(1, 5)
.map(n -> n * n) // IntStream -> IntStream, no boxing
.sum();
System.out.println(total); // Output: 55Compare that with Stream<Integer>, where the same squaring step boxes and unboxes every value. For number crunching over large data, staying primitive end to end pays off.
Keep the three variants clear in your head. A map on a regular stream can change the type completely, a map on a primitive stream keeps the type, and mapToObj exits back to objects.
These two confuse people constantly, yet the rule is short. Use map when one element becomes one element. Use flatMap when one element becomes many.
| Question | map | flatMap |
|---|---|---|
| Mapper returns | A single value | A whole stream |
| Element count | Stays the same | Can grow or shrink |
| Nested lists | Gives Stream<List<T>> | Gives Stream<T> |
| Typical use | Pull out one field | Flatten a list of lists |
List<List<String>> nested = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d"));
// map keeps the nesting
List<List<String>> stillNested = nested.stream()
.map(inner -> inner)
.collect(Collectors.toList());
System.out.println(stillNested); // Output: [[a, b], [c, d]]
// flatMap removes it
List<String> flat = nested.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println(flat); // Output: [a, b, c, d]Our dedicated article on flatMap in Java 8 streams goes much deeper, with nested object examples.
| Method | Argument | Returns | Reach for it when |
|---|---|---|---|
map | Function<T, R> | Stream<R> | The result is any object |
mapToInt | ToIntFunction<T> | IntStream | Counting or whole-number maths |
mapToLong | ToLongFunction<T> | LongStream | Totals or IDs that outgrow int |
mapToDouble | ToDoubleFunction<T> | DoubleStream | Prices, averages, measurements |
All four share the same personality. Every one of them is intermediate, lazy, and safe for the source collection.
These six catch nearly everybody at least once. Read them now and skip the debugging session later.
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3));
numbers.stream().map(n -> n * 10).collect(Collectors.toList());
System.out.println(numbers); // Output: [1, 2, 3] <-- unchanged!
// Keep the result instead
List<Integer> scaled = numbers.stream()
.map(n -> n * 10)
.collect(Collectors.toList());
System.out.println(scaled); // Output: [10, 20, 30]Always assign what collect hands back. A stream pipeline that nobody stores is a pipeline that did nothing useful.
We saw this in section 3.5. A pipeline ending at map never executes the mapper. Your logging statement stays silent, your counter never increments, and you start doubting Java itself.
Check the last line of the chain first whenever a stream seems to do nothing.
List<String> words = Arrays.asList("java", null, "streams");
// Throws NullPointerException on the second element
// words.stream().map(String::toUpperCase).forEach(System.out::println);
// Guard first, then map
List<String> safe = words.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(safe); // Output: [JAVA, STREAMS]A mapper may return null without complaint, but calling a method on null blows up immediately. Filter the nulls out before you map them.
Stream<Student> stream = students.stream(); List<String> names = stream.map(Student::getName).collect(Collectors.toList()); // IllegalStateException: stream has already been operated upon or closed // List<Character> grades = stream.map(Student::getGrade).collect(Collectors.toList());
Every stream works exactly once. Call students.stream() again for a second pipeline, or better, store the stream source rather than the stream itself.
Your mapper should compute a value and return it. Nothing more. Adding to an outer list, mutating a field, or updating a counter inside map creates trouble the moment somebody switches to a parallel stream in Java 8.
peek during debugging, not in mapcollect, never by adding inside the mapperList<Double> prices = Arrays.asList(19.99, 5.49, 120.75); // Truncates towards zero, quietly losing the decimals int[] rounded = prices.stream().mapToInt(Double::intValue).toArray(); System.out.println(Arrays.toString(rounded)); // Output: [19, 5, 120] // Say what you mean instead long[] properly = prices.stream().mapToLong(Math::round).toArray(); System.out.println(Arrays.toString(properly)); // Output: [20, 5, 121]
Mapping a decimal to an int chops the fraction off rather than rounding. When you want rounding, ask for it with Math::round.
A mapper returns exactly one value per element, so the count never moves. Beginners sometimes try to skip elements by returning null, which only pushes the problem downstream.
filterflatMapmapKeep those three jobs separate and your pipelines stay easy to read.
Let us tie every idea together in one small program. We have a list of orders, and the shop owner wants three things: the customer names in capitals, the total revenue, and the average order value for shipped orders only.
package com.javahandson.map;
public class Order {
private String customer;
private double amount;
private boolean shipped;
public Order(String customer, double amount, boolean shipped) {
this.customer = customer;
this.amount = amount;
this.shipped = shipped;
}
public String getCustomer() {
return customer;
}
public double getAmount() {
return amount;
}
public boolean isShipped() {
return shipped;
}
}package com.javahandson.map;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class OrderReport {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("Suraj", 1250.50, true),
new Order("Iqbal", 340.00, true),
new Order("Amar", 890.25, false),
new Order("Suchit", 2100.75, true));
// 1. Customer names in capitals
List<String> customers = orders.stream()
.map(Order::getCustomer)
.map(String::toUpperCase)
.collect(Collectors.toList());
// 2. Total revenue across every order
double revenue = orders.stream()
.mapToDouble(Order::getAmount)
.sum();
// 3. Average value of shipped orders only
double avgShipped = orders.stream()
.filter(Order::isShipped)
.mapToDouble(Order::getAmount)
.average()
.orElse(0.0);
System.out.println("Customers : " + customers);
System.out.printf("Revenue : %.2f%n", revenue);
System.out.printf("Avg ship : %.2f%n", avgShipped);
}
}
// Output: Customers : [SURAJ, IQBAL, AMAR, SUCHIT]
// Output: Revenue : 4581.50
// Output: Avg ship : 1230.42Walk through what each block does and the pattern becomes obvious.
map calls, one for the field and one for the case changemapToDouble, because only a DoubleStream offers sum()orders list, and that list stays untouchedThirty lines of loops shrank into three short pipelines. Better still, each pipeline states its intent right there in the method names.
A: Mapping transforms every element of a stream into something else and collects the results into a new stream. The Stream interface offers four mapping methods: map, mapToInt, mapToLong and mapToDouble. None of them changes the original collection.
A: The map method changes what each element looks like and keeps the element count the same. The filter method keeps elements as they are and drops the ones that fail a condition. Put them together and you can reshape and narrow a stream in one pipeline.
A: With map, your function returns one value per element, so five inputs give five outputs. With flatMap, your function returns a whole stream per element, and Java merges those streams into one flat stream. Choose flatMap when you need to flatten nested lists.
A: No. The map method builds a new stream and leaves your source collection exactly as it was. If you want the transformed values, assign the result of collect to a variable.
A: No, map is an intermediate operation. It returns a stream, so the pipeline can continue. Until a terminal operation such as collect, forEach or count runs, your mapper function never executes even once.
A: Two reasons. First, mapToInt produces an IntStream of raw int values, so Java skips the wrapper objects that a Stream of Integer needs. Second, an IntStream gives you sum, min, max, average and summaryStatistics, which a regular stream lacks.
A: It returns an IntStream, which holds primitive int values rather than Integer objects. Similarly mapToLong returns a LongStream and mapToDouble returns a DoubleStream.
A: Call boxed() on the IntStream. That wraps each primitive into an Integer and lets you use collect(Collectors.toList()) again. For any other target type, use mapToObj instead.
A: Yes, and you should whenever the lambda only calls one method. Writing map(Student::getName) reads better than spelling out the full lambda. Keep the lambda form when the mapper needs extra logic.
A: Absolutely. Each map hands its output stream to the next operation, so chaining several of them is normal. Splitting a complex transformation across two simple map calls usually reads better than one dense lambda.
A: The stream happily carries the null forward, and the trouble shows up later when some other operation calls a method on it. Filter nulls out with filter(Objects::nonNull) before or after the map, depending on where they come from.
Let us wrap up what we covered.
map method takes a Function and returns a Stream of whatever your function producessum, average and summaryStatisticsboxed() or mapToObj() to get back to a stream of objectsflatMap instead when one element should become manyPractise with your own domain objects next. Take any list in your project, pull one field out with map, then total a number with mapToDouble. Two pipelines and the idea sticks for good.
Got a question about mapping in Java 8 streams? Drop it in the comments and we will answer. Sharing this post with a friend who is learning streams helps too.