Mục lục
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 và 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); } }
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