死磕Java 8特性系列---流的深刻

本次,讀了兩本書,一本是《Beginning Java 8 Language Features》,一本是《Java 8 實戰》,有感。感受平時咱們都是使用了個Java8相關特性的皮毛。加上之前面試被人問:你知道一個列表我想同時根據不一樣字段,一次進行分組,怎麼作。我以爲有必要開一個深刻使用Java8的系列文章,來總結總結。本次主要是對流的一次性深刻。其中涉及了咱們不少沒有使用過的點。針對經常使用的,我在此就不在總結java

1、從基本的使用去理解流

這裏我先舉個咱們平常使用流的一個例子,而後根據這個例子進行幾幅圖的解說,可以徹底把流內部的原理理解清楚,不只僅侷限於表現上面的使用程序員

一、基本使用

請看代碼:面試

import java.util.ArrayList;
import java.util.List;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
public class StramMain {
    private class Dash{
        private int calories;
        private String name;
        public int getCalories() {
            return calories;
        }
        public void setCalories(int calories) {
            this.calories = calories;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    public static void main(String[] args) {
        List<Dash> menus = new ArrayList<>();
        //List<String> lowColoricDishesName = menus.parallelStream() 並行流
        List<String> lowColoricDishesName = menus.stream()
                .filter(d -> d.getCalories() < 400)//過濾低於400卡路里
                .sorted(comparing(Dash::getCalories))//根據過濾以後進行排序
                .map(Dash::getName)//對象映射成String類型
                .collect(toList());//結束操做,轉成list輸出
        System.out.println(lowColoricDishesName.toString());

    }
}

二、理解流

對於上面代碼中使用的流的過程,大體總結成下面的流程:數據庫

流執行過程1

相似於一個流水線,每經歷一個節點,都會對原有的集合數據進行一個」加工「的過程,最後加工完了再集中輸出成成品。這裏有幾個感念要理解下:json

  • filter、sorted、map這些方法(操做),叫作中間操做
  • collect這中方法(操做),叫作終端操做
  • 重要的一點:除非流程線上觸發一個終端操做,不然中間操做不會執行任何處理。因此說每一個元素都只會被遍歷一次!

整個流的過程就像一個流水線,collect就像是這個流水線的開關:咱們首先要把這個流水線要作的工序,都安排好,而後最後,咱們一開開關(collect),集合中的每個數據,挨個的一個接一個從流水線上面流過,通過一箇中間操做的節點,就會進行一個加工,最後流入一個新的集合裏面。這就是整個過程。下面是一個更細化的圖:數組

流執行過程2

這樣作的優勢是:數據結構

  • 能夠進行短路:(如上圖)在一個一個Dash通過流水線的過程當中,到了limit(3)這裏,發現,如今元素已經夠了三個了,就不會進行下面元素的遍歷了。這一點算是一種優化。這一點還能夠運用到後面的anyMatch等終端操做中
  • 只遍歷一次:上面作過介紹,看似很長很長的流寫法,其實對元素只內部遍歷一次,甚至有時候不遍歷,這個很牛逼
  • 可以作內部優化:由於內部迭代,因此看似前後書寫的流水線操做代碼,其實不是按照書寫順序進行擺放的,內部會是有最優的順序進行處理
  • 可以並行去作迭代:這也是流的一大優點,若是使用並行流,內部迭代會自動分配不一樣任務到不一樣cpu上面,這種是咱們本身寫迭代器很是困難的

2、一些「風騷」的中間操做

除開咱們經常使用的一些中間操做:app

  • filter
  • map
  • limit

找一些平時想不太到的中間操做講講dom

一、flatMap:「拍扁」操做

傳統操做將一個字符串數組中的全部字符以一個List輸出,不能有重複,例如:ide

String[] words = {"jicheng","gufali"}

變成:List<String> = {"j","i","c","h","e","n","g","u","f","a","l"}

《Java 8 實戰》裏面嘗試了兩種方式,我以爲,頗有助於咱們思考這個拍扁操做的原理

第一種嘗試

public class StramMain {
    public static void main(String[] args) {
        String[] words = {"jicheng", "gufali"};
        List<String[]> list = Arrays.stream(words)
            .map(value -> value.split(""))
            .distinct()
            .collect(toList());
        System.out.println(list);
    }
}

結果以下圖:

拍扁 操做的結果圖1

其實map中間操做裏面把源變成了兩個Stream<String[]>這種類型,最後輸出成list的時候,就成了List<String[]>,顯然和咱們想要的十萬八千里

第二種嘗試

代碼以下:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

public class StramMain {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("jicheng", "gufali");
        List<Stream<String>> collect = words.stream()
                .map(value -> value.split(""))
                .map(Arrays::stream)
                .collect(toList());
        System.out.println(collect);
    }
}
  • 第一個map:將原始的流轉成了Stream<String[]>類型
  • 第二個map:分別將原String數組合併成了兩個Stream<String>這樣一個Stream流
  • 最後:輸出的就是List<Stream<String>>類型

顯然,也不是咱們要的

最終形態

代碼以下,利用了上面的合併數組爲一個流的操做public static <T> Stream<T> stream(T[] array)

import static java.util.stream.Collectors.toList;

public class StramMain {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("jicheng", "gufali");
        List<String> collect = words.stream()
                .map(value -> value.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(toList());
        System.out.println(collect);
        // result:[j, i, c, h, e, n, g, u, f, a, l]
    }
}

下面是過程的流程圖:

流中間操做flapMap

二、findFirst/findAny:查找

使用代碼:

Optional<Dish> dish =menu.stream()
    .filter(Dish::isVegetarian)
    .findAny();
boolean isPresent = dish.isPresent();

查到一頓流操做以後的其中一個或者任意一個。幾點值得注意:

  • 返回的是一個Optional,接下來能夠作幾個處理:
    • 直接使用isPresent方法,寫一個if邏輯判斷
    • 或者直接在流的後面接ifPresent(Consumer<T> block),若是值存在的話,會執行block
  • findAny和findFirst的區別在於,findFirst返回集合中的第一個,findAny返回任意一個,對於使用並行流的時候,findFirst很是很差優化,有可能仍是使用findAny

三、reduce:歸約

這東西,相似於把集合裏面的全部元素進行一個大彙總(求和、最大最小值、平均值等),下面是源碼中的reduce方法:

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

a、詳細解說一個的過程

代碼以下:

public class StramMain {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);
        System.out.println(sum);
        // result:55
    }
}

解說:首先,0做爲Lambda(a)的 第一個參數,從流中得到1做爲第二個參數(b)。0 + 1獲得1,它成了新的累積值。而後再用累 積值和流中下一個元素2調用Lambda,產生新的累積值3。接下來,再用累積值和下一個元素3 調用Lambda,獲得6。以此類推,獲得最終結果21。

b、沒有初始值的版本

public class StramMain {
    public static void main(String[] args) {
		List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Optional<Integer> reduce = numbers.stream().reduce((a, b) -> a + b);
        System.out.println(reduce.get());
        // result:55
    }
}

結果是同樣的,表示:若是沒有初始值,流操做會取第一個數組的值,做爲初始值,因爲不肯定列表是否是有值的,若是沒值,第一個數值就去不到,那求和就不成功,就沒有值。因此返回一個Optional的對象

c、最大最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

一樣的,這個一樣也能夠有個初試的值

public class StramMain {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Integer max = numbers.stream().reduce(Integer.MIN_VALUE, Integer::max);
        Integer min = numbers.stream().reduce(Integer.MAX_VALUE, Integer::min);
        System.out.println("max number:"+max+",min number:"+min);
        // result:max number:10,min number:1
    }
}

四、IntStream/LongStream:數值流

Java 8引入了三個原始類型: IntStream 、 DoubleStream 和LongStream,分別將流中的元素特化爲int、long和double,從而避免了暗含的裝箱成本。每 個接口都帶來了進行經常使用數值歸約的新方法,好比對數值流求和的sum,找到最大元素的max。 此外還有在必要時再把它們轉換回對象流的方法。要記住的是,這些特化的緣由並不在於流的複雜性,而是裝箱形成的複雜性——即相似int和Integer之間的效率差別。

a、映射到數值流

public class StramMain {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1,2,3,4,5);
        int sum = numbers.stream()
            .mapToInt(value -> value)
            .sum();
        System.out.println(sum);
        // result:15
    }
}

b、轉換回去

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

c、最大最小值

public class StramMain {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1,2,3,4,5);
        OptionalInt max = numbers.stream()
                .mapToInt(value -> value)
                .max();
        OptionalInt min = numbers.stream()
                .mapToInt(value -> value)
                .min();
        int maxValue = max.orElse(Integer.MAX_VALUE);//若是沒有最大值默認給一個最大值
        int minValue = min.orElse(Integer.MIN_VALUE);//若是沒有最小值默認給一個最小值
    }
}

d、生成範圍值

Java 8引入了兩個能夠用於IntStream和LongStream的靜態方法,幫助生成這種範圍: range和rangeClosed。這兩個方法都是第一個參數接受起始值,第二個參數接受結束值。但 range是不包含結束值的,而rangeClosed則包含結束值:

IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0);//偶數流
System.out.println(evenNumbers.count());

c、一個風騷的操做:求勾股數

public class StramMain {
    public static void main(String[] args) {

        Stream<double[]> pythagoreanTriples = IntStream.rangeClosed(1, 100)
                .boxed()
                .flatMap(a -> IntStream.rangeClosed(a, 100)
                        .mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)})
                        .filter(t -> t[2] % 1 == 0));
        pythagoreanTriples.limit(3).forEach(value->{
            System.out.println(value[0]+","+value[1]+","+value[2]);
        });
        /**
         * 結果:
         * 3.0,4.0,5.0
         * 5.0,12.0,13.0
         * 6.0,8.0,10.0
         */
    }
}

五、Stream.iterate/Stream.generate:無限流

Stream API提供了兩個靜態方法來從函數生成流:Stream.iterate和Stream.generate。 這兩個操做能夠建立所謂的無限流:不像從固定集合建立的流那樣有固定大小的流。

public class StramMain {
    public static void main(String[] args) {

        Stream.iterate(0, n -> n + 2)
                .limit(10)//註釋掉這一行,就會無限循環的生成下去
                .forEach(System.out::println);
    }
}

解釋:流的第一個元素是初始值0。而後加 上2來生成新的值2,再加上2來獲得新的值4,以此類推。這種iterate操做基本上是順序的, 由於結果取決於前一次應用。請注意,此操做將生成一個無限流——這個流沒有結尾,由於值是 按需計算的,能夠永遠計算下去。

下面的是generate的無限流:

public class StramMain {
    public static void main(String[] args) {

        Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);
    }
}

3、其實你不知道的終端操做

細細讀了《Java8 實戰》,發現其實終端操做纔是真正的大殺器!哪怕是一些中間操做的功能,再終端操做也是能夠完成的。包括裏面的不少設計理念,更是錯中複雜。我這回集中講講下面的幾個點:

  • 在終端操做也能完成的操做:彙總與規約
  • 分組,分組在分組,分組再分組再分組。。。。。
  • List<Object>變換成Map<Object,Object>,經常使用操做

一、在終端操做也能完成的操做:彙總與規約

其實在中間操做中,同樣能夠完成此操做

a、基本示例

下面是一系列歸約彙總的代碼示例片斷,其實不難:

import com.alibaba.fastjson.JSON;

import java.util.Arrays;
import java.util.Comparator;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.function.Function;

import static java.util.stream.Collectors.*;

public class StramMain {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(12, 23, 34, 54);
        //計算一共有多少個值
        Long collect = intList.stream().collect(counting());
        //同上
        Long sameWithCollect = intList.stream().count();
        System.out.println("一共有多少個數字:" + collect);
        //查找最大值
        intList.stream()
                .collect(maxBy(Comparator.comparing(Function.identity())))
                .ifPresent(integer -> {
                    System.out.println("數字中的最大值:" + integer);
                });

        Integer integer = intList.stream()
                .collect(summingInt(value -> value.intValue()));
        System.out.println("全部數字的和是:"+integer);
        Double averageNumber = intList.stream()
                .collect(averagingInt(value -> value.intValue()));
        System.out.println("平均數是:"+averageNumber);
        IntSummaryStatistics intSummaryStatistics = intList.stream()
                .collect(summarizingInt(value -> value.intValue()));
        System.out.println("全部的歸約彙總的結果對象是:"
                + JSON.toJSONString(intSummaryStatistics));
        /**
         * result:
         * 一共有多少個數字:4
         * 數字中的最大值:54
         * 全部數字的和是:123
         * 平均數是:30.75
         * 全部的歸約彙總的結果對象是:{"average":30.75,"count":4,"max":54,"min":12,"sum":123}
         */

    }
}

b、鏈接字符串

joining()工廠方法返回的收集器會把對流中每個對象應用toString方法獲得的全部字符 串鏈接成一個字符串。另外,joining在內部使用了StringBuilder來把生成的字符串逐個追加起來。

import static java.util.stream.Collectors.*;
public class StramMain {
    public static void main(String[] args) {
		List<Integer> intList = Arrays.asList(12, 23, 34, 54);
        String stringJoin = intList.stream()
                .map(value -> value.toString())
                .collect(joining(","));
        System.out.println(stringJoin);
        // result: 12,23,34,54
    }
}

c、廣義的歸約彙總

上面兩小節的歸約操做,其實都是基於一個底層的操做進行的,這個底層的歸約操做就是:reducing(),能夠說上面全部的歸約操做都是當前reducing操做的特殊化,僅僅是方便程序員罷了。固然,方便程序員但是頭等大事兒。說白了,特殊化的歸約,是便於閱讀與書寫的一種模板。

import java.util.Arrays;
import java.util.List;
import static java.util.stream.Collectors.reducing;
public class StramMain {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(12, 23, 34, 54);
        Integer sumNumber = intList.stream()
            	//注意這裏的reducing方法
                .collect(reducing(0, value -> value.intValue(), Integer::sum));
        System.out.println("求和:"+sumNumber);
        // result: 123
    }
}

三個參數的意義:

  • 第一個參數是歸約操做的起始值,也是流中沒有元素時的返回值,因此很顯然對於數值和而言0是一個合適的值。
  • 第二個參數是Function函數式接口,用於定位咱們要返回的具體值類型
  • 第三個參數是一個BinaryOperator,將兩個項目累積成一個同類型的值,就是歸約過程執行函數

二、分組,分組在分組,分組再分組再分組。。。。。

這個話題,是我曾經的一次面試中經歷過的問題:咱們如何實現首先經過一個字段分組以後,在經過另一個字段進行再次的分組呢?當時本身只常常操做一個字段分組的樣子,並無繼續的研究如何經過一個以上字段進行連續分組。因此最終答得也不是很好。其實就是想用流這東西作到相似於數據庫裏面:group by col1,col2,這種操做。最終的結果數據結構,大致上是:Map<K,Map<T,List<O>>>。要實現很簡單,咱們從的源碼中進行分析:

public final class Collectors {
    ...
        
    //①
	public static <T, K> Collector<T, ?, Map<K, List<T>>>
    	groupingBy(Function<? super T, ? extends K> classifier) {
		...
    }
    
    //②
    public static <T, K, A, D>
    	Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        ...
    }

    
    ...
}
  • 三個方法都返回同一個類型Collector
  • ①方法是咱們最常使用,最終使用collect方法可以返回Map<K,List<O>>類型
  • 其中②方法就是實現多級分組的,可見第二個參數是一個Collector類型,咱們能夠再第二個參數地方調用①方法,如此遞進下去
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream()
                .collect(groupingBy(Dish::getType, groupingBy(dish -> {
                    // 這裏進行二次分組的實現函數
                    if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                    else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                    else return CaloricLevel.FAT; 
                })));

三、toMap的操做

這個操做也是很經常使用的,而且常常被咱們忽視的方法。若是一個List是咱們從數據庫裏面查出來的對象,裏面有id和其餘的值,咱們每每想快速經過id定位到一個具體的對象,那就須要將這個List裝換成一個以id爲key的map。以往咱們居然本身手寫map,有了toMap操做,簡直不能再簡單了!咱們來看看源碼中的toMap的幾種重載:

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);
}//①

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

public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                             Function<? super T, ? extends U> valueMapper,
                             BinaryOperator<U> mergeFunction,
                             Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator
        = (map, element) -> map.merge(keyMapper.apply(element),
                                      valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}//③

咱們發現全部方法其實底層都死調用了③這個方法的。先解說下如何使用:

  • ①方法可以直接將List映射成一個Map,第一個參數是key,第二個參數是value,key值重複會拋出IllegalStateException異常
  • ②方法的第三個參數是避免若是出現了key值重複,如何選擇的問題
  • ③方法的第四個參數是決定具體返回是一個什麼類型的map
Map<Integer,Person> idToPerson = persons.stream()
    .collect(Collectors.toMap(Person::getId,Funtion.identity()));

Map<Integer,Person> idToPerson = persons.stream()
    .collect(Collectors.toMap(Person::getId
    						,Funtion.identity()
                            ,(existValue,newValue->existValue)));


TreeMap<Integer,Person> idToPerson = persons.stream()
    .collect(Collectors.toMap(Person::getId
    						,Funtion.identity()
                            ,(existValue,newValue->existValue)
                            ,TreeMap::new));
相關文章
相關標籤/搜索