Header Ads Widget

Responsive Advertisement

Nổi bật

6/recent/ticker-posts

Stream API trong Java8




Chào mừng bạn đến với hướng dẫn Stream API trong Java 8. Trong một vài bài đăng trước về java 8, chúng tôi đã xem xét Các Thay đổi Interface Java 8Functional Interface và Biểu thức Lambda . Hôm nay chúng ta sẽ xem xét một trong những API chính được giới thiệu trong Java 8 - Java Stream.


    1.    Java Stream

    Trước khi chúng ta xem xét các Ví dụ về Java Stream API, hãy xem tại sao nó lại được yêu cầu. Giả sử chúng ta muốn lặp lại một danh sách các số nguyên và tìm ra tổng của tất cả các số nguyên lớn hơn 10.Trước Java 8, cách tiếp cận để làm điều đó sẽ là:

    private static int sumIterator(List<Integer> list) {

           Iterator<Integer> it = list.iterator();

           int sum = 0;

           while (it.hasNext()) {

                  int num = it.next();

                  if (num > 10) {

                         sum += num;

                  }

           }

           return sum;

    }



    Có ba vấn đề chính với cách tiếp cận trên:
    ·    Chúng tôi chỉ muốn biết tổng các số nguyên nhưng chúng tôi cũng sẽ phải cung cấp cách lặp sẽ diễn ra, điều này còn được gọi là external iteration (lặp bên ngoài) vì chương trình khách đang xử lý thuật toán để lặp qua danh sách.
    ·    Chương trình có tính chất tuần tự, không có cách nào chúng ta có thể thực hiện song song việc này một cách dễ dàng.
    ·    Có rất nhiều mã để thực hiện ngay cả một nhiệm vụ đơn giản.

    Để khắc phục tất cả những thiếu sót trên, Java 8 Stream API đã được giới thiệu. Chúng ta có thể sử dụng Java Stream API để triển khai internal iteration (lặp lại nội bộ), điều đó tốt hơn vì khung công tác java kiểm soát việc lặp lại.

    Internal iteration (lặp lại nội bộ), cung cấp một số tính năng như thực thi tuần tự và song song, lọc dựa trên các tiêu chí đã cho, ánh xạ, v.v.

    Hầu hết các đối số của phương thức Java 8 Stream API là các Functional Interface, vì vậy các biểu thức lambda hoạt động rất tốt với chúng. Hãy xem làm thế nào chúng ta có thể viết logic ở trên trong một câu lệnh dòng đơn bằng cách sử dụng Java Streams.

        private static int sumStream(List<Integer> list) {

               return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();

        }


    Lưu ý rằng chương trình trên sử dụng chiến lược lặp lại java framework, các phương pháp lọc và ánh xạ và sẽ tăng hiệu quả.

    Trước hết, chúng ta sẽ xem xét các khái niệm cốt lõi của Java 8 Stream API và sau đó chúng ta sẽ đi qua một số ví dụ để hiểu các phương thức thường được sử dụng nhất.

    2. Collections và Java Stream

    Collections là một cấu trúc dữ liệu trong bộ nhớ để lưu giữ các giá trị và trước khi chúng tôi bắt đầu sử dụng Collection, tất cả các giá trị phải được điền. Trong khi java Stream là một cấu trúc dữ liệu được tính toán theo yêu cầu.

    Java Stream không lưu trữ dữ liệu, nó hoạt động trên cấu trúc dữ liệu nguồn (collection array) và tạo ra dữ liệu tổng hợp mà chúng ta có thể sử dụng và thực hiện các hoạt động cụ thể. Chẳng hạn như chúng ta có thể tạo một Stream từ danh sách và lọc nó dựa trên một điều kiện.

    Các hoạt động của Java Stream sử dụng các Functional interface, điều này làm cho nó rất phù hợp cho việc lập trình chức năng sử dụng biểu thức lambda. Như bạn có thể thấy trong ví dụ trên, việc sử dụng các biểu thức lambda làm cho mã của chúng ta dễ đọc và ngắn gọn.

    Nguyên tắc lặp nội bộ của Java 8 Stream giúp đạt được tính năng tìm kiếm lười biếng trong một số hoạt động của Stream. Ví dụ: lọc, ánh xạ hoặc loại bỏ trùng lặp có thể được thực hiện một cách lười biếng, cho phép hiệu suất và phạm vi tối ưu hóa cao hơn.

    Java Streams là có thể sử dụng được, vì vậy không có cách nào để tạo một tham chiếu đến stream để sử dụng trong tương lai. Vì dữ liệu theo yêu cầu nên không thể sử dụng lại cùng một luồng nhiều lần.

    Java 8 Stream hỗ trợ tuần tự cũng như xử lý song song, xử lý song song có thể rất hữu ích trong việc đạt được hiệu suất cao cho các bộ sưu tập lớn.

    Tất cả các class interface Java Stream API đều nằm trong gói java.util.stream. Vì chúng ta có thể sử dụng các kiểu dữ liệu nguyên thủy như int, long trong bộ sưu tập sử dụng auto-boxing và các hoạt động này có thể mất rất nhiều thời gian, có những lớp học đặc biệt với nhiều loại nguyên thủy - IntStream, LongStream DoubleStream.

    3. Functional interfaces trong Java 8 Stream

    Một số Functional interface thường được sử dụng trong các phương thức Stream API trong Java 8 là:
    1. Function và BiFunction: Function đại diện cho một hàm nhận một loại đối số và trả về một loại đối số khác. Function<T, R> là dạng tổng quát trong đó T là kiểu đầu vào của hàm và R là kiểu kết quả của hàm.

    Để xử lý các loại nguyên thủy, có rất cụ thể các Functional interface - ToIntFunction, ToLongFunction, ToDoubleFunction, ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction, LongToIntFunction, LongToDoubleFunction, IntToLongFunction, IntToDoubleFunction, vv

    Một số phương thức Stream nơi Function hoặc sự chuyên môn hóa ban đầu của nó được sử dụng là:
    Ø  <R> Stream<R> map(Function<? super T, ? extends R> mapper)
    Ø  IntStream mapToInt(ToIntFunction<? super T> mapper) - tương tự đối với Stream cụ thể nguyên thủy trả về long và double.
    Ø  IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper)- tương tự đối với long và double
    Ø  <A> A[] toArray(IntFunction<A[]> generator)
    Ø  <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)

    2. Predicate and BiPredicate: Nó đại diện cho một vị từ dựa vào các phần tử của Stream được kiểm tra. Điều này được sử dụng để lọc các phần tử khỏi Java Stream. Cũng giống như Function, có các interface cụ thể nguyên thủy cho int, long và double.

    Một số phương pháp Stream nơi mà Predicate hoặc BiPredicate chuyên môn được sử dụng là:

    Ø  Stream<T> filter(Predicate<? super T> predicate)

    Ø  boolean anyMatch(Predicate<? super T> predicate)

    Ø  boolean allMatch(Predicate<? super T> predicate)

    Ø  boolean noneMatch(Predicate<? super T> predicate)

    3. Consumer và BiConsumer: Nó đại diện cho một hoạt động chấp nhận một đối số đầu vào duy nhất và không trả về kết quả nào. Nó có thể được sử dụng để thực hiện một số hành động trên tất cả các phần tử của java Stream.

    Một số phương thức Java 8 Stream mà Consumer, BiConsumer hoặc các interface chuyên môn hóa ban đầu được sử dụng là:

    Ø  Stream<T> peek(Consumer<? super T> action)

    Ø  void forEach(Consumer<? super T> action)

    Ø  void forEachOrdered(Consumer<? super T> action)

    4. Supplier: Supplier đại diện cho một hoạt động mà qua đó chúng tôi có thể tạo ra các giá trị mới trong Stream. Một số phương thức trong Stream có đối số Supplier là:

    Ø  public static<T> Stream<T> generate(Supplier<T> s)

    Ø  <R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)

    4.     Java Optional java.util.Optional

    Java Optional là một đối tượng vùng chứa có thể chứa hoặc không thể chứa giá trị khác rỗng. Nếu có giá trị, isPresent() sẽ trả về true get() sẽ trả về giá trị. Các hoạt động đầu cuối Stream trả về đối tượng Optional. Một số phương pháp này là:

    Ø  Optional<T> reduce(BinaryOperator<T> accumulator)
    Ø  Optional<T> min(Comparator<? super T> comparator)
    Ø  Optional<T> max(Comparator<? super T> comparator)
    Ø  Optional<T> findFirst()
    Ø  Optional<T> findAny()

    5.      Spliterator - java.util.Spliterator

    Để hỗ trợ thực thi song song trong Java 8 Stream API, Spliterator interface được sử dụng phương thức trySplit - Spliterator trả về một Spliterator mới quản lý một tập hợp con các phần tử của Spliterator ban đầu.

    6.    Java Stream Intermediate và Terminal Operations

    Các hoạt động Java Stream API trả về một Stream mới được gọi là intermediate operations (các hoạt động trung gian). Hầu hết các trường hợp, các hoạt động này có bản chất lười biếng, vì vậy chúng bắt đầu tạo ra các phần tử Stream mới và gửi nó đến hoạt động tiếp theo. Intermediate operations (các hoạt động trung gian) không bao giờ là hoạt động tạo ra kết quả cuối cùng. Các phép toán trung gian thường được sử dụng là filter map.

    Các hoạt động Java8 Stream API trả về một kết quả hoặc tạo ra một tác dụng phụ. Khi phương thức đầu cuối được gọi trên một luồng, nó sẽ sử dụng luồng và sau đó chúng ta không thể sử dụng luồng. Terminal operations (Các hoạt động đầu cuối) là háo hức về bản chất, tức là chúng xử lý tất cả các phần tử trong luồng trước khi trả về kết quả. Thường được sử dụng phương pháp thiết bị đầu cuối được forEach, toArray, min, max, findFirst, anyMatch, allMatch vv. Bạn có thể xác định các phương pháp thiết bị đầu cuối từ các kiểu trả về, họ sẽ không bao giờ quay trở lại một Stream.

    7.    Java Stream Short Circuiting Operations (Các hoạt động tuần hoàn ngắn trong dòng Java)

    Intermediate operation (Hoạt động trung gian) được gọi là short circuiting (đoản mạch) nếu nó có thể tạo ra dòng hữu hạn cho một dòng vô hạn. Ví dụ limit()skip() là hai hoạt động trung gian ngắn mạch.

    Terminal operation được gọi là short circuiting nếu nó có thể kết thúc trong thời gian hữu hạn đối với dòng vô hạn. Ví dụ anyMatch, allMatch, noneMatch, findFirst và findAnyshort circuiting terminal operations.

    8.    Ví dụ về Java Stream

    Tôi đã trình bày gần như tất cả các phần quan trọng của Java 8 Stream API. Thật thú vị khi sử dụng các tính năng API mới này và chúng ta hãy xem nó hoạt động với một số ví dụ về java Stream.

    Tạo các Java Stream

    Có một số cách mà chúng ta có thể tạo một Stream trong java từ array collection. Hãy xem xét những điều này với các ví dụ đơn giản.

    Ø  Chúng tôi có thể sử dụng Stream.of() để tạo Stream từ loại dữ liệu tương tự. Ví dụ, chúng ta có thể tạo Java Stream các số nguyên từ một nhóm các đối tượng int hoặc Integer.

    Stream<Integer> stream = Stream.of(1,2,3,4);

    Ø  Chúng ta có thể sử dụng Stream.of() với một mảng Object để trả về Stream. Lưu ý rằng nó không hỗ trợ autoboxing, vì vậy chúng tôi không thể chuyển mảng kiểu nguyên thủy.

    Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4});

    //works fine

    Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4});

    // Compile time error, Type mismatch: cannot convert from Stream<int[]> to Stream<Integer>

    Ø  Chúng ta có thể sử dụng Collection stream() để tạo luồng tuần tự và parallelStream() để tạo luồng song song.

    List<Integer> myList = new ArrayList<>();

    for (int i = 0; i < 100; i++)

                myList.add(i);

    // sequential stream

    Stream<Integer> sequentialStream = myList.stream();

    // parallel stream

    Stream<Integer> parallelStream = myList.parallelStream();

    Ø  Chúng ta có thể sử dụng các phương pháp Stream.generate()Stream.iterate() để tạo Stream.

    Stream<String> stream1 = Stream.generate(() -> {return "abc";});

    Stream<String> stream2 = Stream.iterate("abc", (i) -> i);

    Ø  Sử dụng phương pháp Arrays.stream()String.chars().

    LongStream is = Arrays.stream(new long[]{1,2,3,4});

    IntStream is2 = "abc".chars();

    Chuyển đổi Java Stream thành Collection hoặc Array

    Có một số cách để chúng ta có thể lấy Collection hoặc Array từ java Stream.

    Ø  Chúng ta có thể sử dụng phương thức collect() của java Stream để lấy List, Map hoặc Set từ stream.

    Stream<Integer> intStream = Stream.of(1, 2, 3, 4);

    List<Integer> intList = intStream.collect(Collectors.toList());

    System.out.println(intList); // prints [1, 2, 3, 4]

    intStream = Stream.of(1, 2, 3, 4); // stream is closed, so we need to create it again

    Map<Integer, Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i + 10));

    System.out.println(intMap); // prints {1=11, 2=12, 3=13, 4=14}

    Ø  Chúng ta có thể sử dụng phương thức Stream: toArray() để tạo một Array từ Stream.

    Stream<Integer> intStream = Stream.of(1, 2, 3, 4);

    Integer[] intArray = intStream.toArray(Integer[]::new);

    System.out.println(Arrays.toString(intArray)); // prints [1, 2, 3, 4]


    Các hoạt động trung gian của Java Stream

    Hãy xem xét ví dụ về hoạt động trung gian của java Stream thường được sử dụng.
    1. Ví dụ về Stream filter(): Chúng ta có thể sử dụng phương thức filter() để kiểm tra các phần tử của Stream cho một điều kiện và tạo danh sách đã lọc.

    // Stream filter()

    List<Integer> myList = new ArrayList<>();

    for (int i = 0; i < 100; i++)

       myList.add(i);

    Stream<Integer> sequentialStream = myList.stream();

    Stream<Integer> highNums = sequentialStream.filter(p -> p > 90);

    // filter numbers greater than 90

    System.out.print("High Nums greater than 90=");

    highNums.forEach(p -> System.out.print(p + " "));

    // prints "High Nums greater than 90=91 92 93 94 95 96 97 98 99 "


    2. Ví dụ về Stream map():
    Chúng ta có thể sử dụng map() để áp dụng các hàm cho một Stream. Hãy xem cách chúng ta có thể sử dụng nó để áp dụng hàm viết hoa cho danh sách các String.

    // +++++++++++++Stream map()

    Stream<String> names = Stream.of("aBc", "d", "ef");

    System.out.println(names.map(s -> {

              return s.toUpperCase();

    }).collect(Collectors.toList()));// prints [ABC, D, EF]


    3. Ví dụ về Stream sorted():
    Chúng ta có thể sử dụng sorted() để sắp xếp các phần tử của Stream bằng cách truyền đối số Comparator.

    Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456");

    List<String> reverseSorted = names2.sorted(Comparator.reverseOrder())

                     .collect(Collectors.toList());

    System.out.println(reverseSorted); // [ef, d, aBc, 123456]

    Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456");

    List<String> naturalSorted = names3.sorted().collect(Collectors.toList());

    System.out.println(naturalSorted); // [123456, aBc, d, ef]


    4. Ví dụ về Stream flatMap():
    Chúng ta có thể sử dụng flatMap() để tạo luồng từ luồng danh sách. Hãy xem một ví dụ đơn giản để xóa nghi ngờ này.

    Stream<List<String>> namesOriginalList = Stream.of(

              Arrays.asList("Pankaj"),

              Arrays.asList("David", "Lisa"),

              Arrays.asList("Amit"));

    // flat the stream from List<String> to String stream

    Stream<String> flatStream = namesOriginalList.flatMap(

              strList -> strList.stream());

    flatStream.forEach(System.out::println);


    java stream terminal (Hoạt động đầu cuối dòng Java)
    Hãy xem xét một số ví dụ về hoạt động của java stream terminal.

    1. Ví dụ về Stream reduce() : Chúng ta có thể sử dụng reduce() để thực hiện giảm các phần tử của Stream, sử dụng hàm tích lũy liên kết và trả về Optional. Hãy xem cách chúng ta có thể sử dụng nó để nhân các số nguyên trong một Stream.

    //Stream reduce

    Stream<Integer> numbers = Stream.of(1,2,3,4,5);   

    Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;});

    if(intOptional.isPresent())

       System.out.println("Multiplication = "+intOptional.get()); //120


    2. Ví dụ về Stream cout() :
    Chúng ta có thể sử dụng thao tác đầu cuối này để đếm số lượng mục trong luồng.

    /// Stream count

    Stream<Integer> numbers1 = Stream.of(1, 2, 3, 4, 5);

    System.out.println("Number of elements in stream=" + numbers1.count()); // 5


    3. Ví dụ về Stream forEach():
    Điều này có thể được sử dụng để lặp qua Stream. Chúng ta có thể sử dụng điều này thay cho trình lặp. Hãy xem cách sử dụng nó để in tất cả các phần tử của luồng.

    // Stream foreach()

    Stream<Integer> numbers2 = Stream.of(1, 2, 3, 4, 5);

    numbers2.forEach(i -> System.out.print(i + ",")); // 1,2,3,4,5,


    4. Ví dụ về Stream Match():
    Hãy xem một số ví dụ về các phương thức đối sánh trong Stream API.

    // Stream foreach()

    Stream<Integer> numbers2 = Stream.of(1, 2, 3, 4, 5);

    numbers2.forEach(i -> System.out.print(i + ",")); // 1,2,3,4,5,

    // Stream Match()

    Stream<Integer> numbers3 = Stream.of(1, 2, 3, 4, 5);

    System.out.println("Stream contains 4? " + numbers3.anyMatch(i -> i == 4));

    // Stream contains 4? true

    Stream<Integer> numbers4 = Stream.of(1, 2, 3, 4, 5);

    System.out.println("Stream contains all elements less than 10? "

              + numbers4.allMatch(i -> i < 10));

    // Stream contains all elements less than 10? true

    Stream<Integer> numbers5 = Stream.of(1, 2, 3, 4, 5);

    System.out.println("Stream doesn't contain 10? "

              + numbers5.noneMatch(i -> i == 10));

    // Stream doesn't contain 10? true


    5. Ví dụ về Stream findFirst(): Đây là thao tác đầu cuối ngắn mạch, hãy xem cách chúng ta có thể sử dụng nó để tìm chuỗi đầu tiên từ luồng bắt đầu bằng D.

    // Stream findFirst

    Stream<String> names4 = Stream.of("Pankaj", "Amit", "David", "Lisa");

    Optional<String> firstNameWithD = names4.filter(

                     i -> i.startsWith("D")).findFirst();

    if (firstNameWithD.isPresent()) {

       System.out.println("First Name starting with D="

                     + firstNameWithD.get()); // David

    }

    9.    Hạn chế của Stream API Java 8

    Java 8 Stream API mang đến rất nhiều thứ mới để làm việc với danh sách và mảng, nhưng nó cũng có một số hạn chế.

    1. Biểu thức lambda không trạng thái (Stateless): Nếu bạn đang sử dụng dòng song song và biểu thức lambda là trạng thái (stateful), nó có thể dẫn đến các phản hồi ngẫu nhiên. Hãy xem nó với một chương trình đơn giản.

    StatefulParallelStream.java

    package com.codetimes90.Java8.StreamAndLambda;

    import java.util.ArrayList;

    import java.util.Arrays;

    import java.util.List;

    import java.util.stream.Stream;

     

    public class Stream_StatefulParallelStream {

     

           public static void main(String[] args) {

                  List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);

                  List<Integer> result = new ArrayList<Integer>();

                  Stream<Integer> stream = ss.parallelStream();

                  stream.map(s ->{ synchronized (result) {

                         if(result.size() < 10) {

                               result.add(s);

                         }

                  }

                  return s;

                  }).forEach(e -> {});

                  System.out.println(result);  

           }

    }


    Nếu chúng tôi chạy chương trình trên, bạn sẽ nhận được các kết quả khác nhau vì nó phụ thuộc vào cách luồng đang được lặp lại và chúng tôi không có bất kỳ thứ tự nào được xác định để xử lý song song. Nếu chúng ta sử dụng luồng tuần tự, thì vấn đề này sẽ không phát sinh.

    2. Sau khi một Stream được sử dụng, không thể sử dụng luồng đó sau này. Như bạn có thể thấy trong các ví dụ trên, mỗi khi tôi tạo Stream.

    3. Có rất nhiều phương thức trong Stream API và phần khó hiểu nhất là các phương thức được nạp chồng. Nó làm cho đường cong học tập mất thời gian.

    Đó là tất cả cho hướng dẫn ví dụ về Java 8 Stream. Tôi mong muốn sử dụng tính năng này và làm cho mã có thể đọc được với hiệu suất tốt hơn thông qua xử lý song song.

    Bài viết được dịch lại từ journaldev.com. Cảm ơn bạn đã theo dõi!

    Đăng nhận xét

    0 Nhận xét