CodeGym /Các khóa học /JAVA 25 SELF /Stream nguyên thủy và chi phí boxing

Stream nguyên thủy và chi phí boxing

JAVA 25 SELF
Mức độ, Bài học
Có sẵn

1. Vấn đề: vì sao Stream thông thường không phải lúc nào cũng hiệu quả

Khi bạn làm việc với các collection số trong Java và dùng stream thông thường (Stream<Integer>, Stream<Double>), dưới lớp vỏ sẽ diễn ra “đóng gói” (boxing) và “mở gói” (unboxing) các giá trị nguyên thủy vào/ra các đối tượng wrapper (Integer, Double v.v.). Điều này tiện lợi, nhưng không phải lúc nào cũng hiệu quả:

  • Boxing — biến kiểu nguyên thủy (int) thành đối tượng (Integer).
  • Unboxing — thao tác ngược lại: từ đối tượng về kiểu nguyên thủy.

Vấn đề:
Boxing/unboxing là các thao tác thừa và tốn bộ nhớ. Ở những đoạn mã “nóng” (được gọi thường xuyên), điều này có thể dẫn đến suy giảm hiệu năng đáng kể, đặc biệt khi bạn xử lý các mảng số lớn.

Ví dụ:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);

Ở đây mỗi số là một đối tượng Integer, chứ không phải kiểu nguyên thủy int.

2. Stream nguyên thủy: IntStream, LongStream, DoubleStream

Để tránh boxing/unboxing không cần thiết, Java có các stream nguyên thủy:

  • IntStream — cho int
  • LongStream — cho long
  • DoubleStream — cho double

Chúng chỉ làm việc với kiểu nguyên thủy và không tạo thêm đối tượng wrapper.

Cách tạo stream nguyên thủy?

Từ mảng:

int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);

Bằng range/rangeClosed:

IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(1, 5)     // 1..5 bao gồm điểm cuối

Sinh số ngẫu nhiên:

new Random().ints(5, 0, 100)    // 5 số int ngẫu nhiên từ 0 đến 99

Ví dụ: tính tổng các phần tử của mảng

int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15

3. Chuyển đổi giữa Stream<T> và stream nguyên thủy

Đôi khi bạn có stream thông thường, đôi khi là stream nguyên thủy. Để chuyển đổi, dùng các phương thức chuyên biệt:

  • mapToInt, mapToLong, mapToDouble — chuyển stream thông thường thành stream nguyên thủy.
  • boxed() — biến stream nguyên thủy trở lại thành stream đối tượng.

Ví dụ:

List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3

Ngược lại:

IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();

Lưu ý:
boxed() là thao tác ngược, nó lại tạo các đối tượng wrapper (Integer, Double v.v.), và nếu bạn cần hiệu năng tối đa — hãy tránh nó ở những đoạn mã “nóng”.

4. Tổng hợp và thống kê: sum, average, min, max, summaryStatistics

Các stream nguyên thủy có những phương thức tiện lợi để tổng hợp:

  • sum() — tổng của tất cả phần tử
  • average() — giá trị trung bình (trả về OptionalDouble)
  • min(), max() — giá trị nhỏ nhất và lớn nhất (OptionalInt, OptionalLong, OptionalDouble)
  • summaryStatistics() — trả về một đối tượng thống kê đầy đủ (tổng, trung bình, nhỏ nhất, lớn nhất, số lượng)

Ví dụ:

int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum());      // 15
System.out.println(stats.getAverage());  // 3.0
System.out.println(stats.getMin());      // 1
System.out.println(stats.getMax());      // 5
System.out.println(stats.getCount());    // 5

So sánh với Collectors:
Đối với stream thông thường có thể dùng Collectors.summarizingInt, nhưng thường kém hiệu quả hơn so với các phương thức của stream nguyên thủy:

List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));

5. Tránh auto-boxing: khi nào quan trọng và đo lường thế nào

Khi nào là then chốt?

  • Trong các vòng lặp và stream xử lý khối lượng lớn số (ví dụ: xử lý mảng, thống kê, toán học, phân tích dữ liệu).
  • Ở các đoạn mã “nóng” — mã được gọi thường xuyên và ảnh hưởng đến hiệu năng.

Tại sao điều này quan trọng?

  • Mỗi lần boxing sẽ tạo một đối tượng wrapper mới (Integer, Double v.v.).
  • Điều này làm tăng tải cho bộ gom rác (GC).
  • Trong microbenchmark, chênh lệch có thể gấp nhiều lần!

Đo thế nào?
Để so sánh chính xác, hãy dùng framework JMH (Java Microbenchmark Harness). Nó cho phép so sánh hiệu năng một cách công bằng giữa mã có boxing và không boxing.

Ví dụ (pseudo-code):

@Benchmark
public int sumIntStream() {
    return IntStream.range(0, 1_000_000).sum();
}

@Benchmark
public int sumStreamInteger() {
    return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}

Phương án thứ hai sẽ chậm hơn do liên tục tạo các đối tượng Integer.

Kết luận:
Nếu hiệu năng quan trọng — hãy dùng stream nguyên thủy!

6. Khi nào stream nguyên thủy thực sự giúp ích, và khi nào không nên phức tạp hóa

Hãy dùng stream nguyên thủy nếu:

  • Bạn làm việc với các mảng/collection số lớn.
  • Bạn cần nhanh chóng tính tổng, trung bình, nhỏ nhất, lớn nhất.
  • Bạn viết mã mà mỗi mili-giây đều quan trọng (ví dụ: xử lý dữ liệu theo thời gian thực).

Có thể không cần bận tâm nếu:

  • Collection nhỏ (vài chục phần tử trở xuống).
  • Mã trở nên quá phức tạp do chuyển đổi qua lại giữa các kiểu.
  • Hiệu năng không phải yếu tố then chốt (ví dụ: xử lý nhập liệu người dùng).

Ví dụ:

List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Có thể, nhưng reduce thông thường cũng ổn

Lời khuyên: đừng biến code thành “rừng rậm” chỉ vì tối ưu hóa vi mô. Hãy dùng stream nguyên thủy ở nơi thực sự hợp lý.

7. OptionalInt, OptionalDouble: lấy kết quả an toàn

Các phương thức min(), max(), average() của stream nguyên thủy trả về không chỉ một con số, mà là một “wrapper” — OptionalInt, OptionalDouble v.v. Điều này cần thiết để xử lý an toàn các stream rỗng (ví dụ khi mảng rỗng).

Ví dụ:

int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
    System.out.println("Giá trị nhỏ nhất: " + min.getAsInt());
} else {
    System.out.println("Mảng rỗng!");
}

So sánh với Optional thông thường:

  • OptionalInt — cho int
  • OptionalDouble — cho double
  • OptionalLong — cho long

Vì sao không chỉ trả về 0?
Bởi vì 0 có thể là giá trị hợp lệ, còn stream rỗng là một tình huống riêng.

Ví dụ với average:

double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // nếu rỗng — trả về NaN

8. Thực hành: ví dụ sử dụng stream nguyên thủy

Ví dụ 1: Tổng bình phương các số từ 1 đến 1000

int sum = IntStream.rangeClosed(1, 1000)
                   .map(x -> x * x)
                   .sum();
System.out.println(sum);

Ví dụ 2: Lọc và đếm các số chẵn

int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
                   .filter(x -> x % 2 == 0)
                   .count();
System.out.println("Số lượng số chẵn: " + count);

Ví dụ 3: So sánh với Stream<Integer> thông thường

List<Integer> list = IntStream.range(0, 1_000_000)
                              .boxed()
                              .collect(Collectors.toList());

long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");

t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");

Với khối lượng lớn, sự khác biệt sẽ rõ rệt.

9. Các lỗi thường gặp khi làm việc với stream nguyên thủy

Lỗi số 1: Quên về boxing khi chuyển đổi kiểu.
Nếu bạn dùng boxed(), hãy nhớ rằng nó lại tạo các đối tượng wrapper. Đừng dùng nếu không cần thiết.

Lỗi số 2: Dùng stream nguyên thủy cho đối tượng.
IntStream chỉ hoạt động với int, không phải với đối tượng. Nếu bạn cần làm việc với đối tượng — hãy dùng Stream<T> thông thường.

Lỗi số 3: Bỏ qua OptionalInt/OptionalDouble.
Nếu bạn gọi min(), max(), average() — luôn kiểm tra có kết quả hay không (isPresent()), nếu không sẽ ném ngoại lệ.

Lỗi số 4: Chuyển đổi quá phức tạp giữa Stream<T> và IntStream.
Nếu mã trở nên khó đọc vì liên tục mapToInt() → boxed() → mapToDouble() — có lẽ bạn nên đơn giản hóa logic.

Lỗi số 5: Trông chờ “tăng tốc kỳ diệu” trên collection nhỏ.
Với danh sách nhỏ, khác biệt giữa Stream và IntStream là rất ít. Không nên làm phức tạp mã chỉ để tiết kiệm vi mô.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Tính tổng doanh thu hàng ngày của cửa hàng 💰
Tính tổng doanh thu hàng ngày của cửa hàng 💰
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Phân tích độ dài câu thần chú cho Học viện Phép thuật ✨
Phân tích độ dài câu thần chú cho Học viện Phép thuật ✨
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION