Đây là bài tiếp nối vơi bài Java những điều có thể bạn đã biết: Có gì mới trong Java 8 (Phần 1), Trong bài này chúng ta sẽ tiếp tục tìm hiểu tiếp về Mapping cũng như Reducing thông qua Stream API.
Mapping
Stream API cung cấp method map(), flatMap() thể thực hiện việc bước mapping, method này trả về một stream, vì thế nó chính là một intermediary operation.
Ngoài ra cũng như forEach() sử dụng Consumer hay filter() sử dụng Predicate, map() hay flatMap() sử dụng một thứ gọi là Function để quy định việc mapping, và Function cũng là một Functional Inteface.
<R> Stream<R> flatMap(Function<T, Stream<R>> flatMapper); | |
<R> Stream<R> map(Function<T, R> mapper); |
Cách sử dụng đơn giản thôi.
List<Person> people = new ArrayList<>(); | |
Stream<Person> stream = people.stream(); | |
Stream<Integer> ages = stream.map(p -> p.getAge()); |
Phía trên chính là cách sử dụng cơ bản của hàm map(). Method map() sẽ nhận vào một Function, Function đó sẽ gọi từng phần tử trong của input stream để xử lý trả về kết quả, và đưa nó vào trong output stream.
@FunctionalInterface | |
public interface Function<T, R> { | |
R apply(T t); | |
// default method | |
default <V> Function<V, R> compose(Function<? super V, ? extends T> before); | |
default <V> Function<V, T> andThen(Function<? super R, ? extends V> after); | |
// static method | |
static <T> Function<T, T> identity() { | |
return t -> t; | |
} | |
} |
Còn đối với hàm flatMap(), sẽ trả về một stream mà trong stream đó chứa các phần tử là tất cả các phần tử của mapped stream trước đó dựa trên mapping function. Đại loại kết của trả về của flatMap là Stream<Stream<R>>, một stream của các stream và thay vì việc đó thì gom hết vào một stream cho tiện.
Mỗi mapped stream sau khi được xử lý để đưa vào output stream thì sẽ được đóng lại. Đoạn này hơi khó hiểu nhưng cụ thể cách dùng như sau.
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7); | |
List<Integer> list2 = Arrays.asList(2, 4, 6); | |
List<Integer> list3 = Arrays.asList(3, 5, 7); | |
List<List<Integer>> list = Arrays.asList(list1, list2, list3); | |
// Output [[1, 2, 3, 4, 5, 6, 7], [2, 4, 6], [3, 5, 7]] | |
System.out.println(list); | |
Function<List<Integer>, Stream<Integer>> flatMapper = l -> l.stream(); | |
// Output 1 2 3 4 5 6 7 2 4 6 3 5 7 | |
list.stream().flatMap(flatMapper).forEach(System.out::println); |
Reduction
Có hai loại reduction trong Stream API là Aggregation và Collection.
1 – Aggregation
Aggregation bao gồm các phép toán như sum, max, min, blabla…
List<Integer> ages = new ArrayList<>(); | |
Stream<Integer> stream = ages.stream(); | |
Integer sum = stream.reduce(0, (age1, age2) -> age1 + age2)); |
Tham số thứ nhất là identity element để xác định gía trị gốc của kết quả trả về, tham số thứ hai là reduce operation, một reduce operation là một BinaryOperator<T>, BinaryOperator là một Java Interface tương tự như Consumer, Predicate hay Function.
@FunctionalInterface | |
public interface BiFunction<T, U, R> { | |
R apply(T t, U u); | |
// some more default methods | |
} | |
@FunctionalInterface | |
public interface BinaryOperator extends BiFunction<T, T, T> { | |
// T apply(T t1, T t2); | |
// some more static methods | |
} |
Như vậy sẽ đặt ra các câu hỏi như:
- Làm thế nào nếu reduce một empty stream bằng aggregation? Câu trả lời là kết quả của việc reduce một empty stream sẽ là identity element.
- Làm thế nào nếu reduce một stream chỉ có một phần tử? Dễ hiểu kết quả của việc reduce sẽ chính là element đó.
Ví dụ.
List<Integer> list1 = Arrays.asList(10, 10); | |
Stream<Integer> stream1 = list1.stream(); | |
BinaryOperation<Integer> sum = (i1, i2) -> i1 + i2; | |
Integer id1 = 0; | |
Integer reduce1 = stream1.reduce(id1, sum); | |
Stream<Integer> stream2 = Stream.empty(); | |
// result is 0 | |
Integer reduce2 = stream2.reduce(id1, sum); | |
Integer id2 = 100; | |
// result is 120 | |
Integer reduce3 = stream1.reduce(id2, sum); | |
Integer id3 = 0; | |
List<Integer> list2 = Arrays.asList(10); | |
// resut is 10 | |
Integer reduce4 = list2.stream.reduce(id3, Integer::max); | |
Integer id4 = 0; | |
List<Integer> list3 = Arrays.asList(-10); | |
// resut is 0 | |
Integer reduce5 = list3.stream.reduce(id4, Integer::max); |
Ngoài ra, có một số trường hợp khác như sử dụng tìm max trong stream như sau.
List<Integer> list = new ArrayList<>(); | |
Stream<Integer> stream = list.stream(); | |
Optional<Integer> max = stream.max(Comparator.naturalOrder()); |
Như các bạn có thể thấy tôi dùng Optional<Integer>, khi tôi dùng Optional, có nghĩa là kết quả trả về có thể có hoặc không, bởi vì giả sử nếu stream empty chúng ta sẽ không biết được đâu là max. Tóm lại chúng ta dùng Optional khi chúng ta không biết được kết quả trả về sẽ là gì. Cách sử dụng Optional như sau.
Optional<Integer> opt = ... | |
Integer result; | |
if (opt.isPresent()) { | |
result = opt.get(); | |
} else { | |
// more code here | |
} |
Hoặc chúng ta có thể sử dụng bằng cách.
Integer result = opt.orElse(0); | |
Integer result = opt.orElseThrow(MyException::new); // lazy construct |
Khi đó chúng ta sẽ có kết quả trả về là giá trị trong optional nếu tồn tại, nếu không nó sẽ trả về giá trị mặc định là giá trị mà chúng ta đã truyền vào hàm orElse(). Hoặc throw exception được chỉ định trước thông qua việc sử dụng orElseThrow().
Ngoài ra chúng ta còn có các reduction operator như min(), count(), allMatch(), noneMatch(), anyMatch(), findFirst(), findAny(), blabla… các bạn có thể xem thêm trong Javadoc.
Tất cả các reduction operator đều là terminal operator, có nghĩa là khi call reduction quá trình xử lý dữ liệu trong stream sẽ được thực hiện. Cụ thể.
Optional<Integer> minAge = person | |
.stream() | |
.map(p -> p.getAge()) // return Stream<Integer> | |
.filter(age -> age > 20) // return Stream<Integer> | |
.min(Comparator.naturalOrder()); // terminal operation | |
people.stream() | |
.map(p -> p.getLastname()) | |
.allMatch(length < 10); // terminal operation | |
2 – Collection
Collection hay còn gọi là mutable reduction, là việc gom hết tất cả các phần tử trả về từ stream sau khi mapping, filtering vào một container.
Ví dụ.
List<Person> people = new ArrayList<>(); | |
String result = people.stream() | |
.filter(p -> p.getAge() > 20) | |
.map(Person::getLastname) | |
.collect(Collectors.joining(",")); | |
List<String> list = people.stream() | |
.filter(p -> p.getAge() > 20) | |
.map(Person::getLastname) | |
.collect(Collectors.toList()); | |
Map<Integer, List<Person>> map = people.stream() | |
.filter(p -> p.getAge() > 20) | |
.collect(Collectors.groupingBy(Person::getAge)); |
Tôi nghĩ những ví dụ trên khá dễ hiểu về cách hoạt động của Collector rồi. Ngoài ra khi collect các bạn có thể thực hiện reduce ngay trong downstream, ví dụ như sau.
Map<Integer, Long> map = people.stream() | |
.filter(p -> p.getAge() > 20) | |
.collect( | |
Collectors.groupingBy(Person::getAge), | |
Collectors.counting() // the downstream collector | |
); | |
Map<Integer, Set<String> map = people.stream() | |
.filter(p -> p.getAge() > 20) | |
.collect( | |
Collectors.groupingBy( | |
Person::getAge, | |
Collectors.mapping( | |
Person::getLastname, | |
Collectors.toCollection(TreeSet::new) | |
) | |
), | |
); |
Tổng kết
Stream là một đối tượng có thể giúp cho ta xử lý data một cách hiệu quả và dễ dàng, cũng như không giới hạn lượng data đưa vào stream.
Có ba loại hoạt động chính khi sử dụng stream là filtering/mapping/reduction.
Lưu ý khi sử dụng stream, đó là stream không thể reuse, một khi stream đã được xử lý thì sẽ không dùng chính stream đó để xử lý cho việc khác được nữa.
Hy vọng qua hai bài viết ngẳn ngủi này, các bạn đã hiểu được phần nào về Stream và cách xử lý dữ liệu thông qua Stream API để có thể áp dụng vào những trường hợp cụ thể.
Chào thân ái, và hẹn gặp lại trong những bài viết tiếp theo.
Giả sử class Student có 3 trường: Name, Age, Address. Em muốn lấy ra 2 trường Name, Age, bỏ vào kết quả thì viết trong java thế nào ạ.
Trong C# là: var result = students.Select(s => new {s.Name, s.Age});
ý của e là lúc filter hay lúc mapping hay lúc reduce, mình có làm một ví dụ nho nhỏ như sau.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
example.java
hosted with ❤ by GitHub