Tong Hoang Vu
Tong Hoang Vu's blog

Tong Hoang Vu's blog

Các tính chất của Java stream (phần 1)

Các tính chất của Java stream (phần 1)

Java Stream API

Tong Hoang Vu's photo
Tong Hoang Vu
·Sep 30, 2021·

6 min read

Subscribe to my newsletter and never miss my upcoming articles

Hello, tớ đã quay lại với blog rồi đây. Dạo này hơi bận tí, thêm nữa các chủ đề nâng cao này khá khó viết nên lịch ra bài sẽ chậm hơn nhé. Thông cảm nha OTL.

Bài hôm nay sẽ giới thiệu về các tính chất của stream, gồm laziness, short-circuiting, stateful/stateless. Nhưng mà chưa hết đâu, sẽ còn tiếp phần sau nữa nhé. Những bài sau này có hơi nặng về lí thuyết một tí, nhưng nếu nắm rõ được sẽ giúp bạn viết code stream tốt hơn đấy.

1. Laziness

Hãy bắt đầu với tính chất đơn giản nhất là laziness.

Các intermediate operation không được thực thi, cho đến khi terminal operation được gọi. Intermediate operation luôn là lazy.

// Chưa chạy
List.of(2, 3, 5, 7).stream()
    .filter(e -> e % 2 == 0);

// Tới khi có terminal operation mới chạy
List.of(2, 3, 5, 7).stream()
    .filter(e -> e % 2 == 0)
    .forEach(System.out::println);

Lợi ích của lazy như:

  • Hoạt động tốt với các stream vô hạn (không auto start)
  • Di chuyển, xử lý các stream object dễ dàng (không lo nó chạy 😂)

Tính chất này gợi nhớ về hai loại hình lập trình là imperative (mệnh lệnh) và declarative (khai báo). Imperative là viết ra các lệnh và thực hiện ngay lập tức. Trong khi đó declarative chỉ "khai báo" rằng đây là những việc sẽ làm, chứ không thực hiện ngay, do đó giúp stream có được tính lazy.

2. Short-circuiting

Có thể bạn đã nghe về short-circuiting của các toán tử như ANDOR, nghĩa là sẽ break ngay giữa chừng mà không cần tính toán toàn bộ biểu thức.

// Nếu a <= 10 thì biểu thức là false ngay
// Không cần so sánh b < 5 nữa
if (a > 10 && b < 5) ...

// Nếu x > 0 thì biểu thức là true ngay
// Không cần so sánh y > 0 nữa
boolean result = x > 0 || y > 0;

Đây có thể xem là một dạng tối ưu hóa của Java (và các ngôn ngữ khác). Giúp hiệu suất tốt hơn do không phải tính toán toàn bộ, chỉ tính các phần cần thiết. Đặc biệt khi biểu thức chứa các hàm tính toán nặng.

Với stream cũng thế, stream có một vài operation được gọi là short-circuiting, gồm:

  • Các operation trung gian như limit(), takeWhile()
  • Các terminal operation như findFirst(), findAny(), allMatch(), noneMatch(), anyMatch()

Các operation bình thường sẽ xử lý toàn bộ phần tử trong stream, tuy nhiên short-circuiting operation chỉ xử lý một số lượng vừa đủ các phần tử và xong. Nói cách khác, các operation này sẽ dừng ngay khi đạt được kết quả mong muốn, mà không phải xử lý toàn bộ stream.

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Chỉ in 5 số đầu tiên rồi dừng lại, các số còn lại không được xử lý
// Khi số phần tử đạt max là 5 là đủ yêu cầu rồi, không cần tính nữa
nums.stream().limit(5).forEach(System.out::println);

// Chỉ trả về số đầu tiên rồi dừng, các số còn lại bỏ qua
// Khi có 1 phần tử là đạt yêu cầu, dừng luôn không cần tính nữa
nums.stream().filter(e -> e > 5).findFirst();

Mỗi operation sẽ có một yêu cầu khác nhau, dựa vào ý nghĩa có thể suy ra được. Ví dụ allMatch() trả về true nếu mọi element khớp điều kiện. Và nếu có bất kì element nào không thỏa, nó sẽ return false ngay mà không phải xét các element còn lại.

Ngoài ra, các short-circuiting operations có khả năng giới hạn stream vô hạn thành hữu hạn. Tuy nhiên, việc xảy ra short-circuiting là điều kiện cần, nhưng chưa đủ để đảm bảo 100% sẽ giới hạn được.

Having a short-circuiting operation in the pipeline is a necessary, but not sufficient, condition for the processing of an infinite stream to terminate normally in finite time.

Nguồn: https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/util/stream/package-summary.html#StreamOps.

3. Stateful/stateless behavioral parameters

Đầu tiên cần biết về behavioral param là gì. Đó là các tham số của operation, mà bản thân các param này có thể thực thi hành động gì đó. Ví dụ.

Stream.of(1, 2, 3).map(e -> {
    System.out.println(e);
    return 2 * e;
})

Thì tham số của map() gọi là behavioral parameter.

Các operation có thể là stateful hoặc stateless. Tuy nhiên, thực tế vẫn có thể biến stateless operation thành stateful bằng code trong behavioral parameter của nó có tính stateful (ngược lại thì không được). Lý do vì sao sẽ nói ở phần dưới.

3.1 Stateless operation

Stateless operation là các operation, khi xử lý một element, không phụ thuộc vào việc xử lý các phần tử khác. Nói cách khác, các element được xử lý riêng rẽ, độc lập với nhau, không có state chung nào được chia sẻ giữa chúng (stateless).

Hầu hết operation là stateless, ví dụ map(), filter(),... Khi filter() phần tử thứ 5 thì không quan tâm là phần tử thứ 4, thứ 3,... có kết quả là gì. Đó là stateless.

3.2. Stateful operation

Stateless operation là các operation giữ lại một state chung khi xử lý các phần tử. Nghĩa là việc xử lý một element hiện tại có thể sử dụng state từ các element trước.

Có 6 operation sau là stateful:

  • limit()skip()
  • sorted()
  • distinct()
  • takeWhile()dropWhile()

Nói chung khi xét operation có stateful không thì hãy xem nó có lưu giữ lại bất kì state nào giữa các phần tử hay không.

3.3. Vấn đề với stateful

Vậy tại sao lại cần quan tâm tới operation là stateful hay stateless?

Thứ nhất, stateful operation không thể hoạt động trên stream vô hạn.

Ví dụ như gọi sorted() trên stream vô hạn, rõ ràng là không được. Tuy nhiên, limit() là trường hợp đặc biệt, vì nó có cả tính short-circuting, sẽ giúp ngắt stream vô hạn thành hữu hạn.

Thứ hai, stateful behavioral param với mutable state có thể xảy ra vấn đề.

Sử dụng stateful operation sẽ không vấn đề gì, vì internal state được Java xử lý kĩ rồi. Tuy nhiên, với behavioral param có tính stateful (do bạn code ra và gắn vô operation), nếu có sử dụng mutable state bên ngoài thì có thể gây ra:

  • Data race nếu mutable state đó không được synchronize.
  • Nếu có synchronized rồi, thì vẫn bị giảm hiệu quả của parallel stream.

Vì vậy, nên hạn chế dùng hoặc tránh stateful operation nếu được. Và hầu như luôn có một cách để viết lại stream mà không cần đến stateful operation.


Okay mình đã viết khá nhiều rồi nhỉ. Để giúp các bạn đỡ ngán (và cũng tiện cho mình) thì mình sẽ chia ra làm 2 phần riêng nhé. Thank you nhiều nhiều vì đã đọc đến đây.

 
Share this