Java những điều có thể bạn đã biết: Có gì mới trong Java 8 (Phần 1)

Trong các bài trước tôi đã từng đề cập tới những cải tiến của Java 8 như Java 8 Lambda Expressions hay Java 8 Repeating Annotations.

Hôm nay tôi sẽ giới thiệu đến các bạn về Stream APICollectors trong Java 8. Là một trong những tính năng mới và cực kì được các developer yêu thích.

Bắt đầu nào.

Cùng xét ngữ cảnh chúng ta có một list như sau.

List<Peron> people = new ArrayList<>();
view raw list.java hosted with ❤ by GitHub

Bài toán chúng ta có là tính toán độ tuổi trung bình của những người có độ tuổi lớn hơn 20 trong list, cách thông thường các bạn sẽ nghĩ là duyệt hết tất cả các phần tử của list rồi so sánh tuổi của từng người, nếu lớn hơn 20 cộng thêm vào tổng tuổi, và tăng số lượng người lớn hơn 20 tuổi lên, cuối cùng chia ra để tính tuổi trung bình đúng không.

Dưới mindset của Java 8, để giải quyết bài toán này chúng ta sẽ làm chúng ta sử dụng phương pháp Map/Filter/Reduce.

Mapping, chúng ta sẽ làm việc là chuyển đổi dữ liệu từ List<Person> thành List<Integer> chỉ chứa thông tin chúng ta cần xử lý đó là độ tuổi.

Filtering, lọc ra những phần tử pù hợp với điều kiện, cụ thể ở đây là độ tuổi lớn hơn 20. Sau bước này chúng ta sẽ có được một list với các phần tử có tuổi lớn hơn 20.

Reduction, sử dụng các phép tính toán để kết xuất ra dữ liệu cần thiết.

Để thực hiện được concept này, Java 8 đã đưa ra Stream API, vậy Stream API là gì?

Đầu tiên, về mặt kĩ thuật Stream API là một Java Inteface.

public interface Stream<T> extends BaseStream<T, Stream<T>> {
// more code here
}
view raw stream.java hosted with ❤ by GitHub

Như các bạn thấy Stream<T> có nghĩa là chúng ta có thể có Steam<String>, Stream<Integer>, Stream<Person>, blabla…

Stream trông thì có vẻ giống như Collection nhưng nó lại là một concept hoàn toàn khác.

Stream dùng để xử lý dữ liệu một cách hiệu quả. Như thế nào thì gọi là hiệu quả? Hiệu quả ở đây gồm hai thứ. Thứ nhất, là việc xử lý song song. Thứ hai, là việc xử lý pipeline.

Vậy tại sao Java 8 không thay đổi Collection API mà lại tạo thêm Stream API? Đơn giản vì chúng ta, hay những người làm ra Java không muốn thay đổi những gì chúng ta đang làm bằng Collection API, nhưng vẫn cần một phương thức mới để xử lý dữ liệu hiệu quả hơn.

Làm thế nào để có một đối tượng Stream?

Để làm việc này chúng ta có khá nhiều cách. Chẳng hạn như.

Stream<Person> peopleStream = people.stream();
view raw stream.java hosted with ❤ by GitHub

Sau khi có được Stream, thì chúng ta có thể khai báo các operator.

Đầu tiên, chúng ta sẽ xem xét operator forEach(), cũng như cách để consume stream.

stream.forEach(p -> System.out.println(p.name));
view raw stream.java hosted with ❤ by GitHub

Hoặc có thể khai báo một Consumer để xử lý stream, Consumer cũng là một Java Interface, cụ thể nó là một Functional Interface, các bạn có thể tham khảo bài Java 8 Lambda Expressions để biết về Functional Interface

@FunctionalInterface
public Consumer<T> {
void accept(T t);
}
// Hoặc khai báo bằng lamba
Consumer<T> consumer = p -> System.out.println(p);
// Hoặc dùng method reference
Consumer<T> consumer = System.out::println;
// Hoặc phức tạp hơn
@FunctionalInterface
public Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Object.requireNonNull(after);
return (T t) -> {
accept(t);
after.accept(t);
}
}
}
view raw consumer.java hosted with ❤ by GitHub

Ngoài ra cũng có thể tạo một chuỗi các Consumer như sau.

List<Person> people = new ArrayList<>();
Consumer<Person> c1 = people::add;
Consumer<Person> c2 = System.out::println;
Consumer<Person> c3 = c1.andThen(c2);

Tuy nhiên các bạn lưu ý, với stream thì chúng chỉ chỉ có thể truyền vào một Consumer, không thể truyền một chuỗi Consumer.

Tiếp theo, là cách filter dữ liệu với stream.

List<Person> people = new ArrayList<>();
Stream<Person> stream = people.stream();
Stream<Person> filtered = stream.filter(person -> person.getAge() > 20);

Phía trên là cách implement đơn giản nhất, ngoài ra Java 8 cung cấp cho chúng ta một Functional Interface khác để thực hiện việc filter là Predicate.

@FunctionalInterface
public Predicate<T> {
void test(T t);
// default method
default Predicate<T> and(Predicate<? super T> other) {
// more code here
}
default Predicate<T> or(Predicate<? super T> other) {
// more code here
}
default Predicate<T> negate() {
// more code here
}
// static
static <T> Predicate<T> isEqual(Object o) {
// more code here
}
}
view raw predicate.java hosted with ❤ by GitHub

Dễ dàng thấy chúng ta có cách sử dụng kết hợp các Predicate như.

Predicate<Person> p1 = p -> p.getAge() > 20;
Predicate<Person> p2 = p -> p.getAge() < 30;
Predicate<Person> p3 = p -> p.getGender().equals("male");
Predicate<Person> p = p1.and(p2).and(p3);

Trong Predicate có hàm static là isEqual(), sử dụng như sau.

Predicate<String> p = Predicate.isEqual("two");
Stream<String> stream = Stream.of("one", "two", "three");
Stream<String> filtered = stream.filter(p);

Kết quả của filtered sẽ là Stream chứa duy nhất chuỗi “two”.

Chắc các bạn sẽ thắc mắc vậy sau khi filter thì dữ liệu sẽ trả về Stream, vậy trong stream đó là cái gì?

Nó chẳng là gì cả, đặc tính của Stream là không giữ bất cứ dữ liệu nào, dòng code stream.filter() chỉ là một declaration, không có bất cứ dữ liệu nào được xử lý tại dòng code này. Đây là lazy call, có nghĩa là khi tôi gọi method đó, compiler sẽ hiểu là một declaration chứ không thực hiện ngay tức thì. Lazy call cũng là cách gọi chung của tất cả method của Stream.

Sẵn tiện, chúng ta sẽ ngó qua method peek(). Đây là một method tương tự như forEach() nhưng thay vì không trả về gì cả như forEach() thì nó trả về một Stream như filter().

Cùng xem xét đoạn code dưới đây.

List<Person> result = new ArrayList<>();
List<Person> people = new ArrayList<>();
people.stream()
.peek(System.out::println)
.filter(p -> p.getAge() > 20)
.peek(result::add);
view raw peek.java hosted with ❤ by GitHub

Thử chạy các bạn sẽ thấy chẳng có gì được in ra màn hình cũng như chẳng có gì được add vào result. Bời vì một operation trả về Stream được coi là một hoạt động trung gian (Intermediary Operation). Và cũng chỉ như một declaration, chẳng có thứ gì được xử lý cho đến khi Final Operation, ở đây là forEach() được gọi, nó sẽ kích hoạt quá trình xử lý Stream. Thay đổi đoạn code như sau, và bạn sẽ thấy code của mình xuất ra màn hình list person cũng như thêm vào list result những person được đã được filter.

people.stream()
.peek(System.out::println)
.filter(p -> p.getAge() > 20)
.forEach(result::add);
view raw final.java hosted with ❤ by GitHub

Its cool huh?

Tổng kết

Chúng ta đã tìm hiểu về Stream, thế nào là intermediary và final operation, về các API như forEach(Consumer), peek(Consumer), filter(Predicate). Trong bài kế tiếp chúng ta sẽ tìm hiểu tiếp về Mapping cũng như Reducing thông qua Stream API.

Tạm biệt và hẹn gặp lại.

Một suy nghĩ 3 thoughts on “Java những điều có thể bạn đã biết: Có gì mới trong Java 8 (Phần 1)

  1. Phần code của bạn đùng gì để format vậy? Chỉ giúp mình với nhé. :) Với cả bạn format font chữ gì để viết tiếng việt không bị đổi font với các dấu “ế”, “ừ” vậy?

Bình luận

Điền thông tin vào ô dưới đây hoặc nhấn vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s