- Only use
findFirstif order matters. - In most cases,
findAnywill be good enough. - Makes a more significant performance difference in parallel streams
- Example of when to use
findFirst:Optional<String> baseUrl = Stream.of( System.getenv("BASE_URL"), System.getProperty("base.url"), configFile.get("baseUrl")) .filter(Objects::nonNull) .findFirst();
List<Address> primaryAddresses = customerAccount.getAddresses().stream()
.filter(Address::isPrimary)
.collect(Collectors.toList());
Address primaryAddress = primaryAddresses.isEmpty() ? null : primaryAddresses.get(0);Address primaryAddress = customerAccount.getAddresses().stream()
.filter(Address::isPrimary)
.findAny()
.orElse(null);List<Item> outOfStockItems = items.stream()
.filter(item -> item.getStock() == 0)
.collect(Collectors.toList());
boolean anyOutOfStock = !outOfStockItems.isEmpty();boolean anyOutOfStock = order.getItems().stream()
.filter(item -> item.getStock() == 0)
.count() > 0;boolean anyOutOfStock = order.getItems().stream()
.anyMatch(item -> item.getStock() == 0);for (Role role : user.getRoles()) {
for (String perm : role.getPermissions()) {
if (perm.equals(requiredPermission)) {
return; // has permission
}
}
}
throw new AccessDeniedException();user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.filter(perm -> perm.equals(requiredPermission))
.findAny()
.orElseThrow(() -> new AccessDeniedException());List<String> results = new ArrayList<>();
for (Optional<String> opt : optionals) {
if (opt.isPresent()) {
results.add(opt.get());
}
}List<String> results = new ArrayList<>();
for (Optional<String> opt : optionals) {
opt.ifPresent(result -> results.add(result));
}List<String> results = optionals.stream()
.flatMap(Optional::stream) // since Java 9
.collect(Collectors.toList());String delimiter = ", ";
StringBuilder sb = new StringBuilder();
boolean isFirst = true;
for (Product product : products) {
String category = product.getCategory();
if (!isFirst) {
sb.append(delimiter);
} else {
isFirst = false;
}
sb.append(category);
}
String result = sb.toString();List<String> names = products.stream()
.map(product -> product.getCategory())
.collect(Collectors.toList());
String result = String.join(", ", names); // since Java 8String result = products.stream()
.map(Product::getCategory)
.collect(Collectors.joining(", "));Optional<Order> newestOrder = orders.stream()
.sorted(Comparator.comparing(Order::getCreationDate).reversed())
.findFirst();Optional<Order> newestOrder = orders.stream()
.max(Comparator.comparing(Order::getCreationDate));mapToInt/mapToLong/mapToDouble/mapToObj: Creating 50 order objects with the index in the name in a list
List<Order> ordersList = new ArrayList<>();
for (int i = 0; i < 50; i++) {
ordersList.add(new Order("Order #" + i));
}List<Order> ordersList = IntStream.range(0, 50)
.mapToObj(i -> new Order("Order #" + i))
.collect(Collectors.toList());BigDecimal total = BigDecimal.ZERO;
for (Order order : orders) {
total = total.add(order.getTotalAmount());
}BigDecimal total = orders.stream()
.map(Order::getTotalAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);- Only exists on
IntStream,LongStream, andDoubleStream
double totalRadius = shapes.stream()
.filter(s -> s instanceof Circle)
.map(s -> (Circle) s)
.map(Circle::getRadius)
.reduce(0.0, Double::sum);double totalRadius = shapes.stream()
.filter(Circle.class::isInstance)
.map(Circle.class::cast)
.mapToDouble(Circle::getRadius)
.sum();- Only change: Replace
.streamwith.parallelStream - Be careful about performance overhead!
- Uses common
ForkJoinPoolby default (parallelism ≈availableProcessors() - 1) - Parallel doesn't mean async: it's still a blocking operation!
public class ParallelStreamDemo {
public static void main(String[] args) {
long result = LongStream.rangeClosed(1, 100_000)
.parallel() // convert Stream to parallel
.map(ParallelStreamDemo::heavyComputation)
.sum();
}
private static long heavyComputation(long number) {
long result = 0;
for (int i = 0; i < 1000; i++) {
result += (long) Math.sqrt(number * i);
}
return result;
}
}List<Product> favoriteProducts = new ArrayList<>();
for (Product product : user.getFavoriteProducts()) {
if (product.isInStock()) {
favoriteProducts.add(product);
}
}
Collections.sort(favoriteProducts, Comparator.comparing(Product::getName));- Easier to parallelize
List<Product> favoriteProducts = user.getFavoriteProducts().stream()
.filter(Product::isInStock)
.sorted(Comparator.comparing(Product::getName))
.collect(Collectors.toList());- Assuming
.isInStock()calls an API Gatherers.mapConcurrentuses virtual threads, allowing much higher concurrency for blocking API calls thanparallelStream
List<Product> favoriteProducts = user.getFavoriteProducts().stream()
.gather(Gatherers.mapConcurrent(
100, // max number of concurrent API calls
product -> Map.entry(product, product.isInStock())
))
.filter(Map.Entry::getValue) // keep only in-stock
.map(Map.Entry::getKey) // extract `Product`
.sorted(Comparator.comparing(Product::getName))
.toList();List<Product> sortedProducts = homepageProducts.stream()
.sorted(Comparator.comparing(Product::getRating).reversed())
.collect(Collectors.toList());
List<Product> topThree = sortedProducts.size() > 3
? sortedProducts.subList(0, 3)
: sortedProducts;List<Product> topThree = homepageProducts.stream()
.sorted(Comparator.comparing(Product::getRating).reversed())
.limit(3) // no exception if less than 3
.collect(Collectors.toList());List<Message> unreadMessages = messageService.getMessages(customer)
.stream()
.filter(message -> !message.isRead())
.collect(Collectors.toList());
int unread = unreadMessages.size();long unread = messageService.getMessages(customer)
.stream()
.filter(message -> !message.isRead())
.count();import static java.util.function.Predicate.not;
long unread = messageService.getMessages(customer)
.stream()
.filter(not(Message::isRead))
.count();Set<String> discountCodesSet = new HashSet<>();
for (Order order : orders) {
String discountCode = order.getDiscountCode();
if(discountCode != null) {
discountCodesSet.add(discountCode);
}
}
List<String> discountCodesList = new ArrayList<>(discountCodesSet);
Collections.sort(discountCodesList);orders.stream()
.map(Order::getDiscountCode)
.filter(Objects::nonNull)
.distinct() // order matters: better performance
.sorted()
.toList(); // since Java 16Calling .sorted or Collections.sort without a comparator or with natural ordering will throw NullPointerException if any element is null:
var list = Arrays.asList("c", null, "b", "a");
// ❌ throws NPE
Collections.sort(list);
Collections.sort(list, Comparator.naturalOrder());
// ✅ safe
Collections.sort(list, Comparator.nullsFirst(Comparator.naturalOrder()));
Collections.sort(list, Comparator.nullsLast(Comparator.naturalOrder()));List<String> codes = orders.stream()
.map(Order::getDiscountCode)
.collect(Collectors.toList());
Set<String> uniqueCodes = new HashSet<>(codes);Set<String> uniqueCodes = orders.stream()
.map(Order::getDiscountCode)
.collect(Collectors.toSet());- Similar to
Collectors.groupingBy, when keys are unique (every key is mapped to exactly one value). - An optional third parameter can be provided, which allows to define how to handle cases where a key maps to multiple values.
- If no third parameter is provided, it will result in an exception if not unique.
Map<String, Product> cheapestByCategory = new HashMap<>();
for (Product product : products) {
String category = product.getCategory();
if (!cheapestByCategory.containsKey(category)) {
cheapestByCategory.put(category, product);
} else {
Product currentCheapest = cheapestByCategory.get(category);
if (product.getPrice().compareTo(currentCheapest.getPrice()) < 0) {
cheapestByCategory.put(category, product);
}
}
}Map<String, Product> cheapestByCategory = products.stream()
.collect(Collectors.toMap(
Product::getCategory, // must never be `null`!
Function.identity(), // equivalent to: p -> p
BinaryOperator.minBy( // optional
Comparator.comparing(Product::getPrice)
)
));Similar to Collectors.toMap, when keys are mapped to multiple values (to a List of values).
Map<String, List<Order>> ordersByCustomer = new HashMap<>();
for (Order order : orders) {
String customerId = order.getCustomerId();
ordersByCustomer
// avoid NPE if key is absent or mapped to null
.computeIfAbsent(customerId, c -> new ArrayList<>())
.add(order);
}Map<String, List<Order>> ordersByCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::getCustomerId));Most useful when used together with other Collectors, like groupingBy and partitioningBy.
Map<String, List<String>> productNamesByCategory = new HashMap<>();
for (Product product : products) {
String category = product.getCategory();
List<String> productNames = productNamesByCategory.get(category);
if (productNames == null) {
productNames = new ArrayList<>();
productNamesByCategory.put(category, productNames);
}
productNames.add(product.getName());
}Map<String, List<String>> productNamesByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.mapping(Product::getName, Collectors.toList())
));Map<String, Long> mapNameToSales = new HashMap<>();
for (Order order : orders) {
for (Item item : order.getItems()) {
mapNameToSales.put(item.getName(), mapNameToSales.getOrDefault(item.getName(), 0L) + 1L);
// or: mapNameToSales.merge(item.name(), 1L, Long::sum);
}
}Map<String, Long> mapNameToSales = orders.stream()
.flatMap(order -> order.getItems().stream())
.collect(Collectors.groupingBy(Item::getName, Collectors.counting()));mapMulti: email addresses of developers that didn't finish the secure coding training yet (Java 16+)
- Similar to
flatMap: one-to-many transformation to the elements of the stream, flattens the result elements into a new stream. - Preferable to
flatMapwhen:- replacing each stream element with a small (possibly zero) number of elements (avoids overhead of calling
.stream()on every element). - it is easier to use an imperative approach for generating result elements than it is to return them in the form of a Stream.
- replacing each stream element with a small (possibly zero) number of elements (avoids overhead of calling
Set<String> emailsWithoutTraining = companies.stream()
.map(company -> company.getEmployees().stream() // code smell
.filter(Developer.class::isInstance)
.map(employee -> ((Developer) employee).getSecureCodingTraining())
.filter(not(SecureCodingTraining::isCompleted))
.map(SecureCodingTraining::getDeveloperEmail)
.collect(Collectors.toSet()))
.flatMap(Set::stream)
.collect(Collectors.toSet());Set<String> emailsWithoutTraining = companies.stream()
.flatMap(company -> company.getEmployees().stream())
.filter(Developer.class::isInstance)
.map(employee -> ((Developer) employee).getSecureCodingTraining())
.filter(not(SecureCodingTraining::isCompleted))
.map(SecureCodingTraining::getDeveloperEmail)
.collect(Collectors.toSet());Set<String> emailsWithoutTraining = companies.stream()
.flatMap(company -> company.getEmployees().stream())
.filter(Developer.class::isInstance)
.map(employee -> ((Developer) employee).getSecureCodingTraining())
.<String>mapMulti((training, consumer) -> {
if (!training.isCompleted()) {
consumer.accept(training.getDeveloperEmail());
}
}).collect(Collectors.toSet());Set<String> emailsWithoutTraining = companies.stream()
.flatMap(company -> company.getEmployees().stream())
.<String>mapMulti((employee, consumer) -> {
if (employee instanceof Developer developer && // Java 16+
!developer.getSecureCodingTraining().isCompleted()) {
consumer.accept(developer.getEmail()); // direct access to developer
}
}).collect(Collectors.toSet());int totalQuantity = 0;
for (Order order : orders) {
for (Item item : order.getItems()) {
totalQuantity += item.getQuantity();
}
}int totalQuantity = orders.stream()
.flatMap(order -> order.getItems().stream())
.collect(Collectors.summingInt(Item::getQuantity));int sum = 0;
int count = 0;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (Order order : orders) {
for (Item item : order.getItems()) {
int qty = item.getQuantity();
sum += qty;
count++;
if (qty < min) {
min = qty;
}
if (qty > max) {
max = qty;
}
}
}
double average = count == 0 ? 0 : (double) sum / count;IntSummaryStatistics stats = orders.stream()
.flatMap(order -> order.getItems().stream())
.collect(Collectors.summarizingInt(Item::getQuantity));
// Retrieve statistics:
// stats.getCount(), stats.getSum(), stats.getMin(), stats.getMax(), stats.getAverage()Similar result can be achieved with groupingBy, however:
partitioningBy: always returns two keys:trueandfalsegroupingBy: returns a map that contains only the keys that actually occurred
List<Product> availableProducts = new ArrayList<>();
List<Product> outOfStockProducts = new ArrayList<>();
for (Product product : products) {
if (product.getStock() > 0) {
availableProducts.add(product);
} else {
outOfStockProducts.add(product);
}
}Map<Boolean, List<Product>> partitionedProducts = products.stream()
.collect(Collectors.partitioningBy(product -> product.getStock() > 0));
List<Product> availableProducts = partitionedProducts.get(true);
List<Product> outOfStockProducts = partitionedProducts.get(false);Product cheapest = products.get(0);
Product mostExpensive = products.get(0);
for (Product p : products) {
if (p.getPrice().compareTo(cheapest.getPrice()) < 0) {
cheapest = p;
}
if (p.getPrice().compareTo(mostExpensive.getPrice()) > 0) {
mostExpensive = p;
}
}
Pair<Product, Product> priceRange = new Pair<>(cheapest, mostExpensive);Pair<Product, Product> priceRange = products.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparing(Product::getPrice)),
Collectors.maxBy(Comparator.comparing(Product::getPrice)),
(minOpt, maxOpt) -> new Pair<>(minOpt.orElse(null), maxOpt.orElse(null))
));Examples assume list of packets is in chronological order.
The "spike" is the first packet where loss > threshold.
List<Packet> beforeFirstSpike = new ArrayList<>();
for (Packet p : packets) {
if (p.getLoss() <= threshold) {
beforeFirstSpike.add(p);
}
}List<Packet> beforeFirstSpike = packets.stream()
.takeWhile(p -> p.getLoss() <= threshold)
.collect(Collectors.toList());