0
0
Lập trình
Thaycacac
Thaycacac thaycacac

Làm Chủ Java Stream API: Hướng Dẫn Toàn Diện về Lập Trình Hàm

Đăng vào 6 tháng trước

• 8 phút đọc

Chủ đề:

KungFuTech

Giới Thiệu

Java Stream API, được giới thiệu trong Java 8, đã cách mạng hóa cách thức xử lý bộ sưu tập và dữ liệu trong Java. Bằng cách mang đến các khái niệm lập trình hàm cho ngôn ngữ, các dòng (streams) cho phép lập trình viên viết mã ngắn gọn, dễ đọc và dễ bảo trì hơn. Khác với các phương pháp truyền thống tập trung vào "cách" xử lý dữ liệu, các dòng nhấn mạnh vào "những gì" cần thực hiện, dẫn đến mã lệnh mang tính khai báo và biểu đạt hơn.

Một dòng là một chuỗi các phần tử hỗ trợ các thao tác tổng hợp tuần tự và song song. Hãy tưởng tượng nó như một đường ống nơi dữ liệu chảy qua nhiều giai đoạn biến đổi và lọc trước khi đến kết quả cuối cùng.

Tạo Dòng

Từ Bộ Sưu Tập

Cách phổ biến nhất để tạo dòng là từ các bộ sưu tập có sẵn:

java Copy
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
Stream<String> nameStream = names.stream();

// Để xử lý song song
Stream<String> parallelStream = names.parallelStream();

Từ Mảng

java Copy
String[] array = {"apple", "banana", "cherry"};
Stream<String> streamFromArray = Arrays.stream(array);

// Với khoảng
IntStream rangeStream = Arrays.stream(new int[]{1, 2, 3, 4, 5});

Sử Dụng Stream.of()

java Copy
Stream<String> directStream = Stream.of("one", "two", "three");
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);

Dòng Vô Hạn và Dòng Theo Khoảng

java Copy
// Dòng vô hạn với generate
Stream<Double> randomStream = Stream.generate(Math::random);

// Dòng vô hạn với iterate
Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2);

// Dòng theo khoảng cho các kiểu nguyên thủy
IntStream range = IntStream.range(1, 10); // 1 đến 9
IntStream rangeClosed = IntStream.rangeClosed(1, 10); // 1 đến 10

Từ Tệp và I/O

java Copy
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
    lines.forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}

Các Thao Tác Trung Gian

Các thao tác trung gian biến đổi các dòng và là lười biếng — chúng không thực thi cho đến khi một thao tác cuối cùng được gọi. Chúng trả về một dòng mới, cho phép nối phương thức.

map() - Biến Đổi

Phép toán map() biến đổi mỗi phần tử bằng cách sử dụng một hàm được cung cấp:

java Copy
List<String> names = Arrays.asList("alice", "bob", "charlie");
List<String> upperCaseNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// Kết quả: [ALICE, BOB, CHARLIE]

// Biến đổi sang loại khác
List<Integer> nameLengths = names.stream()
    .map(String::length)
    .collect(Collectors.toList());
// Kết quả: [5, 3, 7]

Trường hợp sử dụng thực tế: Chuyển đổi DTO sang thực thể hoặc trích xuất các trường cụ thể từ các đối tượng.

java Copy
List<Employee> employees = getEmployees();
List<String> employeeEmails = employees.stream()
    .map(Employee::getEmail)
    .collect(Collectors.toList());

filter() - Lọc Theo Điều Kiện

Phép toán filter() giữ lại các phần tử phù hợp với một điều kiện nhất định:

java Copy
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
// Kết quả: [2, 4, 6, 8, 10]

Trường hợp sử dụng thực tế: Lọc người dùng hoạt động hoặc sản phẩm trong một khoảng giá.

java Copy
List<User> activeAdultUsers = users.stream()
    .filter(User::isActive)
    .filter(user -> user.getAge() >= 18)
    .collect(Collectors.toList());

sorted() - Sắp Xếp Các Phần Tử

java Copy
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
List<String> sortedNames = names.stream()
    .sorted()
    .collect(Collectors.toList());
// Kết quả: [Alice, Bob, Charlie]

distinct() - Xóa Các Phần Tử Trùng Lặp

java Copy
List<Integer> numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3, 4);
List<Integer> uniqueNumbers = numbersWithDuplicates.stream()
    .distinct()
    .collect(Collectors.toList());
// Kết quả: [1, 2, 3, 4]

limit() và skip() - Cắt Dòng

java Copy
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Lấy 5 phần tử đầu tiên
List<Integer> firstFive = numbers.stream()
    .limit(5)
    .collect(Collectors.toList());
// Kết quả: [1, 2, 3, 4, 5]

Trường hợp sử dụng thực tế: Triển khai phân trang.

java Copy
public List<Product> getProductsPage(int page, int size) {
    return products.stream()
        .skip((page - 1) * size)
        .limit(size)
        .collect(Collectors.toList());
}

peek() - Gỡ Lỗi và Tác Động Phụ

Phép toán peek() thực hiện một tác động phụ trên mỗi phần tử mà không thay đổi dòng:

java Copy
List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .peek(System.out::println) // Gỡ lỗi: in ra các tên đã lọc
    .map(String::toUpperCase)
    .peek(name -> System.out.println("Chữ hoa: " + name))
    .collect(Collectors.toList());

Lưu ý: peek() nên được sử dụng chủ yếu cho việc gỡ lỗi. Tránh sử dụng nó cho logic kinh doanh.

Các Thao Tác Cuối

Các thao tác cuối tạo ra kết quả cuối cùng và kích hoạt việc thực thi của chuỗi dòng.

forEach() - Lặp Qua Các Phần Tử

java Copy
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().forEach(System.out::println);

collect() - Tập Hợp Kết Quả

Phép toán collect() là thao tác cuối cùng đa năng nhất:

java Copy
List<String> list = stream.collect(Collectors.toList());

reduce() - Tính Toán

Phép toán reduce() kết hợp các phần tử của dòng thành một kết quả duy nhất:

java Copy
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
    .reduce(Integer::sum);

count() - Đếm Số Phần Tử

java Copy
long count = names.stream()
    .filter(name -> name.startsWith("A"))
    .count();

Dòng Song Song

Dòng song song tận dụng nhiều lõi CPU để xử lý dữ liệu đồng thời, có thể cải thiện hiệu suất cho các thao tác tốn CPU trên các tập dữ liệu lớn.

Tạo Dòng Song Song

java Copy
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> parallelStream = numbers.parallelStream();

So Sánh Hiệu Suất

java Copy
List<Integer> largeList = IntStream.rangeClosed(1, 10_000_000)
    .boxed()
    .collect(Collectors.toList());

Khi Nào Sử Dụng Dòng Song Song

Sử Dụng Dòng Song Song Khi Tránh Sử Dụng Dòng Song Song Khi
Tập dữ liệu lớn (10,000+ phần tử) Tập dữ liệu nhỏ
Thao tác tốn CPU Thao tác I/O
Các thao tác độc lập Các thao tác có trạng thái
Hệ thống đa lõi Hệ thống đơn lõi
Các thao tác giao hoán và kết hợp Các thao tác phụ thuộc vào thứ tự

Các Lưu Ý về Hiệu Suất

Dòng so với Vòng Lặp Truyền Thống

java Copy
// Cách tiếp cận truyền thống
List<String> result = new ArrayList<>();
for (Person person : persons) {
    if (person.getAge() > 18) {
        result.add(person.getName().toUpperCase());
    }
}

// Cách tiếp cận dòng
List<String> streamResult = persons.stream()
    .filter(person -> person.getAge() > 18)
    .map(person -> person.getName().toUpperCase())
    .collect(Collectors.toList());

Mẹo Tối Ưu Hiệu Suất

  1. Sử dụng dòng nguyên thủy khi có thể: IntStream, LongStream, DoubleStream tránh chi phí đóng gói.

  2. Các thao tác ngắn mạch: Sử dụng findFirst(), findAny(), anyMatch(), v.v. khi bạn không cần tất cả kết quả.

Ví Dụ Ứng Dụng Phức Tạp

Ví Dụ 1: Xử Lý Đơn Hàng Thương Mại Điện Tử

java Copy
public class OrderProcessor {
    public OrderSummary processOrders(List<Order> orders) {
        // Xử lý các đơn hàng
    }
}

Ví Dụ 2: Quy Trình Phân Tích Dữ Liệu

java Copy
public class DataAnalyzer {
    public AnalysisResult analyzeUserBehavior(List<UserActivity> activities) {
        // Phân tích hành vi người dùng
    }
}

Thực Hành Tốt Nhất

1. Ưu Tiên Tham Chiếu Phương Thức

java Copy
// Sử dụng tham chiếu phương thức
names.stream().map(String::toUpperCase);

2. Sử Dụng Các Collector Phù Hợp

java Copy
Set<String> set = stream.collect(Collectors.toSet());

3. Xử Lý Optional Một Cách Đúng Đắn

java Copy
String result = optionalStream.findFirst().orElse("default");

Các Cạm Bẫy Thường Gặp và Cách Tránh Chúng

1. Tái Sử Dụng Các Dòng

java Copy
Stream<String> stream = names.stream();
stream.forEach(System.out::println);
stream.count(); // IllegalStateException!

2. Tác Động Phụ Trong Các Thao Tác Dòng

java Copy
List<String> results = new ArrayList<>();
names.stream()
    .filter(name -> {
        results.add(name); // Tác động phụ!
        return name.startsWith("A");
    })
    .collect(Collectors.toList());

3. Sử Dụng Quá Nhiều Dòng Song Song

java Copy
List<String> smallList = Arrays.asList("a", "b", "c");
smallList.parallelStream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

4. Quên Xử Lý Các Dòng Trống

java Copy
String first = names.stream()
    .filter(name -> name.startsWith("Z"))
    .findFirst()
    .orElse("Not found");

Kết Luận

Java Stream API đại diện cho một sự chuyển mình trong lập trình Java, mang đến các khái niệm lập trình hàm cho ngôn ngữ vốn có tính đối tượng. Bằng cách làm chủ các dòng, lập trình viên có thể viết mã biểu đạt, ngắn gọn và dễ bảo trì hơn.

Lợi Ích Chính của Stream API:

  1. Cải Thiện Tính Đọc Được: Các thao tác dòng đọc như ngôn ngữ tự nhiên, làm mã tự tài liệu hóa.
  2. Giảm Mã Thừa: Loại bỏ các vòng lặp và câu lệnh điều kiện rườm rà.
  3. Tốt Hơn về Trừu Tượng: Tập trung vào việc cần làm thay vì cách làm.
  4. Xử Lý Song Song: Dễ dàng xử lý song song để cải thiện hiệu suất.
  5. Khả Năng Kết Hợp: Các thao tác có thể được nối và kết hợp linh hoạt.
  6. Tính Bất Biến: Khuyến khích các nguyên tắc lập trình hàm và giảm thiểu tác động phụ.

Tác Động Đến Năng Suất:

  • Phát Triển Nhanh Hơn: Ít mã hơn để viết và bảo trì.
  • Ít Lỗi Hơn: Cách tiếp cận hàm giảm thiểu vấn đề trạng thái có thể thay đổi.
  • Kiểm Thử Tốt Hơn: Các hàm thuần túy dễ kiểm thử hơn.
  • Tăng Cường Đánh Giá Mã: Mã dễ đọc hơn dẫn đến tốt hơn trong hợp tác.

Stream API không thay thế tất cả các vòng lặp truyền thống, nhưng nó cung cấp một lựa chọn mạnh mẽ thường cho kết quả mã sạch hơn và dễ bảo trì hơn. Bắt đầu với các biến đổi và thao tác lọc đơn giản, từ từ kết hợp các mẫu phức tạp hơn khi bạn trở nên thoải mái với tư duy lập trình hàm.

Bằng cách chấp nhận các dòng, các lập trình viên Java có thể viết mã không chỉ thanh lịch hơn mà còn phù hợp hơn với các thực tiễn lập trình hiện đại, làm cho các ứng dụng của họ trở nên mạnh mẽ và dễ bảo trì hơn trong thời gian dài.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào