Back to blog

Khám Phá Sâu: Java Streams và Lập Trình Hàm

javastreamsfunctional-programminglambdabackend
Khám Phá Sâu: Java Streams và Lập Trình Hàm

Java Streams và lập trình hàm đã thay đổi cách chúng ta viết code Java. Được giới thiệu trong Java 8 và được cải tiến trong các phiên bản hiện đại, Streams API cho phép xử lý dữ liệu theo cách khai báo, ngắn gọn và có thể song song hóa. Bài viết này bao gồm mọi thứ từ các kiến thức cơ bản về lambda đến các pattern stream nâng cao.

Tại Sao Lập Trình Hàm Trong Java?

Trước Java 8 (Phong Cách Mệnh Lệnh):

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> filtered = new ArrayList<>();
for (String name : names) {
    if (name.length() > 3) {
        filtered.add(name.toUpperCase());
    }
}
Collections.sort(filtered);

Sau Java 8 (Phong Cách Hàm):

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList(); // Java 16+

Lợi ích:

  • Khai báo: Diễn tả cái gì bạn muốn, không phải cách làm
  • Có thể kết hợp: Móc nối các thao tác một cách tự nhiên
  • Có thể song song hóa: Xử lý song song dễ dàng với .parallel()
  • Đánh giá lười: Các thao tác chỉ thực thi khi cần thiết

Lambda Expressions

Lambda expressions là các hàm ẩn danh triển khai functional interfaces.

Cú pháp

// Cú pháp đầy đủ
(parameters) -> { statements; return value; }
 
// Đơn giản hóa
(x, y) -> x + y                    // Nhiều tham số
x -> x * 2                         // Một tham số (không cần ngoặc)
() -> System.out.println("Hi")     // Không có tham số
x -> { return x * 2; }             // Return tường minh (nhiều câu lệnh)

Ví dụ Thực Tế

// Comparator
List<String> names = List.of("Charlie", "Alice", "Bob");
names.sort((a, b) -> a.compareTo(b));
// Hoặc đơn giản hơn:
names.sort(String::compareTo);
 
// Runnable
Thread thread = new Thread(() -> {
    System.out.println("Running in thread: " + Thread.currentThread().getName());
});
 
// Custom functional interface
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}
 
MathOperation add = (a, b) -> a + b;
MathOperation multiply = (a, b) -> a * b;
System.out.println(add.operate(5, 3));      // 8
System.out.println(multiply.operate(5, 3));  // 15

Method References

Method references (::) là cách viết tắt cho lambda gọi một phương thức duy nhất.

Các loại Method References

1. Static Method Reference

// Lambda:  x -> Math.sqrt(x)
// Method ref: Math::sqrt
List<Double> numbers = List.of(4.0, 9.0, 16.0);
List<Double> roots = numbers.stream()
    .map(Math::sqrt)
    .toList();

2. Instance Method Reference (trên đối tượng cụ thể)

String prefix = "Hello, ";
// Lambda:  name -> prefix.concat(name)
// Method ref: prefix::concat
List<String> greetings = names.stream()
    .map(prefix::concat)
    .toList();

3. Instance Method Reference (trên đối tượng tùy ý)

// Lambda:  str -> str.toLowerCase()
// Method ref: String::toLowerCase
List<String> lower = names.stream()
    .map(String::toLowerCase)
    .toList();

4. Constructor Reference

// Lambda:  len -> new String[len]
// Method ref: String[]::new
String[] array = names.stream()
    .toArray(String[]::new);
 
// Khởi tạo đối tượng
record Person(String name) {}
List<Person> people = names.stream()
    .map(Person::new)
    .toList();

Built-in Functional Interfaces

Java cung cấp các functional interfaces tiêu chuẩn trong java.util.function.

Các Interface Cốt Lõi

import java.util.function.*;
 
// Predicate<T>: T -> boolean (kiểm tra điều kiện)
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Hello"));  // false
 
// Function<T, R>: T -> R (chuyển đổi)
Function<String, Integer> length = String::length;
System.out.println(length.apply("Hello"));  // 5
 
// Consumer<T>: T -> void (tác dụng phụ)
Consumer<String> printer = System.out::println;
printer.accept("Hello");
 
// Supplier<T>: () -> T (tạo ra giá trị)
Supplier<Double> random = Math::random;
System.out.println(random.get());
 
// BiFunction<T, U, R>: (T, U) -> R
BiFunction<Integer, Integer, Integer> max = Math::max;
System.out.println(max.apply(5, 3));  // 5
 
// UnaryOperator<T>: T -> T (Function đặc biệt)
UnaryOperator<String> toUpper = String::toUpperCase;
 
// BinaryOperator<T>: (T, T) -> T (BiFunction đặc biệt)
BinaryOperator<Integer> sum = Integer::sum;

Composition

// Kết hợp Predicate
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> combined = isLong.and(startsWithA);
 
// Kết hợp Function
Function<Integer, Integer> multiplyBy2 = x -> x * 2;
Function<Integer, Integer> add3 = x -> x + 3;
Function<Integer, Integer> composed = multiplyBy2.andThen(add3);
System.out.println(composed.apply(5));  // (5 * 2) + 3 = 13

Tạo Streams

Từ Collections

List<String> list = List.of("a", "b", "c");
Stream<String> stream1 = list.stream();
Stream<String> parallelStream = list.parallelStream();

Từ Arrays

String[] array = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(array);
Stream<String> partial = Arrays.stream(array, 1, 3); // chỉ số 1-2

Từ Values

Stream<String> stream3 = Stream.of("a", "b", "c");
Stream<String> empty = Stream.empty();

Từ Generators

// Stream vô hạn (dùng với limit!)
Stream<Double> randoms = Stream.generate(Math::random);
Stream<Integer> naturals = Stream.iterate(0, n -> n + 1);
 
// Java 9+: iterate với điều kiện
Stream<Integer> finite = Stream.iterate(0, n -> n < 10, n -> n + 1);

Từ Files

try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
    lines.filter(line -> !line.isEmpty())
         .forEach(System.out::println);
}

Từ Các Nguồn Khác

// IntStream, LongStream, DoubleStream
IntStream.range(1, 5);          // 1, 2, 3, 4
IntStream.rangeClosed(1, 5);    // 1, 2, 3, 4, 5
 
// Từ String
"Hello".chars()  // IntStream của mã ký tự
    .mapToObj(c -> (char) c)
    .forEach(System.out::print);

Intermediate Operations

Intermediate operations là lazy và trả về một stream mới. Chúng chỉ thực thi khi một terminal operation được gọi.

filter

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
 
// Chỉ giữ các số chẵn
List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .toList();  // [2, 4, 6, 8]

map

record Person(String name, int age) {}
List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25)
);
 
// Trích xuất tên
List<String> names = people.stream()
    .map(Person::name)
    .toList();
 
// Chuyển đổi sang chữ hoa
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

flatMap

Làm phẳng các cấu trúc lồng nhau (stream của streams → stream đơn).

List<List<Integer>> nested = List.of(
    List.of(1, 2, 3),
    List.of(4, 5),
    List.of(6, 7, 8, 9)
);
 
// Làm phẳng thành một danh sách duy nhất
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .toList();  // [1, 2, 3, 4, 5, 6, 7, 8, 9]
 
// Thực tế: Lấy tất cả các từ từ các dòng
List<String> lines = List.of("Hello world", "Java streams");
List<String> words = lines.stream()
    .flatMap(line -> Arrays.stream(line.split(" ")))
    .toList();  // ["Hello", "world", "Java", "streams"]

sorted

// Thứ tự tự nhiên
List<Integer> sorted = numbers.stream()
    .sorted()
    .toList();
 
// Comparator tùy chỉnh
List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .toList();
 
// Thứ tự đảo ngược
List<Integer> reversed = numbers.stream()
    .sorted(Comparator.reverseOrder())
    .toList();
 
// Nhiều tiêu chí
people.stream()
    .sorted(Comparator.comparing(Person::age)
                      .thenComparing(Person::name))
    .toList();

distinct

List<Integer> numbers = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> unique = numbers.stream()
    .distinct()
    .toList();  // [1, 2, 3, 4]

limit & skip

// 3 phần tử đầu tiên
List<Integer> first3 = numbers.stream()
    .limit(3)
    .toList();
 
// Bỏ qua 2 phần tử đầu, lấy 3 phần tử tiếp theo
List<Integer> page = numbers.stream()
    .skip(2)
    .limit(3)
    .toList();

peek

Hữu ích cho việc debug (tác dụng phụ mà không tiêu thụ stream).

List<String> result = names.stream()
    .peek(name -> System.out.println("Before: " + name))
    .map(String::toUpperCase)
    .peek(name -> System.out.println("After: " + name))
    .toList();

Terminal Operations

Terminal operations kích hoạt việc thực thi stream và tạo ra kết quả.

collect

// Sang List (Java 16+)
List<String> list = stream.toList();
 
// Sang mutable List
List<String> mutableList = stream.collect(Collectors.toList());
 
// Sang Set
Set<String> set = stream.collect(Collectors.toSet());
 
// Sang collection cụ thể
LinkedList<String> linked = stream.collect(Collectors.toCollection(LinkedList::new));

reduce

Kết hợp các phần tử thành một kết quả duy nhất.

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
 
// Tổng
int sum = numbers.stream()
    .reduce(0, Integer::sum);  // 15
 
// Tích
int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);  // 120
 
// Max
Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);
 
// Nối chuỗi
String joined = List.of("a", "b", "c").stream()
    .reduce("", (acc, s) -> acc + s);  // "abc"

forEach

names.stream()
    .filter(name -> name.startsWith("A"))
    .forEach(System.out::println);
 
// forEachOrdered (giữ thứ tự trong parallel streams)
names.parallelStream()
    .forEachOrdered(System.out::println);

count

long count = names.stream()
    .filter(name -> name.length() > 5)
    .count();

min / max

Optional<Integer> min = numbers.stream()
    .min(Integer::compareTo);
 
Optional<Person> youngest = people.stream()
    .min(Comparator.comparing(Person::age));

anyMatch / allMatch / noneMatch

boolean hasLong = names.stream()
    .anyMatch(name -> name.length() > 10);
 
boolean allLong = names.stream()
    .allMatch(name -> name.length() > 2);
 
boolean noneEmpty = names.stream()
    .noneMatch(String::isEmpty);

findFirst / findAny

Optional<String> first = names.stream()
    .filter(name -> name.startsWith("A"))
    .findFirst();
 
// findAny (nhanh hơn cho parallel streams)
Optional<String> any = names.parallelStream()
    .filter(name -> name.startsWith("A"))
    .findAny();

Advanced Collectors

Collectors cung cấp khả năng tổng hợp dữ liệu mạnh mẽ.

Joining Strings

String csv = names.stream()
    .collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
 
String quoted = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie]"

Grouping

record Person(String name, String city, int age) {}
List<Person> people = List.of(
    new Person("Alice", "NYC", 30),
    new Person("Bob", "NYC", 25),
    new Person("Charlie", "LA", 35)
);
 
// Nhóm theo thành phố
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice, Bob], LA=[Charlie]}
 
// Đếm theo thành phố
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.counting()
    ));
// {NYC=2, LA=1}
 
// Tuổi trung bình theo thành phố
Map<String, Double> avgAgeByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::city,
        Collectors.averagingInt(Person::age)
    ));

Partitioning

// Chia thành hai nhóm dựa trên predicate
Map<Boolean, List<Person>> partition = people.stream()
    .collect(Collectors.partitioningBy(p -> p.age() >= 30));
// {false=[Bob], true=[Alice, Charlie]}

Custom Collector

// Thu thập vào Map
Map<String, Integer> nameToAge = people.stream()
    .collect(Collectors.toMap(
        Person::name,
        Person::age
    ));
 
// Xử lý các key trùng lặp
Map<String, Person> byName = people.stream()
    .collect(Collectors.toMap(
        Person::name,
        p -> p,
        (existing, replacement) -> existing  // Giữ phần tử đầu tiên
    ));
 
// Sử dụng triển khai Map cụ thể
TreeMap<String, Integer> sorted = people.stream()
    .collect(Collectors.toMap(
        Person::name,
        Person::age,
        (a, b) -> a,
        TreeMap::new
    ));

Statistics

IntSummaryStatistics stats = people.stream()
    .collect(Collectors.summarizingInt(Person::age));
 
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Avg: " + stats.getAverage());

Parallel Streams

Parallel streams tự động chia công việc trên nhiều threads.

Khi nào nên sử dụng

// Tuần tự
long count = hugeList.stream()
    .filter(expensivePredicate)
    .count();
 
// Song song
long count = hugeList.parallelStream()
    .filter(expensivePredicate)
    .count();

Cân nhắc về Hiệu suất

✅ Tốt cho song song:

  • Dataset lớn (10,000+ phần tử)
  • Các thao tác tốn CPU
  • Các thao tác độc lập (không có shared state)
  • Các thao tác tốn thời gian (file I/O, tính toán)

❌ Không tốt cho song song:

  • Dataset nhỏ (overhead > benefit)
  • Các thao tác đã nhanh
  • Các thao tác có tác dụng phụ trên shared state
  • Các thao tác có thứ tự (limit, skip, findFirst)

Ví dụ: Parallel vs Sequential

List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
    .boxed()
    .toList();
 
// Tuần tự
long start = System.currentTimeMillis();
int sum = numbers.stream()
    .mapToInt(n -> n * 2)
    .sum();
System.out.println("Sequential: " + (System.currentTimeMillis() - start) + "ms");
 
// Song song
start = System.currentTimeMillis();
sum = numbers.parallelStream()
    .mapToInt(n -> n * 2)
    .sum();
System.out.println("Parallel: " + (System.currentTimeMillis() - start) + "ms");

Tránh Race Conditions

// ❌ TỒI: Shared mutable state
List<Integer> results = new ArrayList<>();
numbers.parallelStream()
    .forEach(results::add);  // Race condition!
 
// ✅ TỐT: Sử dụng collector
List<Integer> results = numbers.parallelStream()
    .collect(Collectors.toList());

Optional cho Null Safety

Optional<T> đại diện cho một giá trị có thể có hoặc không có.

Tạo Optional

Optional<String> full = Optional.of("Hello");
Optional<String> nullable = Optional.ofNullable(null);
Optional<String> empty = Optional.empty();

Sử dụng Optional

// Kiểm tra nếu có giá trị
if (optional.isPresent()) {
    String value = optional.get();
}
 
// ifPresent (tốt hơn)
optional.ifPresent(value -> System.out.println(value));
 
// ifPresentOrElse (Java 9+)
optional.ifPresentOrElse(
    value -> System.out.println("Found: " + value),
    () -> System.out.println("Not found")
);

Giá trị Mặc định

// orElse: Luôn được đánh giá
String value = optional.orElse("default");
 
// orElseGet: Chỉ được đánh giá nếu rỗng (ưu tiên)
String value = optional.orElseGet(() -> expensiveDefault());
 
// orElseThrow
String value = optional.orElseThrow();
String value = optional.orElseThrow(() -> new IllegalStateException("Missing value"));

Chuyển đổi Optional

Optional<String> name = Optional.of("Alice");
 
// map
Optional<Integer> length = name.map(String::length);
 
// flatMap (khi mapper trả về Optional)
Optional<String> upper = name.flatMap(n -> 
    n.isEmpty() ? Optional.empty() : Optional.of(n.toUpperCase())
);
 
// filter
Optional<String> longName = name.filter(n -> n.length() > 5);

Ví dụ Thực Tế

record User(String id, Optional<String> email) {}
 
// Tìm user, lấy email, hoặc sử dụng mặc định
String email = userRepository.findById("123")
    .flatMap(User::email)
    .filter(e -> e.contains("@"))
    .orElse("[email protected]");

Ví dụ Thực Tế

Ví dụ 1: Pipeline Xử lý Dữ liệu

record Transaction(String id, String category, double amount, LocalDate date) {}
 
List<Transaction> transactions = getTransactions();
 
// Lấy 5 danh mục hàng đầu theo tổng chi tiêu trong 30 ngày qua
Map<String, Double> topCategories = transactions.stream()
    .filter(t -> t.date().isAfter(LocalDate.now().minusDays(30)))
    .collect(Collectors.groupingBy(
        Transaction::category,
        Collectors.summingDouble(Transaction::amount)
    ))
    .entrySet().stream()
    .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
    .limit(5)
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (a, b) -> a,
        LinkedHashMap::new
    ));

Ví dụ 2: Chuyển đổi Đối tượng Lồng nhau

record Order(String id, List<OrderItem> items) {}
record OrderItem(String product, int quantity, double price) {}
 
List<Order> orders = getOrders();
 
// Lấy tất cả các sản phẩm duy nhất đã đặt trong tháng trước
Set<String> products = orders.stream()
    .filter(o -> isLastMonth(o.id()))
    .flatMap(order -> order.items().stream())
    .map(OrderItem::product)
    .collect(Collectors.toSet());
 
// Tính tổng doanh thu theo sản phẩm
Map<String, Double> revenueByProduct = orders.stream()
    .flatMap(order -> order.items().stream())
    .collect(Collectors.groupingBy(
        OrderItem::product,
        Collectors.summingDouble(item -> item.quantity() * item.price())
    ));

Ví dụ 3: Lọc & Ánh xạ Phức tạp

record Employee(String name, String department, int salary, int age) {}
 
List<Employee> employees = getEmployees();
 
// Lấy tên nhân viên ở phòng Engineering kiếm > $80k, sắp xếp theo lương
List<String> highEarners = employees.stream()
    .filter(e -> "Engineering".equals(e.department()))
    .filter(e -> e.salary() > 80_000)
    .sorted(Comparator.comparing(Employee::salary).reversed())
    .map(Employee::name)
    .toList();
 
// Nhóm theo phòng ban, tính lương trung bình, lọc các phòng có avg > $75k
Map<String, Double> highPayingDepts = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::department,
        Collectors.averagingInt(Employee::salary)
    ))
    .entrySet().stream()
    .filter(entry -> entry.getValue() > 75_000)
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue
    ));

Best Practices

✅ NÊN Sử dụng Streams Cho

  1. Xử lý dữ liệu khai báo

    // Ý định rõ ràng
    List<String> result = names.stream()
        .filter(name -> name.length() > 5)
        .map(String::toUpperCase)
        .sorted()
        .toList();
  2. Móc nối nhiều thao tác

    double average = transactions.stream()
        .filter(t -> t.amount() > 100)
        .mapToDouble(Transaction::amount)
        .average()
        .orElse(0.0);
  3. Xử lý song song các dataset lớn

    long count = hugeList.parallelStream()
        .filter(expensivePredicate)
        .count();

❌ KHÔNG NÊN Sử dụng Streams Cho

  1. Lặp đơn giản

    // Tồi: Phức tạp không cần thiết
    names.stream().forEach(System.out::println);
     
    // Tốt: Vòng lặp trực tiếp
    for (String name : names) {
        System.out.println(name);
    }
  2. Vòng lặp có chỉ số

    // Tồi: Khó hiểu với streams
    IntStream.range(0, list.size())
        .forEach(i -> process(list.get(i), i));
     
    // Tốt: Vòng lặp for truyền thống
    for (int i = 0; i < list.size(); i++) {
        process(list.get(i), i);
    }
  3. Checked exceptions

    // Tồi: Cần phải wrap
    files.stream()
        .map(file -> {
            try {
                return Files.readString(file);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
     
    // Tốt: Try-catch truyền thống
    for (Path file : files) {
        try {
            String content = Files.readString(file);
            process(content);
        } catch (IOException e) {
            handleError(e);
        }
    }
  4. Kết thúc sớm với điều kiện phức tạp

    // Tồi: Khó break sớm
    Optional<String> result = names.stream()
        .filter(complexCondition)
        .findFirst();
     
    // Tốt: Break khi cần
    String result = null;
    for (String name : names) {
        if (complexCondition(name)) {
            result = name;
            break;
        }
    }

Mẹo Hiệu suất

  1. Tránh boxing/unboxing với primitive streams

    // Tồi: Overhead của boxing
    List<Integer> numbers = List.of(1, 2, 3, 4, 5);
    int sum = numbers.stream()
        .reduce(0, Integer::sum);
     
    // Tốt: Primitive stream
    int sum = IntStream.of(1, 2, 3, 4, 5)
        .sum();
  2. Sắp xếp các thao tác hiệu quả

    // Tồi: Map tất cả, sau đó filter
    list.stream()
        .map(expensiveTransform)
        .filter(result -> result.isValid())
        .toList();
     
    // Tốt: Filter trước (ít thao tác đắt tiền hơn)
    list.stream()
        .filter(item -> cheapCheck(item))
        .map(expensiveTransform)
        .toList();
  3. Sử dụng các phương thức chuyên biệt

    // Tồi: Reduce chung chung
    long count = stream.reduce(0L, (acc, item) -> acc + 1, Long::sum);
     
    // Tốt: Count built-in
    long count = stream.count();

Rõ ràng Code

  1. Trích xuất các lambda phức tạp

    // Tồi: Khó đọc
    employees.stream()
        .filter(e -> e.department().equals("Engineering") && 
                     e.salary() > 80000 && 
                     e.age() < 40)
        .toList();
     
    // Tốt: Predicate có tên
    Predicate<Employee> isYoungHighPaidEngineer = e ->
        e.department().equals("Engineering") &&
        e.salary() > 80000 &&
        e.age() < 40;
     
    List<Employee> result = employees.stream()
        .filter(isYoungHighPaidEngineer)
        .toList();
  2. Giữ streams ngắn

    // Tồi: Quá nhiều thao tác
    result = data.stream().filter(...).map(...).flatMap(...).sorted(...)
        .distinct().skip(...).limit(...).filter(...).map(...).toList();
     
    // Tốt: Chia thành các bước logic
    Stream<Item> filtered = data.stream()
        .filter(predicate)
        .map(transformer);
     
    Stream<Item> processed = filtered
        .flatMap(nestedMapper)
        .sorted();
     
    List<Item> result = processed
        .distinct()
        .toList();

Kết nối với Spring Boot

Streams rất cần thiết trong các ứng dụng Spring Boot (được đề cập trong Giai đoạn 3 của Lộ trình Java):

@Service
public class UserService {
    
    // Repository trả về Stream cho hiệu quả bộ nhớ
    @Transactional(readOnly = true)
    public List<UserDTO> getActiveUsers() {
        try (Stream<User> users = userRepository.findAllByActiveTrue()) {
            return users
                .filter(user -> user.getLastLoginDate()
                    .isAfter(LocalDate.now().minusMonths(6)))
                .map(this::convertToDTO)
                .toList();
        }
    }
    
    // Xử lý song song cho các thao tác batch
    public void sendNotifications(List<User> users) {
        users.parallelStream()
            .forEach(user -> {
                notificationService.send(user);
            });
    }
}

Tóm tắt

Java Streams và lập trình hàm cho phép:

  • Code sạch hơn: Xử lý dữ liệu khai báo, dễ đọc
  • Ít boilerplate hơn: Không cần vòng lặp thủ công và biến tạm thời
  • Hiệu suất tốt hơn: Song song hóa dễ dàng cho các dataset lớn
  • Có thể kết hợp: Móc nối các thao tác một cách tự nhiên

Điểm chính:

  • Thành thạo lambda expressions và method references
  • Hiểu lazy evaluation và vòng đời stream
  • Sử dụng collectors cho tổng hợp mạnh mẽ
  • Tận dụng Optional cho null safety
  • Biết khi KHÔNG nên sử dụng streams (đơn giản hơn phức tạp)
  • Sử dụng parallel streams một cách thận trọng

Bước tiếp theo:

  • Xây dựng một pipeline xử lý dữ liệu với streams
  • Refactor code mệnh lệnh sang phong cách hàm
  • Thực hành với các bài toán stream trên LeetCode/HackerRank
  • Khám phá các collectors nâng cao và triển khai tùy chỉnh
  • Áp dụng trong các REST APIs Spring Boot (Giai đoạn 3)

Bắt đầu với các chuyển đổi đơn giản và dần dần xây dựng các pipeline phức tạp. Lập trình hàm không phải là về việc sử dụng streams ở khắp mọi nơi—mà là về việc chọn đúng công cụ cho công việc.


Thuộc series Lộ Trình Học Java

Quay lại: Giai Đoạn 3: Core Java APIs

Các Deep Dive Liên Quan:

Bước Tiếp Theo: Bắt Đầu Với Spring Boot

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.