代碼簡潔之路——還在使用fori循環麼?嘗試用Java 的Stream來處理集合吧

新的循環

上一篇介紹過函數式接口(@FunctionalInterface)和Lambda表達式。而伴隨他們出現Java 8提供了一個強大的工具來實現對集合的操作——流(Stream)。配合流我們以前需要很多行代碼才能完成的集合操作現在只需要三五行甚至一行就能完成。

流 stream

關於流JAVA提供了兩種創建方式使用stream()創建串行流和使用parallelStream()創建並行流。這裏都是基於串行流的內容了。關於並行流對於有些功能需要額外操作這裏就沒時間介紹了。

就像之前介紹方法引用,既然可以將方法作爲參數進行傳遞,在相同的方法因爲我們不同的方法邏輯能實現更加複雜的業務邏輯,這裏我也只介紹了我自己使用過的簡單的邏輯。假如之前瞭解過函數式接口(@FunctionalInterface)的使用會很輕鬆的發現不同參數的使用方式,所以這裏希望大家都能去了解學習下函數式接口的用法,不要拘泥於固定的使用方式。

什麼是流

流(Stream)是Java 8 API添加了一個新的抽象。Stream提供的大量對集合操作的支持,尤其是配合函數式接口和Lambda表達式,將之前繁瑣的循環操作進行了壓縮。讓循環變得乾淨和簡潔。

流如何處理數據

使用流處理數據的時候將處理的過程看成是集合中的數據在管道中流動的操作,通過不一樣的管道對上一級傳遞過來的數據進行不同的處理。

整個元素在管道中需要經過中間操作(intermediate operation)的處理然後由最終操作(terminal operation)輸出結果。

經過中間操作(intermediate operation)的處理,最後由最終操作(terminal operation)得到前面處理的結果。

graph LR
集合==>流

subgraph 管道

流 --> filter

filter --> sorted

sorted --> collect
end
collect==>輸出結果

通過流對集合中元素據的篩選操作: stream().filter

filter 方法用於通過設置的條件過濾出元素。

此方法需要提供一個實現了Predicate接口的邏輯,在此邏輯中執行過濾業務

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

下面代碼可以過濾出name=test的數據。最終返回一個符合設置的過濾規則的集合。

    public static List<DataBean> filter(List<DataBean> list) {
        List<DataBean> beanList =
            list.stream().filter(item -> "test".equals(item.getName())).collect(Collectors.toList());
        return beanList;
    }

使用流獲取集合中指定部分的數據: stream().skip和stream().limit

skip和limit的配合可以實現類似分頁操作中獲取指定頁碼內數據方式,skip是將指定索引位置之後的數據加入流中,limit是將指定條目數的數據加入流中。當然兩個參數也可以單獨使用實現自己的業務邏輯

這兩個方法都只需要設置一個數字即可實現邏輯

    Stream<T> skip(long n);
    
    Stream<T> limit(long maxSize)

下面的代碼可以獲取索引2(索引從0開始)開始的兩個元素

    public static List<DataBean> skipAndLimit(List<DataBean> list) {
        List<DataBean> beanList =
            list.stream().skip(2).limit(2).collect(Collectors.toList());
        return beanList;
    }

通過流獲取集合中的極值:stream().max 和 stream().min

max 和 min 根據指定的規則獲取元素中最大或者最小的元素

max 和 min方法都需要提供Comparator接口的實現,用來判斷相關大小的邏輯,同時需要注意此接口返回的不再是Stream對象

Optional<T> min(Comparator<? super T> comparator);

Optional<T> max(Comparator<? super T> comparator);

下面代碼實現了根據getAge()方法的結果,來獲取值最大的元素

    public static DataBean max(List<DataBean> list) {
        DataBean dataBean = list.stream().max(Comparator.comparingInt(DataBean::getAge)).get();
        return dataBean;
    }

下面代碼實現了根據getAge()方法的結果,來獲取值最小的元素

    public static DataBean min(List<DataBean> list) {
        DataBean dataBean = list.stream().min(Comparator.comparingInt(DataBean::getAge)).get();
        return dataBean;
    }

通過流獲取集合中元素進行轉換

stream().map

map實現的是數據的映射,此方法允許我們將集合中元素根據自定義的邏輯映射到我們希望的類型中。使用此方法我們可以很簡單的將元素中數據提取出來,或者轉換爲另外一種類型進行輸出。

map方法需要提供一個進行數據映射的Function接口實現

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

下面代碼會獲取集合中對象的getName()方法返回值的集合

    public static List<String> map(List<DataBean> list) {
        List<String> strings = list.stream().map(item -> item.getName()).collect(Collectors.toList());
        return strings;
    }

stream().toArray

toArray可以將流中數據很簡單的轉換爲數組

toArray提供了兩個方法,而其中無參的方法再其實現類中代碼爲toArray(Object[]::new)最終返回的時候object的數組。

    Object[] toArray();

    <A> A[] toArray(IntFunction<A[]> generator);
    public static DataBean[] toArray(List<DataBean> list) {
        DataBean[] objects = list.stream().toArray(DataBean[]::new);
        return objects;
    }

通過流對集合中元素進行排序:stream().sorted

sorted方法使流可以根據指定的規則對數據進行排序

sorted同樣提供了兩個方法

    Stream<T> sorted();

    Stream<T> sorted(Comparator<? super T> comparator);

而無參的排序方法最終使用下面的排序邏輯

// 返回按照大小寫字母排序的Comparator
Comparator<? super T> comp = (Comparator<? super T>) Comparator.naturalOrder()

下面的代碼會將數據根據getType()方法返回值大小進行排序

    public static List<DataBean> sorted(List<DataBean> list) {
        List<DataBean> beanList =
            list.stream().sorted(Comparator.comparingInt(DataBean::getType)).collect(Collectors.toList());
        return beanList;
    }

ps.對於排序規則可以使用方法引用這種變得更加簡潔,當然對於一些很少使用此特性的人使用起來可能會有些難以識別,使用下面也是可以的

        List<DataBean> beanList =
            list.stream().sorted((o1,o2) -> {return o1.getType() - o2.getType();}).collect(Collectors.toList());

通過流對集合中元素進行修改: stream().peek

很類似map的循環不過不同之處,map操作一般會希望修改流中元素的類型,而peek一般只是希望修改流中元素的內容而不嘗試修改其類型。

peek方法需要提供一個Consumer接口實現來進行類操作邏輯

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

下面的代碼使用流中元素的setName()方法,參數爲peek,來對元素進行修改

    public static List<DataBean> peek(List<DataBean> list) {
        List<DataBean> beanList =
            list.stream().peek(item -> item.setName("peek")).collect(Collectors.toList());
        return beanList;
    }

使用流對集合數據進行去重: stream().distinct

distinct 用來對流中的數據進行去重,但是需要注意的是distinct並沒有提供支持自定義邏輯的參數,其默認使用hashcode和equals進行判斷

distinct並未支持去重邏輯的自定義,其默認使用hashcode和equals進行判斷

Stream<T> distinct();

下面的代碼直接去除流中重複數據

    public static List<DataBean> distinct(List<DataBean> list) {
        List<DataBean> beanList =
            list.stream().distinct().collect(Collectors.toList());
        return beanList;
    }

使用流對集合數據進行檢查: stream().match

*match提供了三個方法,分別是用來檢測流中是否全部存在、部分存在、不存在指定規則的數據。

此三個接口都需要提供匹配規則進行判斷

    boolean anyMatch(Predicate<? super T> predicate);
    boolean allMatch(Predicate<? super T> predicate);
    boolean noneMatch(Predicate<? super T> predicate);

需要注意:stream().allMatch(…) 對於空集合,不管判斷條件是什麼,直接返回 true。

下面代碼用來判斷流中元素屬性和test的匹配情況:allMatch完全匹配,anyMatch部分匹配,noneMatch完全不匹配

    public static boolean match(List<DataBean> list) {
        boolean allMatch = list.stream().allMatch(item -> "test".equals(item.getName()));
        boolean anyMatch = list.stream().anyMatch(item -> "test".equals(item.getName()));
        boolean noneMatch = list.stream().noneMatch(item -> "test".equals(item.getName()));
        return allMatch;
    }

使用流對集合數據進行歸一操作: stream().reduce

reduce 方法允許開發者將集合中的數據通過設置的規則,最終返回唯一的結果。其使用的BinaryOperator接口作爲參數,這樣我們可以使用的自定義規則可以是相加、相見、最大、最小等操作

reduce是一個複雜的接口,每種參數都可以實現不同的歸一操作,這裏我只介紹了自己使用過的,其他歸一操作,大家可以後面自己嘗試下。

    T reduce(T identity, BinaryOperator<T> accumulator);
    
    Optional<T> reduce(BinaryOperator<T> accumulator);
    
    <U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

下面代碼中將集合中元素的getAge()方法的返回值聚合起來,然後設置到外部創建的dataBean對象中

    public static DataBean reduceDataBean(List<DataBean> list) {
        DataBean dataBean = new DataBean();
        dataBean.setName("聚合結果");
        dataBean =
            list.stream().reduce(dataBean, (o1, o2) -> {
                o1.setAge(o1.getAge() + o2.getAge());
                return o1;
            });
        return dataBean;
    }

使用流的循環

stream()提供的循環,這兩個和單獨使用forEach和Iterator。在使用串行流的時候,他們在使用方面上其實差別不大。

stream().forEach

循環

    public static List<DataBean> forEach(List<DataBean> list) {
        list.stream().forEach(item -> item.setName("forEach"));
        return list;
    }

stream().iterator

迭代器

    public static void iterator(List<Integer> list) {
        Iterator<Integer> iterator = list.stream().iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
        }
    }

collectiors

Stream中的數據最終要生成我們使用的集合數據,所以在Stream提供了collect終止操作將流的數據進行輸出

<R, A> R collect(Collector<? super T, A, R> collector)

其方法最終使用Collector進行數據輸出,而collectiors就是Collector的實現,其提供了一系列功能來輸出最終結果。

使用collectiors獲取集合中的極值

Collectors.maxBy

Collectors.maxBy 可以獲取指定規則下集合最大結果的元素

根據元素的getType()的結果,獲取其最大元素

    public static DataBean maxBy(List<DataBean> list) {
        /*DataBean dataBean =
            list.stream().collect(Collectors.maxBy(((Comparator.comparingInt(DataBean::getType))))).get();*/
        DataBean dataBean = list.stream().collect(Collectors.maxBy((((o1, o2) -> o1.getType() - o2.getType())))).get();
        return dataBean;
    }

Collectors.minBy

Collectors.maxBy 可以獲取指定規則下集合最小結果的元素

下面的代碼根據元素的getType()的結果,獲取其最小元素

   public static DataBean minBy(List<DataBean> list) {
        /*DataBean dataBean =
            list.stream().collect(Collectors.minBy(((Comparator.comparingInt(DataBean::getType))))).get();*/
        DataBean dataBean = list.stream().collect(Collectors.minBy((((o1, o2) -> o1.getType() - o2.getType())))).get();
        return dataBean;
    }

使用collectiors對集合中的數據進行分組聚合操作

Collectors.groupBy

Collectors.groupingBy 方法可以將流中的數據進行分組,根據開發者提供的分組規則得到的結果對數據進行分組。最終得到key爲分組規則的值、value爲符合此值的元素的集合的一個Map結果

Collectors.groupBy提供了多個方法,其基礎方法需要提供一個進行排序的Function接口實現,其他則是對輸出內容的處理

    public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier) {
        return groupingBy(classifier, toList());
    }
    
    public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        return groupingBy(classifier, HashMap::new, downstream);
    }
    
    public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) {

下面的代碼將流中數據根據getType()方法的結果進行分組然後獲得結果

    public static Map<Integer, List<DataBean>> groupBy(List<DataBean> list) {
        Map<Integer, List<DataBean>> listMap =
            list.stream().collect(Collectors.groupingBy(DataBean::getType));
        return listMap;
    }

Collectors.partitioningBy

partitioningBy同樣提供了多個方法,正常我們使用的時候一般只使用下面的內容,此時需要傳遞Predicate接口是實現,返回一個boolean類型。

    public static <T>
    Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
        return partitioningBy(predicate, toList());
    }

partitioningBy也會將集合進行分組,但是此方法會根據規則是否匹配將數據切分爲兩部分 key爲false和key爲true兩部分

下面代碼將集合中的元素根據"test".equals(item.getName()) 規則劃分爲兩部分然後返回對應Map

    public static Map<Boolean, List<DataBean>> partitioningBy(List<DataBean> list) {
        Map<Boolean, List<DataBean>> booleanListMap =
            list.stream().collect(Collectors.partitioningBy(item -> "test".equals(item.getName())));
        return booleanListMap;
    }

Collectors.counting

Collectors.counting 用來計算集合中元素的數量,其單獨使用collect(Collectors.counting())可以獲取集合中元素的數量。配合groupingBy我們可以實現對分組中每組數據進行統計

下面的代碼將流中數據根據getType()方法的結果進行分組然後使用Collectors.counting()獲得每個分組內元素數量

    public static Map<Integer, Long> groupByCount(List<DataBean> list) {
        Map<Integer, Long> collect =
            list.stream().collect(Collectors.groupingBy(DataBean::getType, Collectors.counting()));
        return collect;
    }

Collectors.summingInt

Collectors.summingInt 可以將集合中的元素根據規則計算的結果進行求和,其單獨使用collect(Collectors.summingInt())可以獲取集合中元素的計算結果的和。配合groupingBy我們可以實現對分組中每組數據進行求和

下面的代碼將流中數據根據getType()方法的結果進行分組然後使用Collectors.summingInt()將每個元素getAge()方法的返回結果進行相加

    public static Map<Integer, Integer> summingInt(List<DataBean> list) {
        Map<Integer, Integer> map =
            list.stream().collect(Collectors.groupingBy(DataBean::getType, Collectors.summingInt(DataBean::getAge)));
        return map;
    }

Collectors.averagingInt

Collectors.averagingInt 可以將集合中的元素根據規則計算的結果進行求平均,配合Collectors.groupingBy的嵌套使用,我們可以得到每個元素中指定規則下計算出來結果的平均值。

下面的代碼將流中數據根據getType()方法的結果進行分組然後使用Collectors.summingInt()將每個元素getAge()方法的返回結果進行求平均

    public static Map<Integer, Double> averagingDouble(List<DataBean> list) {
        Map<Integer, Double> map =
            list.stream().collect(Collectors.groupingBy(DataBean::getType, Collectors.averagingInt(DataBean::getType)));
        return map;
    }

使用collectiors對集合進行歸一操作

數據歸一:Collectors.reducing

類似stream().reduce方法,Collectors.reducing實現將數據進行歸一操作。

reducing提供了三種方法,這裏只介紹下第三個方法,其接受三個參數,第一個參數爲最終返回的對象,第二個參數爲數據的映射處理,此步驟可以將原始數據轉換爲需要處理的目標類型,然後最後一個參數來確定被轉換後的數據被處理的方式


    public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op) {
        ...
    }
    
    public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op) {
            ...
    }

    public static <T, U> Collector<T, ?, U> reducing(U identity,Function<? super T, ? extends U> mapper,
    BinaryOperator<U> op){
        ...
    }

下面的代碼將原始集合根據item.getType()進行分組,然後在reducing中調用元素的getSome方法返回一個數字,然後將數字求和,最終返回每個分組各個元素的getSome方法返回值的和

    public static Map<Integer, Integer> reducing(List<DataBean> list) {
        Map<Integer, Integer> collect = list.stream().collect(
            Collectors.groupingBy(item -> item.getType(),
            Collectors.reducing(0, DataBean::getSome, Integer::sum)
            ));
        return collect;
    }

字符串拼接:Collectors.joining

Collectors.joining 可以將集合中每個元素根據規則返回的結果進行字符串拼接

下面代碼根據元素getName()方法返回的結果進行字符串拼接,並返回結果

    public static String join(List<DataBean> list) {
        String collect = list.stream().map(item -> item.getName())
            .collect(Collectors.joining(",", "", ""));
        return collect;
    }

使用collectiors獲取集合中元素元素進行轉換:Collectors.mapping

此方法類似於Map操作,將集合中元素,根據指定規則轉換爲需要的數據類型進行輸出

下面代碼獲取集合元素的getName()方法的結果,然後收集爲List返回

    public static List<String> mapping(List<DataBean> list) {
        List<String> collect =
            list.stream().collect(Collectors.mapping(item -> item.getName(), Collectors.toList()));
        return collect;
    }

Collectors.collectingAndThen

Collectors.collectingAndThen 允許在對流中數據根據規則進行收集之後,再次進行處理,先用map提取出集合中元素getAge()方法的返回結果,

下面代碼使用Collectors.collectingAndThen,首先獲取其最大結果,然後拼裝結果語句

    public static String collectingAndThen(List<DataBean> list) {
        String str = list.stream().map(DataBean::getAge)
            .collect(Collectors.collectingAndThen(
                Collectors.maxBy((Comparator.comparingInt(item -> item))),
                item -> "最大的數字是:" + item.get())
            );
        return str;
    }

結果輸出

Collectors.toList和Collectors.toSet

Collectors.toList 將流中數據輸出到一個List中,默認使用ArrayList。如果需要更多地控制返回的List,請使用toCollection(Supplier)

    public static List<DataBean> toList(List<DataBean> list) {
        return list.stream().collect(Collectors.toList());
    }

Collectors.toMap

Collectors.toMap需要傳遞兩個接口實現,分別是key創建的邏輯以及value創建的邏輯

toMap接收2個參數一個是來確定key生成的規則一個是用來確定value生成的規則

   public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
        return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
    }

下面代碼輸出了一個key爲getName輸出值,value爲數據本地的map

    public static Map<String, DataBean> toMap(List<DataBean> list) {
        Map<String, DataBean> collect = list.stream().collect(Collectors.toMap(o -> o.getName(), o -> o));
        return collect;
    }

後話

對於一個已經發布五六年的版本,今天再去整理這些內容顯得有些晚了。Stream對集合的支持雖然已經存在很久。但是在我實際開發的感受即使已經發布了很久但是很多人這些新的特性尤其是Lambda表達式都敬而遠之。可能很多人都擔心代碼越簡潔,理解起來越困難的問題。但是我個人的體會,一個新功能你不去嘗試它,那它永遠都很難理解。而我平時開發的感受是:代碼越多、問題越多。官方既然提供了這麼好的工具,使用者就應該好好利用。在我看來代碼要想做的簡潔那就需要讓每一行代碼都更加有價值。


個人水平有限,上面的內容可能存在沒有描述清楚或者錯誤的地方,假如開發同學發現了,請及時告知,我會第一時間修改相關內容。假如我的這篇內容對你有任何幫助的話,麻煩給我點一個贊。你的點贊就是我前進的動力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章