Java8 的 Stream 流式操作之王者歸來

歐陽思海發表於2019-09-03
個人部落格地址:http://blog.ouyangsihai.cn/ja...

相對於Java8之前的Java的相關操作簡直是天差地別,Java8 的流式操作的出現,也很大程度上改變了開發者對於Java的繁瑣的操作的印象,從此,Java也走向了函數語言程式設計的道路!

1 流的建立

1.1 流的建立方法

既然需要聊聊流的操作,那麼,首先還是先看看怎麼建立流。

建立流的方法有三種,分別是:Stream.of()、Stream.iterate()、Stream.generate(),然後,分別看一下這三個方法的宣告。

static <T> Stream<T> of(T... values)

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)

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

Stream.of():引數很簡單,就是一系列的泛型引數。
Stream.iterate():第一個引數是一個初始值,第二個引數是一個操作。
Stream.generate():引數就是一個Supplier的供給型的引數。

1.2 流的建立方法舉例

/*
     * @Author 歐陽思海
     * @Description  建立流
     * @Date 11:05 2019/8/26
     * @Param []
     * @return void
     **/
    @Test
    public void testCreateStream() {
        //利用Stream.of方法建立流
        Stream<String> stream = Stream.of("hello", "world", "Java8");
        stream.forEach(System.out::println);
        System.out.println("##################");
        //利用Stream.iterate方法建立流
        Stream.iterate(10, n -> n + 1)
                .limit(5)
                .collect(Collectors.toList())
                .forEach(System.out::println);
        System.out.println("##################");
        //利用Stream.generate方法建立流
        Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);
        System.out.println("##################");
        //從現有的集合中建立流
        List<String> strings = Arrays.asList("hello", "world", "Java8");
        String string = strings.stream().collect(Collectors.joining(","));
        System.out.println(string);
    }

在上面的例子中,Stream.of()方法的引數是幾個字串,Stream.iterate()方法的第一個引數是初始值 10,第二個引數是在10 的基礎上每次加 1 的操作,Stream.generate()的引數是用 Random 方法產生隨機數。

1.3 流的建立總結

流的建立有三種方法,分別是Stream.of()、Stream.iterate()、Stream.generate(),這幾個都是 Stream 類的靜態方法,所以,使用起來非常的方便。

2 流的操作

在上一節中,我們知道怎麼建立流了,接下來,我們就看看對流可以進行哪些操作,使用了 Stream 流之後,是否會比 Java8 之前方便很多呢?

2.1 裝箱流

在處理物件流的時候,可以利用 Collectors 類的靜態方法轉換為集合,例如,將字串流轉換為 List<String> ,這種方式是沒有問題的。

但是,如果遇到 double流想要轉換為 List<Double> 時,這是就會報錯。

DoubleStream.of(1.0, 2.0, 3.0)
                .collect(Collectors.toList());//錯誤的寫法

這種方式就是錯誤的,編譯是不能通過的。

別慌,對於這種問題,有 3 種比較好的解決方法。

利用 boxed 方法

利用 boxed 方法,可以將 DoubleStream 轉換為 Stream<Double> ,例如;

DoubleStream.of(1.0, 2.0, 3.0)
                .boxed()
                .collect(Collectors.toList());

這樣就解決了上面的問題。

利用 mapToObj 方法

利用 mapToObj 方法也可以實現上面的功能,另外,也提供了 mapToInt、mapToLong、mapToDouble 等方法將基本型別流轉換為相關包裝型別。

DoubleStream.of(1.0, 2.0, 3.0)
                .mapToObj(Double::valueOf)
                .collect(Collectors.toList());
collect 方法

一般情況下,我們利用 collect 方法的時候,都是用於將流的資料收集為基本型別的集合,例如;

stream.collect(Collectors.toList())

然而,collect 方法其實還有一種更加一般化的形式,如下;

<R> R collect(Supplier<R> supplier,
                        ObjIntConsumer<R> accumulator,
                        BiCnsumer<R,R> combiner)

上面這種方法的第一個引數是一個供給器,相當於初始化一個容器,第二個引數是累加器,相當於給初始化的容器賦值,第三個引數是組合器,相當於將這些元素全部組合到一個容器

下面,我們通過一個簡單的例子來看看到底是怎麼使用的!

List<Double> list = DoubleStream.of(1.0, 2.0, 3.0)
                .collect(ArrayList<Double>::new, ArrayList::add, ArrayList::addAll);

上面的例子我們可以看到,第一個引數:使用一個靜態方法初始化一個 List 容器,第二個引數:使用靜態方法 add ,新增元素,第三個引數:使用靜態方法 addAll ,用於聯合所有的元素。

從最後的返回值為 List<Double>,我們也可以看出,全部組合成一個初始化的 List 集合中了。

2.2 字串與流之間的轉換

這一小節主要講解一下字串與流之間的轉換,將 String 轉為流有兩種方法,分別是 java.lang.CharSequence 介面定義的預設方法 charscodePoints ,而將流轉為字串就是我們前面已經講解到的方法 collect

/*
     * @Author 歐陽思海
     * @Description  字串與流之間的轉換
     * @Date 9:41 2019/9/2
     * @Param []
     * @return void
     **/
    @Test
    public void testString2Stream() {
        String s = "hello world Java8".codePoints()//轉換成流
                .collect(StringBuffer::new,
                        StringBuffer::appendCodePoint,
                        StringBuffer::append)//將流轉換為字串
                .toString();

        String s1 = "hello world Java8".chars()//轉換成流
                .collect(StringBuffer::new,
                        StringBuffer::appendCodePoint,
                        StringBuffer::append)//將流轉換為字串
                .toString();
    }

在上面的例子中,先用charscodePoints 方法轉換為流,然後都是利用 collect 方法再轉回字串。

2.3 流的對映 map 與 flatMap

流的對映是什麼意思呢,我們先將一個在 Java8 之前的例子,我們常常需要將一個集合的物件的某一個欄位取出來,然後再存到另外一個集合中,這種場景我們在 Java8 之前我們會這樣實現。

/*
     * @Author 歐陽思海
     * @Description  Java8之前的用法
     * @Date 19:31 2019/9/2
     * @Param []
     * @return void
     **/
    @Test
    public void testList() {
        List<Person> list = new ArrayList<>();
        List<Friend> friends = new ArrayList<>();
        friends.add(new Friend("Java5"));
        friends.add(new Friend("Java6"));
        friends.add(new Friend("Java7"));
        Person person = new Person();
        person.setFriends(friends);
        list.add(person);

        List<String> strings = new ArrayList<>();

        for(Person p : list){
            strings.add(p.getName());
        }
    }

是不是這樣很麻煩,這也就是以前大家一直所說的 Python 用一招,Java 需要用花招!

但是,Java8 卻改變了這種現實,我們來看一看怎麼使用 mapflatMap

首先,我們先看一下這倆個方法的宣告

<R> Stream<R> map(Function<? super T,? extends R> mapper)

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

接下來,我們用這兩個方法改寫上面的方式,先看看 map 方法;

/*
     * @Author 歐陽思海
     * @Description  map、flatMap方法
     * @Date 9:50 2019/9/2
     * @Param []
     * @return void
     **/
    @Test
    public void testMapAndFlatMap() {
        List<Person> list = new ArrayList<>();
        List<Friend> friends = new ArrayList<>();
        friends.add(new Friend("Java5"));
        friends.add(new Friend("Java6"));
        friends.add(new Friend("Java7"));
        Person person = new Person();
        person.setFriends(friends);
        list.add(person);

        //對映出名字
        List<String> strings = list.stream().map(Person::getName).collect(Collectors.toList());
    }

通過使用 map 方法,引數給定 Person::getName 對映出 name,然後再用 collect 收集到 List 中,就完成了上面的負責的操作,是不是很舒服。

但是,如果我們用 map 方法想要對映出 friends 屬性,會遇到一個問題;

//對映出朋友
        List<List<Friend>> collect = list.stream().map(Person::getFriends).collect(Collectors.toList());

我們發現,上面的返回值是 List<List<Friend>>,這種形式集合裡面還包著集合,處理有點麻煩,但是,不是還有另外 flatMap 沒有使用嗎,這個方法正好能夠解決這個問題。

List<Friend> collect1 = list.stream().flatMap(friend -> friend.getFriends().stream()).collect(Collectors.toList());

發現,這個方法的返回值是 List<Friend>,正如我們看到的,flatMap 的方法能夠“展平”包裹的流,這就是 mapflatMap 的區別。

2.4 流的連線

流的連線有兩種方式,如果是兩個流的連線,使用 Stream.concat 方法,如果是三個及三個以上的流的連線,就使用 Stream.flatMap 方法。

/**
     * @return void
     * @Author 歐陽思海
     * @Description 流的連線
     * @Date 10:13 2019/9/2
     * @Param []
     **/
    @Test
    public void testConcatStream() {
        //兩個流的連線
        Stream<String> first = Stream.of("sihai", "sihai2", "sihai3");
        Stream<String> second = Stream.of("sihai4", "sihai5", "sihai6");
        Stream<String> third = Stream.of("siha7", "sihai8", "sihai9");
        Stream<String> concat = Stream.concat(first, second);

        //多個流的連線
        Stream<String> stringStream = Stream.of(first, second, third).flatMap(Function.identity());

    }

3 流的規約操作

流的規約操作幾種型別,這裡都講一下。

內建的規約操作

基本型別流都有內建的規約操作。包括average、count、max、min、sum、summaryStatistics,前面的幾個方法相信不用說了,summaryStatistics 方法是前面的幾個方法的結合,下面我們看看他們如何使用。

/**
     * @return void
     * @Author 歐陽思海
     * @Description 內建規約操作
     * @Date 22:04 2019/9/1
     * @Param []
     **/
    @Test
    public void testReduce1() {
        String[] strings = {"hello", "sihai", "hello", "Java8"};
        long count = Arrays.stream(strings)
                .map(String::length)
                .count();
        System.out.println(count);

        System.out.println("##################");

        int sum = Arrays.stream(strings)
                .mapToInt(String::length)
                .sum();
        System.out.println(sum);

        System.out.println("##################");

        OptionalDouble average = Arrays.stream(strings)
                .mapToInt(String::length)
                .average();
        System.out.println(average);

        System.out.println("##################");

        OptionalInt max = Arrays.stream(strings)
                .mapToInt(String::length)
                .max();
        System.out.println(max);

        System.out.println("##################");

        OptionalInt min = Arrays.stream(strings)
                .mapToInt(String::length)
                .min();
        System.out.println(min);

        DoubleSummaryStatistics statistics = DoubleStream.generate(Math::random)
                .limit(1000)
                .summaryStatistics();
        System.out.println(statistics);
    }

就是這麼簡單!

基本的規約操作

基本的規約操作是利用前面講過的 reduce 方法實現的,IntStream 介面定義了三種 reduce 方法的過載形式,如下;

OptionalInt reduce(IntBinaryOperator op)

int reduce(int identity, IntBianryOperator op)

<U> U reduce(U identity,
      BiFunction<U,? super T,U> accumulator,
      BianryOperator<U> combiner)

上面的 identity 引數就是初始化值的意思,IntBianryOperator 型別的引數就是操作,例如 lambda 表示式;BianryOperator<U> combiner是一個組合器,在前面有講過。

下面我們通過一個例子來講解一下。

/**
     * @return void
     * @Author 歐陽思海
     * @Description reduce規約操作
     * @Date 22:20 2019/9/1
     * @Param []
     **/
    @Test
    public void testReduce2() {
        int sum = IntStream.range(1, 20)
                .reduce((x, y) -> x + y)
                .orElse(0);
        System.out.println(sum);

        System.out.println("##################");

        int sum2 = IntStream.range(1, 20)
                .reduce(0, (x, y) -> x + 2 * y);
        System.out.println(sum2);

        System.out.println("##################");

        int sum3 = IntStream.range(1, 20)
                .reduce(0, Integer::sum);
        System.out.println(sum3);

    }

例子中的第一個是1到20累加的操作,第二個以0為初始值,然後2倍累加,第三個是以0為初始值,累加

流的計數

流的數量統計有兩種方法,分別是 Stream.count() 方法和 Collectors.counting() 方法。

/**
     * @return void
     * @Author 歐陽思海
     * @Description 統計測試
     * @Date 23:29 2019/9/1
     * @Param []
     **/
    @Test
    public void testStatistics() {
        //統計數量
        String[] strings = {"hello", "sihai", "hello", "Java8"};
        long count = Arrays.stream(strings)
                .count();
        System.out.println(count);

        System.out.println("##################");

        Long count2 = Arrays.stream(strings)
                .collect(Collectors.counting());
        System.out.println(count2);
        
    }

4 流的查詢與匹配

流的查詢

流的查詢 Stream 介面提供了兩個方法 findFirstfindAny

findFirst 方法返回流中的第一個元素的 Optional,而 findAny 方法返回流中的某個元素的 Optional

我們來看一個例子。

        String[] strings = {"hello", "sihai", "hello", "Java8"};
        Optional<String> first = Arrays.stream(strings)
                .findFirst();
        System.out.println(first.get());

        System.out.println("##################");

        Optional<String> any = Arrays.stream(strings).findAny();
        System.out.println(any.get());

        System.out.println("##################");

流的匹配

流的匹配 Stream 介面提供了三個方法,分別是 anyMatch(任何一個元素匹配,返回 true)、allMatch(所有元素匹配,返回 true)、noneMatch(沒有一個元素匹配,返回 true)。

        boolean b = Stream.of(1, 2, 3, 4, 5, 10)
                .anyMatch(x -> x > 5);
        System.out.println(b);

        System.out.println("##################");

        boolean b2 = Stream.of(1, 2, 3, 4, 5, 10)
                .allMatch(x -> x > 5);
        System.out.println(b2);

        System.out.println("##################");

        boolean b3 = Stream.of(1, 2, 3, 4, 5, 10)
                .noneMatch(x -> x > 5);
        System.out.println(b3);

5 流的總結

這篇文章主要講解了流的一些操作,包括下面幾個方面。

  • 流的建立方法。
  • 流的系列操作,包括裝箱流、字串與流之間的轉換、流和對映 map 和 flatMap、流的連線。
  • 流的規約操作
  • 流的查詢與匹配

文章有不當之處,歡迎指正,如果喜歡微信閱讀,你也可以關注我的微信公眾號好好學java,獲取優質學習資源。

相關文章