Streams provide a powerful, declarative way to filter large datasets and generate insightful reports. By chaining operations like filter()
, map()
, collect()
, and aggregation methods, you can transform raw data into meaningful summaries efficiently and readably.
Below are two practical examples from common domains: employee performance filtering and product expiration reporting.
Suppose you have a list of employees with sales numbers and want to generate a report of employees who exceeded a sales threshold.
import java.util.*;
import java.util.stream.Collectors;
class Employee {
String name;
double sales;
Employee(String name, double sales) {
this.name = name;
this.sales = sales;
}
public double getSales() {
return sales;
}
@Override
public String toString() {
return name + " (Sales: " + sales + ")";
}
}
public class EmployeeReport {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", 15000),
new Employee("Bob", 9000),
new Employee("Carol", 20000),
new Employee("David", 12000),
new Employee("Eve", 8000)
);
double threshold = 10000;
List<Employee> topPerformers = employees.stream()
.filter(e -> e.getSales() > threshold)
.sorted(Comparator.comparingDouble(Employee::getSales).reversed())
.collect(Collectors.toList());
System.out.println("Top Performing Employees:");
topPerformers.forEach(System.out::println);
}
}
Output:
Top Performing Employees:
Carol (Sales: 20000.0)
Alice (Sales: 15000.0)
David (Sales: 12000.0)
Explanation: We start by filtering employees whose sales exceed the threshold, then sort them in descending order by sales, and finally collect them into a list for reporting.
Assume you manage a product catalog with expiration dates. The goal is to extract and report all products that have expired as of today.
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
class Product {
String name;
LocalDate expirationDate;
Product(String name, LocalDate expirationDate) {
this.name = name;
this.expirationDate = expirationDate;
}
@Override
public String toString() {
return name + " (Expires: " + expirationDate + ")";
}
}
public class ProductReport {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Milk", LocalDate.now().minusDays(1)),
new Product("Bread", LocalDate.now().plusDays(2)),
new Product("Cheese", LocalDate.now().minusDays(5)),
new Product("Butter", LocalDate.now().plusDays(10))
);
LocalDate today = LocalDate.now();
List<Product> expiredProducts = products.stream()
.filter(p -> p.expirationDate.isBefore(today) || p.expirationDate.isEqual(today))
.collect(Collectors.toList());
System.out.println("Expired Products:");
expiredProducts.forEach(System.out::println);
}
}
Output:
Expired Products:
Milk (Expires: 2025-06-22)
Cheese (Expires: 2025-06-18)
Explanation: The stream filters products whose expiration date is before or equal to the current date, gathering all expired items for the report.
These patterns apply broadly—from HR analytics to inventory management—helping transform large data into actionable insights.
Streams are ideal for reshaping and preparing data for export into formats such as JSON, CSV, or custom string representations. By combining mapping operations and collectors, you can easily transform domain objects into Data Transfer Objects (DTOs) or formatted strings, and then write them to files or other output streams.
Suppose you have a list of Employee
objects and want to export their details as CSV lines.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
class Employee {
String name;
String department;
double salary;
Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
}
public class CsvExportExample {
public static void main(String[] args) throws IOException {
List<Employee> employees = List.of(
new Employee("Alice", "Engineering", 85000),
new Employee("Bob", "Sales", 72000),
new Employee("Carol", "Engineering", 91000)
);
// Map each employee to a CSV line
List<String> csvLines = employees.stream()
.map(e -> String.join(",", e.name, e.department, String.valueOf(e.salary)))
.collect(Collectors.toList());
// Add header at the beginning
csvLines.add(0, "Name,Department,Salary");
// Write to CSV file
Path outputPath = Path.of("employees.csv");
Files.write(outputPath, csvLines);
System.out.println("CSV export completed: " + outputPath.toAbsolutePath());
}
}
Explanation: We transform each Employee
into a CSV string using map()
, collect the lines into a list, prepend a header line, and write the list to a file with Files.write()
. This approach preserves the data structure and is easy to maintain or extend.
For lightweight JSON export without external libraries, streams can build JSON representations manually.
import java.util.List;
import java.util.stream.Collectors;
class Product {
String name;
double price;
Product(String name, double price) {
this.name = name;
this.price = price;
}
}
public class JsonExportExample {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Laptop", 1200.50),
new Product("Phone", 650.00),
new Product("Tablet", 300.99)
);
String json = products.stream()
.map(p -> String.format("{\"name\":\"%s\",\"price\":%.2f}", p.name, p.price))
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("JSON output:");
System.out.println(json);
}
}
Explanation: This example transforms each Product
into a JSON-like string, then joins them with commas, wrapping the entire collection in square brackets to form a JSON array. This pattern is useful for simple JSON export without external dependencies.
map()
to convert domain objects into strings or DTOs for the desired output format.Collectors.toList()
or Collectors.joining()
to assemble the output.Files.write()
or similar APIs to persist results into files.Streams provide a clean, functional approach to transform and export data efficiently, supporting formats ranging from CSV and JSON to any custom output your application requires.
In modern web applications, Java Streams offer a powerful, concise way to handle common data processing tasks such as filtering API request payloads, transforming query results, and preparing data for JSON responses. Their fluent, declarative style fits naturally within service layers or controllers, enabling clean, readable, and maintainable code.
Imagine a REST endpoint receives a list of user roles submitted from a form. The service needs to validate and filter out invalid or duplicate roles before further processing.
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
class UserRoleService {
private static final Set<String> VALID_ROLES = Set.of("ADMIN", "USER", "MODERATOR");
public List<String> filterValidRoles(List<String> submittedRoles) {
return submittedRoles.stream()
.map(String::toUpperCase) // Normalize case
.filter(VALID_ROLES::contains) // Keep only valid roles
.distinct() // Remove duplicates
.collect(Collectors.toList());
}
}
// Example usage in a controller or service
public class UserController {
public static void main(String[] args) {
UserRoleService service = new UserRoleService();
List<String> submittedRoles = List.of("admin", "user", "guest", "user");
List<String> filteredRoles = service.filterValidRoles(submittedRoles);
System.out.println("Filtered Roles: " + filteredRoles);
}
}
Explanation: This example uses a stream pipeline to clean and validate input, demonstrating how Streams can be integrated into business logic for form processing or API input validation.
Suppose your backend retrieves a list of Product
entities from the database, but the API response requires a simplified DTO with only name and price, formatted as strings.
import java.util.List;
import java.util.stream.Collectors;
class Product {
String name;
double price;
String description; // extra field not needed in response
Product(String name, double price, String description) {
this.name = name;
this.price = price;
this.description = description;
}
}
class ProductDTO {
String name;
String price;
ProductDTO(String name, String price) {
this.name = name;
this.price = price;
}
}
class ProductService {
public List<ProductDTO> getProductDTOs(List<Product> products) {
return products.stream()
.map(p -> new ProductDTO(p.name, String.format("$%.2f", p.price)))
.collect(Collectors.toList());
}
}
// Simulated Controller method
public class ProductController {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Laptop", 1200.99, "High-end laptop"),
new Product("Mouse", 25.50, "Wireless mouse"),
new Product("Keyboard", 45.00, "Mechanical keyboard")
);
ProductService productService = new ProductService();
List<ProductDTO> response = productService.getProductDTOs(products);
// In a real app, response would be serialized as JSON by the web framework
response.forEach(dto -> System.out.println(dto.name + ": " + dto.price));
}
}
Explanation: Here, streams cleanly handle the conversion of entity objects to DTOs tailored for API responses, demonstrating separation of concerns and data shaping before JSON serialization.
Optional
or filter
).Streams empower Java web applications to efficiently process, validate, and transform data, enhancing code maintainability and performance in APIs and user interfaces alike.