-1

Các khái niệm và đặc điểm của stream trong java 8

Khi thử, nghiên cứu hay học 1 vấn đề nào đó, để nắm được sâu, hiểu hết vấn đề chúng ta nên hiểu từ các khái niệm cơ bản của nó. Nó thực sự là gì, cố gắng thể hiện, giải thích cho đơn giản hơn thông qua các đối tượng, ví dụ trong thực tế. Đừng vội kết luận Stream nhanh ( hoặc chậm hơn ) so với duyệt mảnh hoặc vòng lặp của collection. Đầu tiên: mạnh mồm chém gió 1 vấn đề cao siêu này kia, trong khi các khái niệm cơ bản của nó còn chưa hiểu hết Thứ hai là: so sánh như trên rất là chung chung. Khi so sánh phải đặt vào tình huống cụ thể: trong trường hợp nào , bài toán nào hay tất cả?

Tất nhiên cách học hay nghiên cứu có thể mỗi người có 1 cách riêng. Và tùy vào hoàn cảnh: phạm vi tìm hiểu cũng như thời gian cần hoàn thành. Ví dụ: giao cho 1 vị 1 task nghiên cứu 1 tiện ích của 1 thư viện để áp dụng vào 1 tính năng nhỏ của dự án, vị đó không thể đòi mất vài ngày liền để tìm hiểu hết các ngóc ngách, khía cạnh của thư viện đó được. Liên quan 1 chút để kỹ năng đánh giá ( estimate ) yêu cầu.

Cũng như bài viết về Agile/Scrum. Bài viết đầu tiên về Stream, mình muốn đề cập đến các khái niệm, đặc điểm của Stream trước khi đi đến các phần về cách sử dụng chi tiết, cũng như 1 vài trường hợp so sánh của Stream với Collection, vòng lặp. Tên các khái niệm sẽ được để là tiếng anh, để có sự quen thuộc và thống nhất khi đọc cái tài liệu khác trên mạng.

1. Ta có thể định nghĩa stream thông qua so sánh sự khác biệt với collections

  • Không lưu trữ. Stream không phải là 1 kiểu câu trúc dữ liệu để chứa các đối tượng, thành phần. Stream truyền tải các thành phần từ các nguồn (source) như 1 cấu trúc dữ liệu, một mảng nào đó
  • Không chỉnh sửa. Một toán tử thực hiện trên một stream tạo ra 1 kết quả nào đó nhưng nó không sửa nguồn của nó.
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("Nam", 8));
        studentList.add(new Student("Hoang", 5));
        studentList.add(new Student("Nga", 4));

        List<Student> goodStudent = studentList.stream().filter(st -> st.getPoint() > 5).collect(Collectors.toList());

        System.out.println("Old list: " + studentList.size());
        System.out.println("New list: " + goodStudent.size());

		// result
		Old list: 3
		New list: 1

  • Lazy seek: Nhiều toán tử thao tác trên stream như filter, map thực hiện theo cơ chế lazy. Các toán tử của stream chia làm 2 loại intermediate và terminal Intermediate operation luôn luôn là lazy. Chi tiết về các loại toán tử sẽ được đề cập ở phần bên dưới.

  • Có thể không bị chặn: Collection có kích thước giới hạn, stream thì không. Các toán tử limit() và findFirst() có thể cho phép tính toán trên các stream vô hạn trong thời gian giới hạn

  • Tiêu hao (Consumable): Các thành phần của một stream chỉ được duyệt 1 lần trong vòng đời của 1 stream. Để duyệt lại các đối tượng thì stream cần được sinh lại.

2. Stream có thể được lấy từ các data source bằng các cách sau

  • Từ các class con của Collection thông qua phương thức stream() và parallelStream().
  • Từ 1 mảng bằng cách sử dụng Arrays.stream(Object[]);
  • Từ các dòng của 1 file thông qua BufferedReader.lines();
  • Hoặc từ các method của class Files

Ví dụ: studentList.stream() studentList.parallelStream()

3. Các loại toán tử của Stream

  • Intermediate operation: toán tử trung gian trả về stream mới, chúng luôn là lazy. Ví dụ khi thực hiện filter(), nó không thực hiện việc lọc ngay lập tức mà nó tạo ra 1 stream mới. Cái mà khi được duyệt(thực thi) sẽ chứa các phần tử hợp lệ của stream ban đầu. Lưu ý Stream được tạo từ toán tử intermediate sẽ không bắt đầu duyệt cho đến khi có 1 toán tử terminal thực hiện
  • Terminal operation: toán tử đầu cuối, sẽ duyệt(thực thi) 1 stream để trả về 1 kết quả. Sau khi toán tử terminal được thực hiện, stream sẽ được xét như là đã được tiêu thụ, dùng (consumed), không còn được sử dụng nữa. Trong trường hợp bạn vẫn muốn duyệt cùng kiểu tập hợp dữ liệu đó, bạn cần phải quay lại data source và tạo ra stream mới.

Hãy liên hệ stream như hình ảnh sau: Data source: nguồn lấy dữ liệu giống như 1 bể chứa các đối tượng có các đặc điểm,kích cỡ khác nhau Toán tử intermediate và terminal kết hợp tạo thành đường ống dẫn dòng. Intermediate là các ống lọc đặt từ bể chứa. Khi đặt ống lọc không có nghĩa là các đối tượng đã bắt đầu lọc (lazy). Toán tử terminal có thể coi như là thiết bị đầu cuối ví dụ như 1 máy hút. Nước chỉ chảy qua ống khi máy hút bắt đầu chạy.

Một 1 ví dụ khác: về băng chuyền in nhãn mác sản phẩm chẳng hạn. Các hộp chứa sản phẩm chỉ được vận chuyển trên băng chuyền khi máy in nhãn mác ở đầu cuối bắt đầu phát lệnh in.

Toán tử intermediate có thể chia làm 2 loại: stateless and stateful operation

  • Toán tử stateless như filter và map: không giữ lại trạng thái của 1 phần tử trước đó khi đang xử lý (tương tác) 1 đối tượng mới
  • Toán tử stateful như distinct và sorted: có thể sử dụng trạng thái của đối tượng trước để xử lý (tương tác) đối tượng mới. Stateful thường đòi hỏi xử lý (duyệt) toàn bộ data source trước khi tạo ra kết quả. Lưu ý đặc điểm này khi so sánh stream với các kiểu lặp. Đặc điểm này cũng là yếu tố cần cân nhắc khi quyết định sử dụng đơn stream hay song song stream (luồng này chờ kết quả luồng kia)

4. Parallelism Xử lý các đối tượng với vòng lặp for là kiểu xử lý tuần tự. Stream tạo điều kiện thực hiện song song bằng cách sắp xếp lại các tính toán của các luồng khác nhau - luồng này có thể coi như là luồng thu thập tổng hợp Tất cả các toán tử của steam đều có thể thực hiện theo tuần tự hoặc song song. JVM thực thi stream theo tuần tự là mặc định trừ khi chỉ ra, khai báo chính xác là song song.

Ví dụ 2 method của các class còn của Collection

  • Collection.stream() : tạo ra stream tuần tự
  • Collection.parallelStream(): tạo ra các stream song song

Để muốn xác định mode của stream thì sử dụng method isParallel().Ví dụ:

studentList.stream().filter(st -> st.getPoint() > 5).filter(st -> st.getName().equals("Nam")).isParallel();

Để thay đổi mode của stream sử dụng 2 method sequential() và parallel(). Ví dụ

studentList.stream().filter(st -> st.getPoint() > 5).filter(st -> st.getName().equals("Nam")).parallel();

Trên đây là các khái niệm, đặc điểm cơ bản cần biết khi làm việc với Stream trong Java 8. Phần tiếp theo của series về Stream, mình sẽ trình bày về các method, trường hợp phổ biến sử dụng stream.

Link references: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.