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

Đâ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);
view raw mapping.java hosted with ❤ by GitHub

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());
view raw mapping.java hosted with ❤ by GitHub

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;
}
}
view raw function.java hosted with ❤ by GitHub

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);
view raw flatMap.java hosted with ❤ by GitHub

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));
view raw aggregation.java hosted with ❤ by GitHub

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
}
view raw bifunction.java hosted with ❤ by GitHub

 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);
view raw reduce.java hosted with ❤ by GitHub

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());
view raw max.java hosted with ❤ by GitHub

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
}
view raw optional.java hosted with ❤ by GitHub

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
view raw optional.java hosted with ❤ by GitHub

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
view raw terminal.java hosted with ❤ by GitHub

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));
view raw collection.java hosted with ❤ by GitHub

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)
)
),
);
view raw downstream.java hosted with ❤ by GitHub

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.

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

  1. 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});

    1. ý 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.

      import java.util.Arrays;
      import java.util.List;
      import java.util.stream.Collectors;
      public class HelloWorld{
      public static class Person {
      public String name;
      public String address;
      public Integer age;
      public Person(String name, String address, Integer age) {
      this.name = name;
      this.address = address;
      this.age = age;
      }
      @Override
      public String toString() {
      return name + " " + address + " " + age;
      }
      }
      public static class Result {
      public String name;
      public String address;
      public Result(String name, String address) {
      this.name = name;
      this.address = address;
      }
      @Override
      public String toString() {
      return name + " " + address;
      }
      }
      public static void main(String []args){
      List<Person> people = Arrays.asList(new Person("Hoang", "1", 19), new Person("Hoa", "2", 20));
      List<Result> result = people.stream().map(p > new Result(p.name, p.address)).collect(Collectors.toList());
      System.out.println(result);
      }
      }

      view raw
      example.java
      hosted with ❤ by GitHub

Trả lời Phạm Huy Hoàng Hủy trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích 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 )

Google photo

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

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đă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