Khi làm việc với java thì việc hiểu và biết cách sử dụng stream để tối ưu hóa hiệu suất công việc là điều thực sự cần thiết, hôm nay mình sẽ hướng dẫn các bạn cách dùng một số chức năng mà java stream cung cấp, giúp mã của các bạn trở nên dễ nhìn và thanh lịch hơn.

Trước tiên chúng ta cứ đi tìm hiểu qua những hàm phổ biến mà java stream cung cấp trước đã.

Những hàm phổ biến trong java stream

filter()

filter loại bỏ những phần tử không thoả mãn điều kiện lọc predicate, hoặc cách nói khác là giữ lại những phần tử thỏa mãn điều kiện lọc

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var domains = Arrays.asList("cafeincode", "medium", "google");
        var filtered = domains.stream()
                .filter(item -> item.startsWith("c"))
                .peek(item -> System.out.println("Result: " + item))
                .collect(Collectors.toList());
    }
}
Result: cafeincode

map()

map làm nhiệm vụ ánh xạ từng phần tử trong stream sang một kiểu dữ liệu khác thông qua hàm mà các bạn chỉ định và tạo ra một stream mới, như ví dụ dưới mình dùng luôn hàm toUpperCase có sẵn

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var input = Arrays.asList("cafeincode", "medium", "google");
        var mapped = input.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());
        System.out.println("Result: " + mapped);
    }
}
Result: [CAFEINCODE, MEDIUM, GOOGLE]

flatMap()

flatMap được sử dụng để xử lý các phần tử của một stream và giúp biến đổi chúng thành một stream mới hoặc một danh sách các phần tử, tức là nó sẽ kết hợp các stream con thành stream cha

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

public class CafeincodeExample {

    public static void main(String[] args) {
        var nestedNumbers = Arrays.asList(
                Arrays.asList(1, 2),
                Arrays.asList(3, 4),
                Arrays.asList(5, 6)
        );
        var flattenedNumbers = nestedNumbers.stream()
                .flatMap(List::stream)
                .collect(Collectors.toList());
        System.out.println("Result: " + flattenedNumbers);
    }
}
Result: [1, 2, 3, 4, 5, 6]

distinct()

distinct được sử dụng để loại bỏ các phần tử trùng lặp từ một stream và nó sẽ trả về một stream mới chỉ chứa các phần tử duy nhất

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var numbers = Arrays.asList(1, 2, 2, 3, 3, 4, 5, 5);
        var distinctNumbers = numbers.stream()
                .distinct()
                .collect(Collectors.toList());
        System.out.println("Result: " + distinctNumbers);
    }
}
Result: [1, 2, 3, 4, 5]

sorted()

sorted được sử dụng để sắp xếp các phần tử của một stream theo một thứ tự nhất định và sẽ trả về một stream mới chứa các phần tử đã được sắp xếp

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
        var sortedNumbers = numbers.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("Result: " + sortedNumbers);
    }
}
Result: [1, 1, 2, 3, 4, 5, 5, 6, 9]

peek()

peek thường được sử dụng để thực hiện các hoạt động debug hoặc ghi log trên các phần tử trong quá trình xử lý stream mà không thay đổi nội dung của stream

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        IntStream.range(1, 6)
                .peek(element -> System.out.println("Processing element: " + element))
                .map(CafeincodeExample::mapping)
                .forEach(System.out::println);
    }

    private static Integer mapping(Integer input) {
        return input * input;
    }
}
Processing element: 1
1
Processing element: 2
4
Processing element: 3
9
Processing element: 4
16
Processing element: 5
25

limit()

limit được sử dụng để giới hạn số lượng phần tử trong một stream, nó sẽ trả về một stream mới chứa số lượng phần tử được giới hạn theo một giá trị nào đó

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        IntStream.range(1, 100)
                .limit(5)
                .forEach(System.out::println);
    }
}
1
2
3
4
5

skip()

skip được sử dụng để bỏ qua một số lượng phần tử trong một stream và trả về một stream mới bắt đầu từ vị trí đã bị bỏ qua

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        IntStream.range(1, 11)
                .skip(5)
                .forEach(System.out::println);
    }
}
6
7
8
9
10

Trong ví dụ này, IntStream.range(1, 11) tạo ra một Stream chứa các số từ 1 đến 10.

  • skip(5) được sử dụng để bỏ qua 5 phần tử đầu tiên của stream
  • forEach(System.out::println) được sử dụng để in ra các phần tử còn lại của stream, bắt đầu từ phần tử thứ 6 đến phần tử cuối cùng

toArray()

toArray được sử dụng để chuyển đổi một stream thành một mảng, phương thức này trả về một mảng chứa các phần tử của stream theo thứ tự

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        int[] numbers = IntStream.range(1, 6)
                .toArray();

        for (int number : numbers) {
            System.out.println("Result: " + number);
        }
    }
}
Result: 1
Result: 2
Result: 3
Result: 4
Result: 5

reduce()

reduce được sử dụng để thực hiện một phép biến đổi trên các phần tử của stream để tính toán một giá trị cuối cùng

import java.util.Arrays;

public class CafeincodeExample {

    public static void main(String[] args) {
        Integer[] integers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        var result = Arrays.stream(integers).reduce(0, Integer::sum);
        System.out.println("Result: " + result);
    }
}
Result: 55

collect()

collect được sử dụng để thu thập các phần tử của stream vào một cấu trúc dữ liệu cụ thể, như một List, Set hoặc Map

import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
        var list = stream.collect(Collectors.toList());
        System.out.println("Result: " + list);
    }
}
Result: [1, 2, 3, 4, 5]

count()

count được sử dụng để đếm số lượng phần tử trong một stream, phương thức này trả về một giá trị nguyên là số lượng phần tử trong stream

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        // Tạo một Stream của các số nguyên từ 1 đến 10
        var stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 11);

        // Đếm số lượng phần tử trong Stream
        var count = stream.count();

        // In ra số lượng phần tử
        System.out.println("Result: " + count);
    }
}
Result: 10

anyMatch()

anyMatch được sử dụng để kiểm tra xem có ít nhất một phần tử trong stream thỏa mãn theo điều kiện hay không

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "orange", "grape", "kiwi");
không
        var anyMatch = stream.anyMatch(str -> str.startsWith("a"));

        if (anyMatch) {
            System.out.println("Có phần tử bắt đầu bằng chữ 'a' trong Stream.");
        } else {
            System.out.println("Không có phần tử bắt đầu bằng chữ 'a' trong Stream.");
        }
    }
}
Có phần tử bắt đầu bằng chữ 'a' trong Stream.

allMatch()

allMatch được sử dụng để kiểm tra xem tất cả các phần tử trong stream có thỏa mãn theo điều kiện đặt ra hay không

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(2, 4, 6, 8, 10);
        var allMatch = stream.allMatch(number -> number % 2 == 0);

        if (allMatch) {
            System.out.println("Result: Tất cả các số trong Stream đều chia hết cho 2");
        } else {
            System.out.println("Result: Có ít nhất một số trong Stream không chia hết cho 2");
        }
    }
}
Result: Tất cả các số trong Stream đều chia hết cho 2

noneMatch()

hàm này chức năng sẽ na ná như hai hàm trên, tức là nó vẫn trả về một giá trị kiểu boolean, tuy nhiên hàm này nó sẽ kiểm tra tất cả các phần tử trong stream đều phải không thỏa mãn một điều kiện

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(2, 4, 6, 8, 10);
        var noneMatch = stream.noneMatch(number -> number % 5 == 0);

        if (noneMatch) {
            System.out.println("Result: Không có số nào trong Stream chia hết cho 5.");
        } else {
            System.out.println("Result: Có ít nhất một số trong Stream chia hết cho 5.");
        }
    }
}
Result: Có ít nhất một số trong Stream chia hết cho 5.

findFirst()

 findFirst trả về phần tử đầu tiên trong stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "cherry", "avocado", "blueberry");
        Optional<String> firstElement = stream.findFirst();
        if (firstElement.isPresent()) {
            System.out.println("Result: First element: " + firstElement.get());
        } else {
            System.out.println("Result: Stream is empty.");
        }
    }
}
Result: First element: apple

findAny()

findAny trả về bất kì phần tử nào trong stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "cherry", "avocado", "blueberry");
        Optional<String> anyElement = stream.findAny();
        if (anyElement.isPresent()) {
            System.out.println("Result: Any element: " + anyElement.get());
        } else {
            System.out.println("Result: Stream is empty.");
        }
    }
}
Result: Any element: apple

min()

min trả về phần từ nhỏ nhất trong stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(5, 2, 8, 1, 3);

        Optional<Integer> minElement = stream.min(Integer::compareTo);

        if (minElement.isPresent()) {
            System.out.println("Minimum element: " + minElement.get());
        } else {
            System.out.println("Stream is empty.");
        }
    }
}
Minimum element: 1

max()

max trả về phần tử lớn nhất trong stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(5, 2, 8, 1, 3);

        Optional<Integer> maxElement = stream.max(Integer::compareTo);

        if (maxElement.isPresent()) {
            System.out.println("Maximum element: " + maxElement.get());
        } else {
            System.out.println("Stream is empty.");
        }
    }
}
Maximum element: 8

groupingBy()

groupingBy dùng để nhóm các phần tử trong stream theo một điều kiện nhất định

import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "cherry", "avocado", "blueberry");
        var groupedByLength = stream.collect(Collectors.groupingBy(String::length));
        System.out.println("Result: " + groupedByLength);
    }
}
Result: {5=[apple], 6=[banana, cherry], 7=[avocado], 9=[blueberry]}

partitioningBy()

partitioningBy được sử dụng để phân chia các phần tử của stream thành hai nhóm dựa trên một điều kiện được cung cấp, kết quả trả về là một Map với hai key: true và false

Các phần tử thỏa mãn điều kiện sẽ được gán cho khóa true và các phần tử không thỏa mãn điều kiện sẽ được gán cho khóa false

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Map<Boolean, List<Integer>> partitioned = stream.collect(Collectors.partitioningBy(i -> i % 2 == 0));
        System.out.println("Even numbers: " + partitioned.get(true));
        System.out.println("Odd numbers: " + partitioned.get(false));
    }
}
Even numbers: [2, 4, 6, 8, 10]
Odd numbers: [1, 3, 5, 7, 9]

joining()

joining được sử dụng để kết hợp các phần tử của stream thành một chuỗi, các bạn có thể cung cấp một chuỗi ngăn cách giữa các phần tử hoặc để mặc định

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {
    public static void main(String[] args) {
        var data = Arrays.asList("apple", "banana", "cherry");
        String result = data.stream().collect(Collectors.joining(", "));
        System.out.println("Result: " + result);
    }
}
Result: apple, banana, cherry

iterating()

iterating thường được sử dụng khi cần tạo một chuỗi các giá trị được tạo ra theo một quy tắc nhất định nào đó

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> evenNumbers = Stream.iterate(2, n -> n + 2).limit(5);
        evenNumbers.forEach(System.out::println);
    }
}
2
4
6
8
10

of()

of được sử dụng để tạo ra một stream từ các phần tử được cung cấp dưới dạng đối số

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("apple", "banana", "cherry");
        stream.forEach(System.out::println);
    }
}
apple
banana
cherry

concat()

concat được sử dụng để nối hai stream lại với nhau, tạo ra một stream mới chứa tất cả các phần tử của hai stream gốc

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<String> stream1 = Stream.of("apple", "banana");
        Stream<String> stream2 = Stream.of("cherry", "grape");
        Stream<String> concatenatedStream = Stream.concat(stream1, stream2);
        concatenatedStream.forEach(System.out::println);
    }
}
apple
banana
cherry
grape

unordered()

unordered được sử dụng để chỉ định rằng stream sẽ không tuân theo thứ tự được xác định ban đầu.

Điều này có nghĩa là các phần tử trong stream có thể xuất hiện ở bất kỳ thứ tự nào sau khi thực hiện các phép biến đổi hoặc thu thập trên stream

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

        // Áp dụng unordered()
        Stream<Integer> unorderedStream = stream.unordered();

        // In ra các phần tử của Stream
        unorderedStream.forEach(System.out::println);
    }
}
1
2
3
4
5

range()

range được sử dụng để tạo ra một stream chứa các số nguyên liên tiếp, bắt đầu từ một giá trị A và kết thúc tại một giá trá trị B-1

import java.util.stream.IntStream;

public class CafeincodeExample {
    public static void main(String[] args) {
        IntStream rangeStream = IntStream.range(1, 6);

        // In ra các số trong phạm vi từ 1 đến 5
        rangeStream.forEach(System.out::println);
    }
}
1
2
3
4
5

rangeClosed()

rangeClosed được sử dụng để tạo ra stream có các số nguyên trong phạm vi từ A đến B

import java.util.stream.IntStream;

public class CafeincodeExample {
    public static void main(String[] args) {
        IntStream rangeClosedStream = IntStream.rangeClosed(1, 5);

        // In ra các số trong phạm vi từ 1 đến 5
        rangeClosedStream.forEach(System.out::println);
    }
}
1
2
3
4
5

generate()

generate được sử dụng để tạo ra một stream bằng cách tạo các phần tử dựa trên một supplier được cung cấp, supplier này tạo ra các phần tử một cách độc lập mỗi khi được gọi

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<String> stream = Stream.generate(() -> "Cafeincode").limit(3);

        // In ra các phần tử của Stream
        stream.forEach(System.out::println);
    }
}
Cafeincode
Cafeincode
Cafeincode

takeWhile()

takeWhile được sử dụng để lấy các phần tử từ một stream cho đến khi một điều kiện không còn được thỏa mãn nữa nó sẽ dừng lại

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Lấy các số nguyên từ đầu của Stream cho đến khi gặp số lớn hơn 7
        Stream<Integer> takenStream = stream.takeWhile(n -> n <= 7);

        // In ra các phần tử của Stream đã lấy
        takenStream.forEach(System.out::println);
    }
}
1
2
3
4
5
6
7

dropWhile()

ngược lại với takeWhile thì dropWhile được sử dụng để loại bỏ các phần tử từ một stream cho đến khi một điều kiện không còn được thỏa mãn nữa, phần còn lại của stream sẽ là những phần tử không thỏa mãn điều kiện

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Loại bỏ các số nguyên từ đầu của Stream cho đến khi gặp số lớn hơn 6
        Stream<Integer> droppedStream = stream.dropWhile(n -> n <= 6);

        // In ra các phần tử của Stream đã loại bỏ
        droppedStream.forEach(System.out::println);
    }
}
7
8
9
10

boxed()

boxed được sử dụng để chuyển đổi các phần tử của một stream từ kiểu nguyên (primitive) sang kiểu đối tượng (boxed type), điều này rất hữu ích khi bạn muốn làm việc với stream của các đối tượng thay vì các kiểu nguyên thủy

import java.util.stream.IntStream;
import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        IntStream intStream = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);

        // Chuyển đổi IntStream thành Stream<Integer>
        Stream<Integer> boxedStream = intStream.boxed();

        // In ra các phần tử của Stream
        boxedStream.forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8

parallel()

được sử dụng để chuyển đổi một stream thành một stream có thể xử lý song song.

Khi một stream được xử lý song song, các phần tử của stream có thể được xử lý đồng thời trên nhiều luồng, giúp tăng hiệu suất của ứng dụng trên các hệ thống có nhiều bộ vi xử lý

Tuy nhiên trong thực tế, việc sử dụng parallel trong một vài trường hợp không thực sự đem lại quá nhiều hiệu suất đáng kể, các bạn có thể tự tìm hiểu thêm phần này

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

        // Chuyển đổi Stream thành một Stream có thể xử lý song song
        Stream<Integer> parallelStream = stream.parallel();

        // In ra các phần tử của Stream có thể xử lý song song
        parallelStream.forEach(System.out::println);
    }
}
3
5
4
2
1

sequential()

được sử dụng để chuyển đổi một stream từ việc xử lý song song (parallel processing) sang việc xử lý tuần tự (sequential processing)

Khi một stream được xử lý tuần tự, các phần tử của stream sẽ được xử lý theo thứ tự từ đầu đến cuối trên một luồng duy nhất

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> parallelStream = Stream.of(1, 2, 3, 4, 5).parallel();

        // Chuyển đổi Stream từ xử lý song song thành xử lý tuần tự
        Stream<Integer> sequentialStream = parallelStream.sequential();

        // In ra các phần tử của Stream đã chuyển đổi thành xử lý tuần tự
        sequentialStream.forEach(System.out::println);
    }
}
1
2
3
4
5

Best practice trong việc sử dụng java stream

Việc sử dụng stream hợp lý giúp cho code của bạn trở nên thanh lịch, dễ nhìn, gọn gàng hơn phong cách viết code truyền thống.

Tuy nhiên nói đi cũng phải nói lại, cái gì nhiều quá cũng không hẳn là tốt, việc lạm dụng stream hoặc những cách viết khó hiểu cũng sẽ khiến cho các bạn thực sự đau đầu mỗi khi debug.

Vậy nên dưới đây sẽ là một số best practice mà theo mình là nên áp dụng để có thể vừa tận dụng tốt nó, vừa tránh lạm dụng không cần thiết.

  • Khi dùng stream có nhiều phương thức liên tục được áp dụng, hãy đặt mỗi hàm trên một dòng khác nhau, sẽ cực kì hữu ích khi debug
  • Sử dụng những phương thức mình đã liệt kê ở trên map(), filter(), reduce(), collect(),… một cách phù hợp để thực hiện các thao tác trên stream
  • Kiểm tra nullability trong quá trình thực hiện các thao tác map, filter
  • Tránh lạm dụng parallel trong quá trình code, trong nhiều trường hợp nó không đạt được nhiều value về hiệu suất như bạn tưởng, nếu có thể hãy cứ chỉ dùng mặc định sequential thôi
  • Đặt tên biến khi sử dụng một cách phù hợp, đừng sử dụng tên biến theo kiểu những chữ cái a, b, c mặc định vì khi đọc sẽ rất khó hiểu
  • Sử dụng Optional hợp lý trong những trường hợp sử dụng findFirst hoặc findAny
  • Trong thực tế sẽ có nhiều phương thức bạn cần triển khai lại chứ không sử dụng mặc định, ví dụ như sorted
  • Sử dụng peek để debug một cách hợp lý
  • Trong trường hợp convert List sang Map, cần cẩn thận chú ý việc trùng key
  • Java stream sử dụng lazy evaluation (bàn luận ở một bài khác), nghĩa là các phần tử chỉ được tính toán khi cần thiết, các bạn có thể sử dụng điều này để tăng hiệu suất bằng cách tránh tính toán không cần thiết

Xem thêm một số bài viết nổi bật bên dưới: