filter and map
Keep the elements you want with filter, transform each one with map, and use the pair to turn entities into response DTOs without leaking fields.
stream.filter(x -> condition)
stream.map(x -> transform(x))
stream.mapToInt(Type::intField)filter and map are the two operations you will write most. filter(predicate) keeps the elements for which the predicate returns true. map(function) replaces each element with the function's result, possibly of a different type. Neither changes the source list.
List<UserResponse> body = users.stream()
.filter(User::active)
.map(u -> new UserResponse(u.id(), u.email()))
.toList();Entities to DTOs
In a web service, map is where an entity becomes the object you return. Mapping to a dedicated record such as UserResponse means fields like a password hash or internal flags never reach the JSON, even when someone later adds a column to the entity. Filter first, then map: you avoid building objects you are about to throw away, and the predicate can use fields the DTO does not have.
Writing predicates
Method references keep simple filters readable: filter(User::active). For the opposite, Predicate.not(String::isBlank) (Java 11) avoids a lambda. Predicates combine with and, or and negate, and a named Predicate<Order> variable documents a business rule better than a long inline lambda. Several small filter calls in a row are fine; they cost very little and each reads as one rule.
Mistakes people make
- map returning null. A lookup such as
map(cache::get)putsnullinto the stream and the failure appears later, far away. Filter out unknown keys first, filterObjects::nonNullafter, or return anOptionaland useflatMap. - Side effects in map.
mapshould compute a value. Saving to a database or incrementing a counter inside it is hard to follow, and the call may not happen at all if a laterlimitorfindFirststops the pipeline. - Parsing before validating.
map(Long::parseLong)on raw query parameter values throwsNumberFormatExceptionon the first bad one. Filter with a check first, or decide that bad input should fail the request with a 400 instead of being dropped silently.
For numbers, prefer the primitive variants mapToInt, mapToLong and mapToDouble, which lead to sum() and average().
Example
When to use it
- Return only active users from an admin endpoint, mapped to a DTO that leaves out the password hash.
- Parse a comma-separated ids query parameter into a list of longs for a batch lookup.
- Map product codes to display names from a reference table while dropping unknown codes.
More examples
Combining predicates
Predicate.not, and and negate build rules from smaller named pieces. Two filter calls in a row behave the same as one filter with and.
import java.util.List;
import java.util.function.Predicate;
void main() {
List<String> tags = List.of("java", " ", "Spring", "", "jvm", "SQL");
Predicate<String> lowerCase = s -> s.equals(s.toLowerCase());
List<String> clean = tags.stream()
.filter(Predicate.not(String::isBlank))
.filter(lowerCase)
.toList();
IO.println(clean);
List<String> needFixing = tags.stream()
.filter(Predicate.not(String::isBlank).and(lowerCase.negate()))
.toList();
IO.println(needFixing);
}
// Output:
// [java, jvm]
// [Spring, SQL]Parsing ids from a query parameter
Each value is trimmed and checked before Long::parseLong runs, so abc, the empty value and -4 are dropped instead of throwing; the 18-digit cap also keeps an oversized number from overflowing a long. In a real endpoint you may prefer to reject the request with a 400 and tell the client which value was wrong.
import java.util.Arrays;
import java.util.List;
void main() {
String raw = "12,abc,7, 30,,-4";
List<Long> ids = Arrays.stream(raw.split(","))
.map(String::strip)
.filter(s -> s.matches("\\d{1,18}"))
.map(Long::parseLong)
.toList();
IO.println(ids);
}
// Output:
// [12, 7, 30]A lookup that returns null
Map.get returns null for XX and the null flows on into the list. Filtering Objects::nonNull straight after the lookup keeps the gap from spreading.
import java.util.List;
import java.util.Map;
import java.util.Objects;
void main() {
Map<String, String> countries = Map.of("AU", "Australia", "NZ", "New Zealand");
List<String> codes = List.of("AU", "XX", "NZ");
List<String> names = codes.stream()
.map(countries::get)
.toList();
IO.println(names);
List<String> known = codes.stream()
.map(countries::get)
.filter(Objects::nonNull)
.toList();
IO.println(known);
}
// Output:
// [Australia, null, New Zealand]
// [Australia, New Zealand]
Discussion