Hướng dẫn入门 Java Streams

Xin chào các nhà pháp sư Java tương lai! Hôm nay, chúng ta sẽ bắt đầu một hành trình thú vị vào thế giới của Java Streams. Đừng lo lắng nếu bạn là người mới bắt đầu lập trình - tôi sẽ là người hướng dẫn thân thiện của bạn, và chúng ta sẽ cùng nhau từng bước. Cuối bài hướng dẫn này, bạn sẽ biết cách xử lý dữ liệu như một chuyên gia!

Java - Streams

Java Streams là gì?

Hãy tưởng tượng bạn đang ở một nhà hàng sushi tự động. Các đĩa sushi liên tục di chuyển qua bạn, và bạn có thể chọn những gì bạn muốn. Đó chính là Esscence của Stream trong Java - nó là một chuỗi các phần tử mà bạn có thể xử lý từng cái một, mà không cần phải lưu trữ chúng tất cả trong bộ nhớ cùng một lúc.

Streams được giới thiệu trong Java 8 để làm cho việc làm việc với các bộ dữ liệu trở nên dễ dàng và hiệu quả hơn. Chúng cho phép chúng ta thực hiện các thao tác trên dữ liệu theo cách thưa minh hơn, nói với Java điều chúng ta muốn làm thay vì cách làm nó.

Tạo Streams trong Java

Hãy bắt đầu bằng cách tạo Stream đầu tiên của chúng ta. Có nhiều cách để làm điều này, nhưng chúng ta sẽ bắt đầu với một ví dụ đơn giản:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {
public static void main(String[] args) {
// Tạo Stream từ một List
List<String> fruits = Arrays.asList("apple", "banana", "cherry", "date");
Stream<String> fruitStream = fruits.stream();

// Tạo Stream trực tiếp
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);
}
}

Trong ví dụ này, chúng ta đã tạo hai Streams. fruitStream được tạo từ một List các quả trái cây. numberStream được tạo trực tiếp bằng cách sử dụng phương thức Stream.of().

Các thao tác Stream phổ biến

Bây giờ chúng ta đã có Streams, hãy nhìn vào một số thao tác phổ biến mà chúng ta có thể thực hiện trên chúng.

Phương thức forEach

Phương thức forEach giống như một robot thân thiện đi qua từng phần tử trong Stream và làm điều gì đó với nó. Hãy sử dụng nó để in ra các quả trái cây:

fruits.stream().forEach(fruit -> System.out.println(fruit));

Điều này sẽ in ra:

apple
banana
cherry
date

Ở đây, fruit -> System.out.println(fruit) được gọi là một biểu thức lambda. Đây là một cách viết ngắn gọn để nói với Java điều cần làm với từng phần tử.

Phương thức map

Phương thức map giống như một cây cậy ma thuật biến đổi từng phần tử trong Stream. Hãy sử dụng nó để viết hoa tất cả các quả trái cây:

List<String> upperCaseFruits = fruits.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseFruits);

Điều này sẽ in ra:

[APPLE, BANANA, CHERRY, DATE]

String::toUpperCase là một tham chiếu phương thức, một tính năng hay của Java, tương đương với "sử dụng phương thức toUpperCase trên từng String".

Phương thức filter

Phương thức filter giống như một bảo vệ câu lạc bộ, chỉ cho phép một số phần tử qua. Hãy sử dụng nó để giữ lại chỉ các quả trái cây bắt đầu bằng chữ 'a':

List<String> aFruits = fruits.stream()
.filter(fruit -> fruit.startsWith("a"))
.collect(Collectors.toList());
System.out.println(aFruits);

Điều này sẽ in ra:

[apple]

Phương thức limit

Phương thức limit giống như nói "Tôi chỉ muốn số này, cảm ơn!" Nó giới hạn Stream chỉ với một số lượng phần tử nhất định:

List<String> firstTwoFruits = fruits.stream()
.limit(2)
.collect(Collectors.toList());
System.out.println(firstTwoFruits);

Điều này sẽ in ra:

[apple, banana]

Phương thức sorted

Phương thức sorted giống như sắp xếp kệ sách của bạn. Nó sắp xếp các phần tử theo thứ tự:

List<String> sortedFruits = fruits.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedFruits);

Điều này sẽ in ra:

[apple, banana, cherry, date]

Xử lý song song

Một trong những điều tuyệt vời về Streams là chúng có thể dễ dàng được xử lý song song, có thể tăng tốc các thao tác trên các bộ dữ liệu lớn. Bạn có thể tạo một Stream song song như sau:

fruits.parallelStream().forEach(fruit -> System.out.println(fruit + " " + Thread.currentThread().getName()));

Điều này có thể in ra điều gì đó như:

banana ForkJoinPool.commonPool-worker-1
apple main
date ForkJoinPool.commonPool-worker-2
cherry ForkJoinPool.commonPool-worker-3

Lần tiếp theo bạn chạy nó, thứ tự có thể khác vì các quả trái cây được xử lý song song!

Collectors

Collectors là những công cụ đặc biệt giúp chúng ta thu thập kết quả của các thao tác Stream. Chúng ta đã sử dụng collect(Collectors.toList()) trong các ví dụ của mình để chuyển đổi kết quả Stream thành Lists. Có rất nhiều Collectors hữu ích:

// Nối các phần tử lại thành một chuỗi String
String fruitString = fruits.stream().collect(Collectors.joining(", "));
System.out.println(fruitString);  // In ra: apple, banana, cherry, date

// Đếm số phần tử
long fruitCount = fruits.stream().collect(Collectors.counting());
System.out.println(fruitCount);  // In ra: 4

// Nhóm các phần tử
Map<Character, List<String>> fruitGroups = fruits.stream()
.collect(Collectors.groupingBy(fruit -> fruit.charAt(0)));
System.out.println(fruitGroups);  // In ra: {a=[apple], b=[banana], c=[cherry], d=[date]}

Thống kê

Streams cũng có thể giúp chúng ta tính toán thống kê trên dữ liệu số:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt(Integer::intValue).summaryStatistics();
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("Average: " + stats.getAverage());

Điều này sẽ in ra:

Count: 5
Sum: 15
Min: 1
Max: 5
Average: 3.0

Ví dụ Java Streams

Hãy kết hợp tất cả lại với một ví dụ phức tạp hơn. Hãy tưởng tượng chúng ta có một danh sách học sinh với tuổi và điểm số của họ:

class Student {
String name;
int age;
double grade;

Student(String name, int age, double grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}

public class StreamExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Alice", 22, 90.5),
new Student("Bob", 20, 85.0),
new Student("Charlie", 21, 92.3),
new Student("David", 23, 88.7)
);

// Lấy điểm trung bình của học sinh trên 21 tuổi
double averageGrade = students.stream()
.filter(student -> student.age > 21)
.mapToDouble(student -> student.grade)
.average()
.orElse(0.0);

System.out.println("Điểm trung bình của học sinh trên 21 tuổi: " + averageGrade);

// Lấy tên của 2 học sinh có điểm cao nhất
List<String> topStudents = students.stream()
.sorted((s1, s2) -> Double.compare(s2.grade, s1.grade))
.limit(2)
.map(student -> student.name)
.collect(Collectors.toList());

System.out.println("2 học sinh có điểm cao nhất: " + topStudents);
}
}

Ví dụ này minh họa cách chúng ta có thể chaining nhiều thao tác Stream lại với nhau để thực hiện các nhiệm vụ xử lý dữ liệu phức tạp một cách ngắn gọn và dễ đọc.

Và thế là bạn đã bước những bước đầu tiên vào thế giới của Java Streams. Nhớ rằng, thực hành là cách tốt nhất để trở nên hoàn hảo, vì vậy đừng ngần ngại thử nghiệm với các khái niệm này. Trước khi bạn biết, bạn sẽ xử lý dữ liệu như một chuyên gia! Chúc các bạn lập trình vui vẻ!

Credits: Image by storyset