Code "xịn" hơn với Java Streams - Java 8

  • Phuong Dang
  • 04/May/2022
Java - 8 Stream API
  1. Stream là gì?
  2. Phân loại Stream
  3. So sánh intermediate và terminal operation
  4. Tạo Stream sources như nào
  5. Common terminal operations
  6. Common intermediate operations
  7. Stream pipeline
  8. Kết luận.

Khi tham gia vào dự án thực tế thì bạn sẽ thấy rằng không xử lý code được với Java Streams - Java 8 thì source code của mình nhìn sẽ "quê" thế nào.

1. Streams là gì?

"Stream" được sử dụng trong bài viết này không có liên quan đến stream mà các bạn đã biết trong Java IO như InputStream, OuputStream.

Một Stream thể hiện cho một "sequence of data".

Một Stream pipeline là tập hợp các operation được thực hiện trên stream và tạo ra một kết quả. Ta có thể tưởng tượng Stream pipeline giống như những dây chuyền sản xuất trong nhà máy. Ví dụ với dây chuyền đóng gói sản phẩm ta cần làm những bước sau:

  1. Bắt đầu: Nhận sản phẩm cần đóng gói
  2. Thực hiện kiểm tra trạng thái có móp mép, vỡ hay không
  3. Bọc nilon chống sốc
  4. Đóng vào thùng caton và dán tem
  5. Kết thúc: Hoàn thiện quy trình đóng gói sản phẩm được bọc trong thùng caton đạt tiêu chuẩn

Với mỗi Stream pipeline sẽ bao gồm 3 phần:

  1. Source: Định nghĩa stream đến từ đâu
  2. Intermediate operations: Chuyển đổi dữ liệu trong stream sang 1 dạng mới (a -> a'). Chúng ta có thể có 1 hoặc nhiều intermediate operations trong Stream pipeline.
  3. Terminal operation: Tạo ra 1 kết quả cuối cùng và kết thúc stream. Stream sẽ không còn hợp lệ sau khi Terminal operation được thực hiện.
A Stream pipeline
A Stream pipeline

2. Phân loại Stream

Trong Java có 2 loại Stream:

  • Sequential Stream: Mặc định không chỉ định thì mỗi Stream được tạo ra sẽ hoạt động theo dạng Sequential Stream. Tức là chỉ có 1 luồng xử lý tuần tự các nhiệm vụ cho đến khi kết thúc stream pipeline.
  • Parallel Stream: Với Parallel Stream thì Java sẽ tạo nhiều luồng cùng xử lý, giúp tốc độ xử lý tốt hơn. Tuy nhiên cần phải cần nhắc sử dụng hợp lý, với những small stream thì performance thâm chí còn kém hơn Sequential Stream. Vì lúc đó chúng ta sẽ tốn cost cho việc phân luồng dữ liệu nữa.
Sequential vs Parallel Stream
Sequential vs Parallel Stream (Refer geeksforgeeks)

3. So sánh intermediate và terminal operation

# Intermediate Operation Terminal Operations
Thành phần bắt buộc của 1 Stream pipeline NO YES
Xuất hiện nhiều lần trong 1 Stream pipeline YES NO
Return type là 1 Stream YES NO
Lazy evaluation YES NO
Stream còn hợp lệ sau khi call YES NO

Lazy evaluation: Intermediate operations sẽ chỉ thực hiện khi terminal operation được gọi.

4. Tạo Stream sources như nào

Thông thường Stream có thể được tạo từ Collection:

1
2
3
4
5
6
 List<String> strList = Arrays.asList("A", "B");
 Set<Integer> strSet = new HashSet<>(Arrays.asList(1, 2, 3));

 Stream<String> stream = strList.stream();
 Stream<String> stream01 = strList.parallelStream();
 Stream<Integer> stream02 = strSet.stream();

Hoặc chúng ta có thể tạo Stream từ fixed value:

1
2
3
 Stream<String> stream = Stream.empty(); // count = 0
 Stream<Integer> stream01 = Stream.of(1, 2); // count = 2
 Stream<Double> stream02 = Stream.of(1.5, 2.6, 3.7); // count  = 3

5. Common terminal operations

5.1. count() method

Method Signature:

long count()

Return về tổng số lượng phần tử trong Stream

1
2
3
 List<String> strList = Arrays.asList("A", "B");
 long count = strList.stream().count();
 System.out.println(count); // Print "2"

5.2. min()/max() method

Method Signature:

Optional<T> min(Comparator<? super T> comparator)
Optional<T> max(Comparator<? super T> comparator)

Cho phép truyền vào một custom comparator để tìm giá trị lớn nhất và nhỏ nhất. Những method này return về Optional có nghĩa rằng sẽ trường hợp ta không tìm được min/max, ví dụ: Stream source bị empty.

1
2
3
4
5
6
7
 List<Integer> strList = Arrays.asList(7, 9, 7);

 Optional<Integer> minOpt = strList.stream().min(Comparator.comparingInt(s -> s));
 minOpt.ifPresent(System.out::println); // Print 7

 Optional<Integer> maxOpt = strList.stream().max(Comparator.comparingInt(s -> s));
 maxOpt.ifPresent(System.out::println); // Print 9

5.3. findAny()/findFirst() method

Method Signature:

Optional<T> findFirst();
Optional<T> findAny();

Method sẽ return 1 element trong stream ngoại trừ trường hợp stream bị empty. Method findFirst() sẽ luôn trả về phần tử đầu tiên của stream. Còn method findAny() sẽ hữu ích khi chúng ta dùng parallel stream, nó sẽ giúp bạn return về phần tử đầu tiên mà nó sẽ gặp trong bất kỳ luồng xử lý nào thay vì phần tử đầu tiên của stream. Vì thể mà kết quả mỗi lần trả về có thể khác nhau.

1
2
3
4
5
6
7
 Optional<String> findFirstResult = Stream.of("Winzone.vn", "Winzone blog")
                .findFirst();
 Optional<String> findAnyResult = Arrays.asList("Winzone.vn", "Winzone blog").parallelStream()
                .findAny();

 findFirstResult.ifPresent(System.out::println); // Always print "Winzone.vn"
 findAnyResult.ifPresent(System.out::println); //"Winzone.vn" or "Winzone blog"

5.4. allMatch()/anyMatch()/noneMatch() method

Method Signature:

boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)

Những method này sẽ nhận đầu vào là 1 custom predicate và thực hiện search data của stream dựa trên predicate đó, giá trị trả về sẽ là true/false tùy thuộc vào dữ liệu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 List<String> strList = Arrays.asList("Winzone.vn", "FA", "Fresher Academy");
 Predicate<String> predicate = str -> str.length() > 3;

 // Method sẽ return TRUE khi có 1 element match predicate
 System.out.println(strList.stream().anyMatch(predicate)); // true

 // Method sẽ return TRUE khi tất cả elements match predicate
 System.out.println(strList.stream().allMatch(predicate)); // false

 // Method sẽ return TRUE khi tất cả elements không match predicate
 System.out.println(strList.stream().noneMatch(predicate)); // false

5.4. forEach() method

Method Signature:

void forEach(Consumer<? super T> action)

Method nhận vào 1 custom Consumer action, với cấu trúc loop tất cả các element của stream sẽ đều thực hiện action. Lưu ý method này terminal operation return void, do đó muốn thay đổi dữ liệu thì cần xử lý trong custom Consumer truyền vào.

1
2
 Stream.of("Winzone", "Sharing knowledge")
      .forEach(s -> System.out.print(s + ", ")); // Winzone, Sharing knowledge,

5.4. collect() method

Method Signature:

<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);

Đây là một trong những method hay sử dụng nhất, nó sẽ giúp bạn transform data của Stream source sang một dạng mới theo mong muốn (Thông thường là List, Set, Map).

Stream<String> stream1 = Stream.of("w", "i", "n");
Stream<String> stream2 = Stream.of( "z", "o", "n", "e");

Set<String> set = stream1.collect(Collectors.toCollection(LinkedHashSet::new));
List<String> list = stream2.collect(Collectors.toList());
System.out.println(set); // [w, i, n]
System.out.println(list); // [z, o, n, e]

6. Common intermediate operations

6.1. filter() method

Method Signature:

Stream<T> filter(Predicate<? super T> predicate);

Đây là method giúp ta loại bỏ dữ liệu không phù hợp, tham số đầu vào là 1 Predicate. Những item có giá trị không phù hợp với điều kiện của Predicate sẽ bị loại bỏ.

Stream<String> s = Stream.of("winzone.vn", "blog winzone.vn", "Java");
        s.filter(x -> x.startsWith("winzone")).forEach(System.out::print); // winzone.vn

6.2. map() method

Method Signature:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Đây là method giúp ta convert item của stream source thành một loại dữ liệu mới để tiếp tục xử lý ở next stream hoặc terminate sang 1 dạng data type mới.

List<String> list = Arrays.asList("win", "zone");
List<String> newUppercaseList = list.stream()
                                    .map(s -> s.toUpperCase()).collect(Collectors.toList());
System.out.println(newUppercaseList); // [WIN, ZONE]

6.3. sorted() method

Method Signature:

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

Đây là method giúp sắp xếp các phần tử trong stream source, mặc định sẽ theo thứ tự tăng dần nếu không được truyền vào lambda expression.

List<String> list = Arrays.asList("zone", "win", "blog");
List<String> newSortedList = list.stream().sorted().collect(Collectors.toList());
System.out.println(newSortedList); // [blog, win, zone]

List<String> list = Arrays.asList("zone", "win", "blog");
List<String> newSortedList = list.stream()
             .sorted(Comparator.reverseOrder()) // DESC
             .collect(Collectors.toList());
System.out.println(newSortedList); // [zone, win, blog]

7. Stream pipeline

Với Stream ta có thể kết hợp nhiều intermedia operator cùng 1 lúc để xử lý và terminal operator để ouput ra một kết quả theo mong muốn.

Giả sử có có 1 List<String> như sau: ["win", "zone", "ab", null, "vn", "blog", "win"], và yêu cầu bài toán như sau:

  • Loại bỏ giá trị null
  • Loại bỏ giá trị có length <= 2
  • Uppercase giá trị và sắp xếp giảm dần
  • Loại bỏ giá trị trùng lặp

Ta có thể xử lý ngắn gọn với Stream như sau:

List<String> list = Arrays.asList("win", "zone", "ab", null, "vn", "blog", "win");
Set<String> newList = list.stream()
                          .filter(s -> Objects.nonNull(s) && s.length() > 2)
                          .map(String::toUpperCase)
                          .sorted(Comparator.reverseOrder())
                          .collect(Collectors.toCollection(LinkedHashSet::new));
System.out.println(newList); // [ZONE, WIN, BLOG]

8. Kết luận

Stream còn rất nhiều Intermediate và Terminal operation hữu ích khác chưa thể giới thiệu trong bài viết này. Tuy nhiên bài viết này cũng đã cung cấp đến cho các bạn những kiến thức cơ bản về cú pháp, cách dùng và cách thức hoạt động của Stream API.

Mong rằng nó sẽ là nền tảng để giúp các bạn tiếp cận với những method còn lại. Hãy thực hành thật nhiều để source code nhìn thật "xịn sò" nhé.