java8 Stream常用方法和特性淺析


前言:對大數據量的集合的循環處理,stream擁有極大的優勢,完全可以用stream去代替for循環。

Stream介紹

  先說下Stream的優勢:它是java對集合操作的優化,相較於迭代器,使用Stream的速度非常快,並且它支持並行方式處理集合中的數據,默認情況能充分利用cpu的資源。同時支持函數式編程,代碼非常簡潔。

  Stream是一種用來計算數據的流,它本身並沒有存儲數據。你可以認為它是對數據源的一個映射或者視圖。

  它的工作流程是:獲取數據源->進行一次或多次邏輯轉換操作->進行歸約操作形成新的流(最后可以將流轉換成集合)。

1.生成流

Stream的創建需要一個數據源(通常是一個容器或者數組):

例1:Stream<String> stream = Stream.of("I", "got", "you", "too");

例2:String [] strArray = new String[] {"a", "b", "c"};

  stream = Arrays.stream(strArray);

例3:List<String> list = Arrays.asList(strArray);

  stream = list.stream();

2.流的操作

流的操作類型分2種:中間操作與聚合操作

2.1中間操作(intermediate ):

中間操作就是對容器的處理過程,包括:排序(sorted...),篩選(filter,limit,distinct...),映射(map,flatMap...)等

 

2.1.1 排序操作(sorted):(參考:https://www.cnblogs.com/a-du/p/8289537.html

sorted提供了2個接口:

1、 sorted() 默認使用自然序排序, 其中的元素必須實現 Comparable 接口 。
2、 sorted(Comparator<? super T> comparator) :我們可以使用lambada 來創建一個 Comparator 實例。可以按照升序或着降序來排序元素。 
注意sorted是一個有狀態的中間操作,即,只有全部執行完所有的數據才能知道結果。

比如:將一些字符串在地址中按出現的順序排列:

          String address = "中山北路南京大學仙林校區";
		List<String> aList = new ArrayList<>();
		aList.add("南京");
		aList.add("大學");
		aList.add("仙林校區");
		aList.add("仙林大學城");
		aList.add("中山北路");
		aList.stream().sorted(
			Comparator.comparing(a->address.indexOf(a))
          ).forEach(System.out :: println);

也可以像下面這樣不使用比較器:

		aList.stream().sorted(
				(a,b)->address.IndexOf(a)-address.IndexOf(b)
		).forEach(System.out :: println);//由大到小排序

輸出結果:

注:1.這里仙林大學城這個字段沒有出現,所以序號是-1,被排在最前面。

  2.Comparator.comparing();這個是比較器提供的一個方法,它返回的也是一個比較器,源碼如下:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

2.1.2 篩選操作(filter)

上一步中,我們把一些字符串,按照在地址中出現的順序排序。

接下來我們可能想要進行篩選,把不在地址中,但是indexof為“-1”,排在最前面的數據篩選掉:

filter可以對集合進行篩選,它的參數可以是一個lambda表達式,流中的數據將會通過該lambda表達式返回新的流。

這里Stream有一個特性很重要,它像一個管道,可以將多個操作連接起來,並只執行一次for循環,這樣大大提高了效率,即使第二次的流操作需要第一次流操作的結果,時間復雜度也只有一個for循環:

於是我可以在前面加個filter(),這樣把“-1”過濾掉:

String address = "中山北路南京大學仙林校區";
List<String> aList = new ArrayList<>();
aList.add("南京");
aList.add("大學");
aList.add("仙林校區");
aList.add("仙林大學城");
aList.add("中山北路");
aList.stream().filter(a->address.indexOf(a)!=-1)
.sorted(
  Comparator.comparing(a->address.indexOf(a))
).forEach(System.out :: println);

輸出結果:

注:foreach是一個終端操作,參數也是一個函數,它會迭代中間操作完成后的每一個數據,這里它將每個不為空的元素打印出來。

其它的過濾操作還包括:

limit(long maxSize):獲得指定數量的流。

distinct():通過hashCode和equals去除重復元素。

 

2.1.3 映射操作(map)

映射操作,就像一個管道,可以將流中的元素通過一個函數進行映射,返回一個新的元素。

這樣遍歷映射,最終返回一個新的容器,注意:這里返回的新容器數據類型可以不與原容器類型相同:

舉個例子:我們將address中每個元素的位置找出,並返回一個int類型的存儲位置信息的數組:

 @Test public void test() { String address = "中山北路南京大學仙林校區"; List<String> aList = new ArrayList<>(); aList.add("南京"); aList.add("大學"); aList.add("仙林校區"); aList.add("仙林大學城"); aList.add("中山北路"); List<Integer> aIntegers =aList.stream() .map(str->mapFunc(address, str)).collect(Collectors.toList()); System.out.println(aIntegers);//.forEach(System.out :: println); 
 } private int mapFunc(String address,String str) { return address.indexOf(str); }

結果如下:

 

2.2規約操作(reduction ):

之前的中間操作只是對流中數據的處理,最終我們還是要將它們整合輸出為一個結果,比如,返回一個最大值,返回一個新的數組,或者將所有元素進行分組等,這就是規約(末端)操作的作用。

我們常用的末端操作函數有Reduce()和collect();

2.2.1Reduce

reduce就是減少的意思,它會將集合中的所有值根據規則計算,最后只返回一個結果。

它有三個變種,輸入參數分別是一個參數、二個參數以及三個參數;


1.一個參數的Reduce

它的參數就是一個函數接口:Optional<T> reduce(BinaryOperator<T> accumulator)

比如,我們找出數組中長度最大的一個數:

public void test() {
        String address = "中山北路南京大學仙林校區";
        List<String> aList = new ArrayList<>();
        aList.add("南京");
        aList.add("大學");
        aList.add("仙林校區");
        aList.add("仙林大學城");
        aList.add("中山北路");
        Optional<String> a =aList.stream()
                .reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
        System.out.println(a.get());//仙林大學城
    }

這里的Optional<T>就是一個容器,它可以避免空指針,具體可以百度,這里也可以返回一個String的。

 

2.兩個參數的Reduce

T reduce(T identity, BinaryOperator<T> accumulator)

2個參數其實除了一個函數接口以外,還包括一個固定的初始化的值,它會作為容器的第一個元素進入計算過程:

例:將每個字符串拼接,並在之前加上“value:”:

public void test() {
        String address = "中山北路南京大學仙林校區";
        List<String> aList = new ArrayList<>();
        aList.add("南京");
        aList.add("大學");
        aList.add("仙林校區");
        aList.add("仙林大學城");
        aList.add("中山北路");
        String t="value:";
        String a =aList.stream()
                .reduce(t, new BinaryOperator<String>() {
                    @Override
                    public String apply(String s, String s2) {
                        return s.concat(s2);
                    }
                });    
        System.out.println(a);
    }

結果如下:

3.三個參數的情況主要是在並行(parallelStream)情況下使用:可以參考(https://blog.csdn.net/icarusliu/article/details/79504602),有需要可以了解下。

 

2.2.2Collect

collect是一個非常常用的末端操作,它本身的參數很復雜,有3個:

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

還好,考慮到我們日常使用,java8提供了一個收集器(Collectors),它是專門為collect方法量身打造的接口:

我們常常使用collect將流轉換成List,Map或Set:

1.轉換成list:

Stream<String> stream = Stream.of("I", "love", "you", "too"); 
List<String> list = stream.collect(Collectors.toList());

2.轉換成Map:

我們可以使用Collector.toMap()接口:Collectors.toMap(keyMapper, valueMapper),這里就需要我們指定key和value分別是什么。

例:我們將數組中的字符串作為key,字符串長度作為value,生成一個map:

String address = "中山北路南京大學仙林校區";
        List<String> aList = new ArrayList<>();
        aList.add("南京");
        aList.add("大學");
        aList.add("仙林校區");
        aList.add("仙林大學城");
        aList.add("中山北路");
        String t="value:";
        
        Map<String, Integer> maps = 
                aList.stream().collect(Collectors.toMap(Function.identity(), String::length));
        System.out.println(maps);

 

打印結果:

{中山北路=4, 大學=2, 仙林大學城=5, 仙林校區=4, 南京=2}

 

通常,我們在進行分組操作的時候也會將容器轉換為Map,這里也說明一下:Collectors.groupingBy(classifier)

groupingBy與sql的group by類似,就是一個分組函數,

例:我們將數組中的字符串按長度分組:

        String address = "中山北路南京大學仙林校區";
        List<String> aList = new ArrayList<>();
        aList.add("南京");
        aList.add("大學");
        aList.add("仙林校區");
        aList.add("仙林大學城");
        aList.add("中山北路");
        String t="value:";
        
        Map<Integer, List<String>> maps = 
                aList.stream().collect(Collectors.groupingBy(String::length));
        System.out.println(maps);

打印結果:

{2=[南京, 大學], 4=[仙林校區, 中山北路], 5=[仙林大學城]}

 

其他的末端操作api: 

 findFirst:返回第一個元素,常與orElse一起用:  Stream.findFirst().orElse(null):返回第一個,如果沒有則返回null

 allMatch:檢查是否匹配所有元素:Stream.allMatch(str->str.equals("a"))

 anyMatch:檢查是否至少匹配一個元素.

 

3.Stream的特性

1. 中間操作惰性執行:一個流后面可以跟隨0到多個中間操作,主要目的是打開流,並沒有真正的去計算,而是做出某種程度的數據映射 /過濾,然后返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅調用到這類方法,並沒有真正開始流的遍歷,並沒有消耗資源。
還有多個中間操作的話,這里的時間復雜度並不是n個for循環,轉換操作都是 lazy 的, 多個轉換操作只會在 Terminal 操作的時候融合起來,一次循環完成可以這樣簡單的理解,Stream 里有個操作函數的集合,每次轉換操作就是把轉換函數放入這個集合中,在Terminal操作的時候循環 Stream 對應的集合,然后對每個元素執行所有的函數。

2.流的末端操作只能有一次: 當這個操作執行后,流就被使用“光”了,無法再被操作。所以這必定是流的最后一個操作。之后如果想要操作就必須新打開流。

 

 

關於流被關閉不能再操作的異常:

這里曾經遇到過一個錯誤:stream has already been operated upon or closed

意思是流已經被關閉了,這是因為當我們使用末端操作之后,流就被關閉了,無法再次被調用,如果我們想重復調用,只能重新打開一個新的流。

 

 

 

 

 

 

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM