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);

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>, 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 Steam 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, 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, 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 lại.

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.

Bài gốc: https://codeaholicguy.wordpress.com/2015/12/20/java-nhung-dieu-co-the-ban-da-biet-co-gi-moi-trong-java-8-phan-2/

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Hoàng Nguyễn

35 bài viết.
398 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
82 34
Nhu cầu về Javascript developer hiện nay trong thị trường IT là rất lớn. Nếu bạn có kiến thức ở mảng này thì cơ hội nghề nghiệp cũng như thu nhập c...
Hoàng Nguyễn viết gần 2 năm trước
82 34
White
51 19
Microservices hiện đang nhận được rất nhiều sự chú ý: các bài viết, các blog, các cuộc thảo luận trên phương tiện truyền thông, trên mạng xã hội, v...
Hoàng Nguyễn viết hơn 2 năm trước
51 19
White
32 2
Chuyện tối ưu code, (Link) là công việc hàng ngày của mỗi lập trình viên, điều đó ai cũng biết. Nhưng liệu code tối ưu có phải là code đẹp, và ngượ...
Hoàng Nguyễn viết 8 tháng trước
32 2
Bài viết liên quan
White
9 0
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ư (Link) hay (Link). Hôm nay tôi sẽ giới thiệu đến các bạn về Stream API và...
Hoàng Nguyễn viết hơn 2 năm trước
9 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
35 bài viết.
398 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!