Xử lý nhanh các collection với Stream API trong java

Stream API được giới thiệu trong java 8, được sử dụng để xử lý các collection với rất nhiều tính năng ưu việt như lọc, rút trích dữ liệu, lặp etc.

Stream là một luồng nối tiếp các object hỗ trợ các hoạt động tổng hợp tuần tự và song song.

Đặc điểm nổi bật của Stream API

  • No storage: Stream không phải là một cấu trúc dữ liệu để lưu các phần tử. Thay vào đó nó sẽ mang các phần tử từ các cấu trúc dữ liệu như array, I/O chanel thông qua một luồng các hoạt động tính toán.
  • Functional in nature: Các hoạt động trên stream sẽ cho ra một kết quả, nhưng sẽ không sửa đổi nguồn của nó. Ví dụ như đoạn code ở trên, chúng ta filter những giá trị nào có giá trị dương, những kết quả thoả mãn sẽ được tạo ra trong một stream mới mà không xoá nó ra khởi list. 
  • Laziness-seeking: Các hoạt hoạt động như liltering, mapping đều có thể implement lazily. Điều này sẽ rất có lợi cho việc tối ưu hoá. Ví dụ khi bạn muốn tìm chuỗi đầu tiên có 3 chữ ‘abc’ liên tiếp trong một danh sách các chuỗi input. Stream sẽ không kiểm tra hết các chuỗi input đầu vào mà nó chỉ kiểm tra một lượng vừa đủ, cứ tiếp tục cho đến khi tìm được một chuỗi thoả điều kiện. Stream operators được chia thành intermediate operations terminal operations.
  • Possibly unbounded: Các hoạt động trên stream có thể tính toán trên luồng các đối tượng vô hạn. 
  • Consumable: Các phần tử trong stream chỉ được ghé thăm một lần trong vòng đời của stream.

Quá trình thực thi của một Stream

import java.util.Arrays;
import java.util.List;

class Main {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
        int sum = list.stream()
                .filter(value -> value > 0)
                .mapToInt(value -> value)
                .sum();
        System.out.println(sum);
    }
}

stream api java

Một Stream sẽ trải qua 3 đoạn chính:

  • Khởi tạo Stream
  • Hoạt động trung gian – Intermediate Operations
  • Hoạt động đầu cuối – Terminal Operations

Cách tạo Stream trong java

Tạo stream với kiểu dữ liệu nguyên thuỷ

Stream API chỉ làm việc với các object, vì vậy để tạo Stream cho các dữ liệu nguyên thuỷ chúng ta phải sử dụng các Stream cụ thể cho từng kiểu dữ liệu như IntStream cho int, DoubleStream cho double, etc

public class PrimitiveStreamExample {
 
    public static void main(String[] args) {
        IntStream.range(1, 4).forEach(System.out::println); // 1 2 3
 
        IntStream.of(1, 2, 3).forEach(System.out::println); // 1 2 3
 
        DoubleStream.of(1, 2, 3).forEach(System.out::println); // 1.0 2.0 3.0
 
        LongStream.range(1, 4).forEach(System.out::println); // 1 2 3
 
        LongStream.of(1, 2, 3).forEach(System.out::println); // 1 2 3
    }
}

Tạo stream cho các collection

Để tạo stream cho các collection trong java chỉ đơn giản là gọi stream() method mà bất cứ collection nào cũng hỗ trợ.

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

class Main {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        Set<String> set = new HashSet<>();
        set.add("1"); set.add("2");set.add("3"); set.add("4");
        
        int sum = list.stream()
                .filter(value -> value > 0)
                .mapToInt(value -> value)
                .sum();
        
        set.stream().forEach(s -> System.out.println(s));
        System.out.println(sum);
    }
}

Điều kiện tiên quyết

Trước khi đi qua các ví dụ của Stream trong java, chúng ta cần chuẩn bị trước một Student class gồm các thuộc tính name, isMale, age, score, danh sách các subject.

import java.util.List;

public class Student {
    public String name;
    public boolean isMale; // true - male or false female
    public int age;
    public int score;
    public List<String> subjects;

    public Student(String name, boolean isMale, int age, int score, List<String> subjects) {
        this.name = name;
        this.isMale = isMale;
        this.age = age;
        this.score = score;
        this.subjects = subjects;
        
    }
    
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", isMale=" + isMale +
                ", age=" + age +
                ", score=" + score +
                '}';
    }

}

Hàm main() chuẩn bị sẵn dữ liệu dùng trong các ví dụ dưới đây

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("A", true, 18, 5, Arrays.asList("Toan", "Ly", "Hoa")),
                new Student("A", true, 18, 5, Arrays.asList("Van", "Su", "Hoa")),
                new Student("B", false, 15, 8, Arrays.asList("Toan", "Van", "Anh", "Su")),
                new Student("C", false, 12, 9, Arrays.asList("Cong nghe", "Dia ly")),
                new Student("D", true, 10, 3, Arrays.asList("Anh van", "Hoa", "Sinh")),
                new Student("E", true, 10, 2, Arrays.asList("My Thuat", "Am nhac")),
                new Student("F", false, 18, 10, Arrays.asList()));
    }
}

Intermediate operations Stream

Các phép tính trung gian intermediate operation để thao tác tiền xử lý với các dữ liệu ban đầu trước khi rút trích ra kết quả cuối cùng. Mỗi intermediate operation đều sẽ trả về 1 stream mới nên luồng dữ liệu sẽ đi qua từng Stream tương ứng với từng intermediate operation, chúng được cập nhật, lọc, tính toán etc tương ứng với từng Stream.

Filter

Stream filter() được dùng để lọc các phần tử theo một điều kiện xác định.

Ví dụ lọc các Student có giới tính là nam.

students.stream()
                .filter(s -> s.isMale)
                .forEach(s -> System.out.println(s.toString()));

// Output
Student{name='A', isMale=true, age=18, score=5}
Student{name='A', isMale=true, age=18, score=5}
Student{name='D', isMale=true, age=10, score=3}
Student{name='E', isMale=true, age=10, score=2}

Ví dụ Sử dụng filter() lọc các Student là nam, có tuổi trên 10, điểm từ trung bình trở lên.

students.stream()
        .filter(s -> s.isMale && s.age > 10 && s.score >= 5)
        .forEach(s -> System.out.println(s.toString()));
// Output
Student{name='A', isMale=true, age=18, score=5}
Student{name='A', isMale=true, age=18, score=5}

Skip, Limit

skip(long n) Trả về một Stream lược bỏ n phần tử kể từ vị trí đầu tiên, còn limit(long l) trả về một Stream với số lượng phần tử tối đa là l.

Ví dụ Sử dụng filter() lọc các Student là nam, có tuổi trên 10, điểm từ trung bình trở lên. Quá trình filter sẽ bắt đầu từ sinh viên thứ 2 trong ArrayList xử lý tối đa 3 Student.

students.stream().skip(1).limit(3)
        .filter(s -> s.isMale && s.age > 10 && s.score >= 5)
        .forEach(s -> System.out.println(s.toString()));

//Output
Student{name='A', isMale=true, age=18, score=5}

map

Stream map() dùng để ánh xạ Stream object sang Stream object khác tương ứng. 

Ví dụ trích danh sách điểm của tất cả các sinh viên.

List<Integer> scores = students.stream()
          .map(s -> s.getScore())
          .collect(Collectors.toList());
 // Output 
5 5 8 9 3 2 10

flatMap

Stream flatMap() dùng để ánh xạ một Stream collection của object sang một Stream object khác ứng. 

Ví dụ liệt qua tất cả các môn học của tất cả các Student.

Set<String> subject = students.stream()
           .flatMap(s -> s.subjects.stream())
           .collect(Collectors.toSet());
 // output
[Van, Su, Cong nghe, Sinh, My Thuat, Anh, Hoa, Am nhac, Dia ly, Ly, Anh van, Toan]

sorted

Stream sorted(Comparator<? super T> comparator) trả về một Stream được sắp xếp theo một comparator truyền vào.

Ví dụ sắp xếp các Student theo độ tuổi.

students.stream()
        .sorted(Comparator.comparingInt(s -> s.age))
        .forEach(s -> System.out.println(s.toString()));

// Output
Student{name='D', isMale=true, age=10, score=3}
Student{name='E', isMale=true, age=10, score=2}
Student{name='C', isMale=false, age=12, score=9}
Student{name='B', isMale=false, age=15, score=8}
Student{name='A', isMale=true, age=18, score=5}
Student{name='A', isMale=true, age=18, score=5}
Student{name='F', isMale=false, age=18, score=10}

Ví dụ sắp xếp các Student theo tuổi, với các Student bằng tuổi sắp xếp theo score. 

students.stream()
        .sorted(Comparator.comparingInt(s -> ((Student)s).age).thenComparingInt(s -> ((Student)s).score))
        .forEach(s -> System.out.println(s.toString()));
// output

Student{name='E', isMale=true, age=10, score=2}
Student{name='D', isMale=true, age=10, score=3}
Student{name='C', isMale=false, age=12, score=9}
Student{name='B', isMale=false, age=15, score=8}
Student{name='A', isMale=true, age=18, score=5}
Student{name='A', isMale=true, age=18, score=5}
Student{name='F', isMale=false, age=18, score=10}

distinct

distinct() trả về một Stream với các phần tử không trùng lặp. Ví dụ liệt kê danh sách các môn học của tất cả các sinh viên.

students.stream()
        .flatMap(s -> s.subjects.stream())
        .distinct()
        .forEach(subject -> System.out.print(subject + " "));

// Output
Toan Ly Hoa Van Su Anh Cong nghe Dia ly Anh van Sinh My Thuat Am nhac

Terminal Operations Stream

Terminal operation lấy về các kết quả từ quá trình intermediate operations.

forEach

Duyệt các phần tử trong Stream. Ví dụ lọc các Student nam và xuất ra màn hình.

        
students.stream()
        .filter(s -> s.isMale)
        .forEach(s -> System.out.println(s.toString()));

// Output
Student{name='A', isMale=true, age=18, score=5}
Student{name='A', isMale=true, age=18, score=5}
Student{name='D', isMale=true, age=10, score=3}
Student{name='E', isMale=true, age=10, score=2}

collect

collect trả về một collection được chỉ định.

List<Student> list = students.stream()
                .filter(s -> s.isMale)
                .collect(Collectors.toList());

Set<String> set = students.stream()
            .flatMap(s -> s.subjects.stream())
            .collect(Collectors.toSet());

allMatch

Stream anyMatch(Predicate<? super T> predicate) trả về true khi tất cả các phần tử trong Stream thoả mãn điều kiện predicate. Ví dụ kiểm tra xem toàn bộ sinh viên name có điểm trên trung bình hay không?

boolean good = students.stream()
        .filter(s -> s.isMale)
        .allMatch(s -> s.score > 5); // false

anyMatch

Stream anyMatch(Predicate<? super T> predicate) trả về true khi có bất kỳ một phần tử nào thoả điều kiện predicate. Ví dụ kiểm tra xem có sinh viên nam nào dưới điểm trung bình hay không?

        
boolean good = students.stream()
         .filter(s -> s.isMale)
         .anyMatch(s -> s.score < 5); // true

min

Stream min(Comparator<? super T> comparator) trả về phần tử nhỏ nhất dựa theo comaprator truyền vào. Ví dụ xuất ra màn hình Student có điểm thấp nhất. 

Student student = students.stream()
        .min(Comparator.comparingInt(s -> s.score))
        .orElse(null);
System.out.println(student.toString());
// Student{name='E', isMale=true, age=10, score=2}

max

Stream max(Comparator<? super T> comparator) trả về phần tử lớn nhất dựa theo comaprator truyền vào. Ví dụ xuất ra màn hình Student có điểm cao nhất. 

Student student = students.stream()
        .max(Comparator.comparingInt(s -> s.score))
        .orElse(null);
System.out.println(student.toString()); 
// Student{name='F', isMale=false, age=18, score=10}

findFirst

Stream findFirst() trả về phần tử đầu tiên của Stream. 

Student student = students.stream()
        .findFirst().orElse(null);
System.out.println(student.toString());
// Student{name='A', isMale=true, age=18, score=5

findAny

Stream findAny() trả về phần tử bất kỳ của Stream. 

Student student = students.stream()
         .findAny().orElse(null);

count

Stream count() trả về số lượng phần tử trong Stream.

long count = students.stream().count(); // 7

recude

Stream reduce giúp chúng ta lặp lại lập lại một thao tác. Ví dụ muốn tính tổng điểm của các Student.

      
  long sum = students.stream()
          .mapToInt(s -> s.score)
          .reduce(0, (s1, s2) -> s1 + s2); // 42

Tóm lược

Qua bài viết trên chúng ta đã thấy được sức mạnh của Stream API như thế nào rồi phải không. Nó gần như thay thế tất các tác vụ hằng ngày chúng ta thường làm như duyệt, lọc, tính toán etc. Hiện nay trong dự án của mình gần như toàn bộ được sử dụng với Stream, sẽ rất khó để thấy vòng lặp for, while bình thường như ngày xưa! Stream tạo nên phong cách code mới!!!!

Một mẹo là để phân biệt Intermediate hay terminate operator rất đơn giản, Intermediate sẽ trả về một Stream trong khi terminate trả về kết quả là một object.

Nguồn tham khảo

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

https://www.logicbig.com/tutorials/core-java-tutorial/java-util-stream/stream-api-intro.html

4 2 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x