JDK1.8-Stream中經常使用的API(流操做)

1 Stream

Stream是一組用來處理數組,集合的API。java

1.1 特性

  1. 不是數據結構,沒有內部存儲。
  2. 不支持索引訪問。
  3. 延遲計算
  4. 支持並行
  5. 很容易生成數據或集合
  6. 支持過濾,查找,轉換,彙總,聚合等操做。

1.2 運行機制

Stream分爲源source,中間操做,終止操做。編程

流的源能夠是一個數組,集合,生成器方法,I/O通道等等。數組

一個流能夠有零個或多箇中間操做,每個中間操做都會返回一個新的流,供下一個操做使用,一個流只會有一個終止操做。bash

Stream只有遇到終止操做,它的源纔會開始執行遍歷操做。數據結構

1.3 Stream的建立

  1. 經過數組,Stream.of()
  2. 經過集合
  3. 經過Stream.generate方法來建立
  4. 經過Stram.iterate方法
  5. 其餘API
import java.util.Arrays;
    import java.util.List;
    import java.util.stream.IntStream;
    import java.util.stream.Stream;
    
    public class CreateStream {
    
    	//經過數組,Stream.of()
        static void gen1(){
            String[] str = {"a","b","c"};
            Stream<String> str1 = Stream.of(str);
        }
    
    	//經過集合
        static void gen2(){
            List<String> strings = Arrays.asList("a", "b", "c");
            Stream<String> stream = strings.stream();
        }
    
    	//經過Stream.generate方法來建立
        static void gen3(){
        	//這是一個無限流,經過這種方法建立在操做的時候最好加上limit進行限制
            Stream<Integer> generate = Stream.generate(() -> 1);
            generate.limit(10).forEach(x -> System.out.println(x));
        }
        
    	//經過Stram.iterate方法
        static void gen4(){
            Stream<Integer> iterate = Stream.iterate(1, x -> x +1);
            iterate.forEach(x -> System.out.println(x));
        }
    
    	//其餘API
        static void gen5(){
            String str = "abc";
            IntStream chars = str.chars();
            chars.forEach(x -> System.out.println(x));
        }
    }
複製代碼

2 Stream經常使用的API

2.1 中間操做

2.1.1 filter 過濾

該操做會接受一個謂詞(一個返回boolean的函數)做爲參數,並返回一個包括全部符合謂詞的元素的流。說白了就是給一個條件,filter會根據這個條件截取流中得數據。框架

public static void testFilter(){
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        //截取全部能被2整除得數據
        List<Integer> collect = integers.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());
        System.out.println("collect = " + collect);
    }
複製代碼

結果:異步

collect = [2, 4, 6, 8, 10]
複製代碼

2.1.2 distinct 去重

該操做會返回一個元素各異(根據流所生成元素的hashCode和equals方法實現)的流。編程語言

public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        List<Integer> collect = numbers.stream().distinct().collect(Collectors.toList());
        System.out.println("collect = " + collect);
}
複製代碼

結果:ide

collect = [1, 2, 3, 4]
複製代碼

2.1.3 sorted 排序

對流中得數據進行排序,能夠以天然序或着用Comparator 接口定義的排序規則來排序一個流。Comparator 能使用lambada表達式來初始化,還可以逆序一個已經排序的流。函數式編程

public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(5, 8, 2, 6, 41, 11);
        //排序默認爲順序  順序 = [2, 5, 6, 8, 11, 41]
        List<Integer> sorted = integers.stream().sorted().collect(Collectors.toList());
        System.out.println("順序 = " + sorted);
        //逆序    逆序 = [41, 11, 8, 6, 5, 2]
        List<Integer> reverseOrder = integers.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
        System.out.println("逆序 = " + reverseOrder);
        //也能夠接收一個lambda
        List<Integer> ages = integers.stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList());
    }
複製代碼

2.1.4 limit 截取

該方法會返回一個不超過給定長度的流。

public static void testLimit(){
        List<Integer> integers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        //截取流中得前三個元素  collect = [1, 2, 1]
        List<Integer> collect = integers.stream().limit(3).collect(Collectors.toList());
        System.out.println("collect = " + collect);
    }
複製代碼

2.1.5 skip 捨棄

該方法會返回一個扔掉了前面n個元素的流。若是流中元素不足n個,則返回一個空流。

public static void testSkip(){
        List<Integer> integers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        //丟掉流中得前三個元素  collect = [3, 3, 2, 4]
        List<Integer> collect = integers.stream().skip(3).collect(Collectors.toList());
        System.out.println("collect = " + collect);
}
複製代碼

2.1.6 map 概括

該方法會接受一個函數做爲參數,這個函數會被應用到每一個元素上,並將其映射成一個新的元素。就是根據指定函數獲取流中得每一個元素得數據並從新組合成一個新的元素。

public static void main(String[] args) {
        //本身建好得一個獲取對象list得方法
        List<Dish> dishList = Dish.getDishList();
        //獲取每一道菜得名稱  並放到一個list中
        List<String> collect = dishList.stream().map(Dish::getName).collect(Collectors.toList());
        //collect = [pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
        System.out.println("collect = " + collect);
    }
複製代碼

2.1.7 flatMap 扁平化

該方法key可讓你把一個流中的每一個值都換成另外一個流,而後把全部的流都連接起來成爲一個流。

給 定 單 詞 列 表["Hello","World"] ,你想要返回列表 ["H","e","l", "o","W","r","d"],你可能會認爲這很容易,經過map你能夠把每一個單詞映射成一張字符表,而後調用 distinct 來過濾重複的字符,可是這個方法的問題在於,傳遞給 map 方法的Lambda爲每一個單詞返回了一個 String[] ( String列表)。所以, map 返回的流其實是Stream<String[]> 類型的。而你真正想要的是用Stream 來表示一個字符流。

正確寫法應該是經過flatMap對其扁平化並做出對應處理。

public static void main(String[] args) {
        String[] words = {"Hello", "World"};
        List<String> collect = Stream.of(words).        //數組轉換流
                map(w -> w.split("")).  //去掉「」並獲取到兩個String[]
                flatMap(Arrays::stream).        //方法調用將兩個String[]扁平化爲一個stream
                distinct().                     //去重    
                collect(Collectors.toList());
        //collect = [H, e, l, o, W, r, d]
        System.out.println("collect = " + collect);
    }
}
複製代碼

2.1.8 peek

peek 的設計初衷就是在流的每一個元素恢復運行以前,插入執行一個動做。

public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
        List<Integer> result =
                numbers.stream()
                        .peek(x -> System.out.println("from stream: " + x))
                        .map(x -> x + 17)
                        .peek(x -> System.out.println("after map: " + x))
                        .filter(x -> x % 2 == 0)
                        .peek(x -> System.out.println("after filter: " + x))
                        .limit(3)
                        .peek(x -> System.out.println("after limit: " + x))
                        .collect(Collectors.toList());
        
    }
複製代碼

結果:

from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22
after filter: 22
after limit: 22
複製代碼

在這裏插入圖片描述

2.1.9 collect 收集

從上面得代碼已經能夠看出來,collect是將最終stream中得數據收集起來,最終生成一個list,set,或者map。

public static void main(String[] args) {
        List<Dish> dishList = Dish.getDishList();
        //list
        List<Dish> collect = dishList.stream().limit(2).collect(Collectors.toList());
        //set
        Set<Dish> collect1 = dishList.stream().limit(2).collect(Collectors.toSet());
        //map
        Map<String, Dish.Type> collect2 = dishList.stream().limit(2).collect(Collectors.toMap(Dish::getName, Dish::getType));
}
複製代碼

這裏面生成map得toMap方法有三個重載,傳入得參數都不一樣,這裏使用得是傳入兩個Function類型得參數。固然,Collectors的功能還不止這些,下面的收集器中會有其餘的詳解。

2.2 終止操做

  • 循環 forEach
  • 計算 min、max、count、average
  • 匹配 anyMatch、allMatch、noneMatch、findFirst、findAny
  • 匯聚 reduce
  • 收集器 collect

2.3 查找和匹配

另外一個常見的數據處理套路是看看數據集中的某些元素是否匹配一個給定的屬性。Stream API經過allMatch,anyMatch,noneMatch,findFirst和findAny方法提供了這樣的工具。

查找和匹配都是終端操做。

2.3.1 anyMatch

anyMatch方法能夠回答「流中是否有一個元素能匹配到給定的謂詞」。會返回一個boolean值。

public class AnyMatch {
    public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        boolean b = dish.stream().anyMatch(Dish::isVegetarian);
        System.out.println(b);
    }
}
複製代碼

2.3.2 allMatch

allMatch方法和anyMatch相似,校驗流中是否都能匹配到給定的謂詞。

class AllMatch{
    public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        //是否全部菜的熱量都小於1000
        boolean b = dish.stream().allMatch(d -> d.getCalories() < 1000);
        System.out.println(b);
    }
}
複製代碼

2.3.3 noneMatch

noneMatch方法能夠確保流中沒有任何元素與給定的謂詞匹配。

class NoneMatch{
    public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        //沒有任何菜的熱量大於等於1000
        boolean b = dish.stream().allMatch(d -> d.getCalories() >= 1000);
        System.out.println(b);
    }
}
複製代碼

anyMatch,noneMatch,allMatch這三個操做都用到了所謂的短路。

2.3.4 findAny

findAny方法將返回當前流中的任意元素。

class FindAny{
    public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        Optional<Dish> any = dish.stream().filter(Dish::isVegetarian).findAny();
        System.out.println("any = " + any);
    }
}
複製代碼

2.3.5 findFirst

findFirst方法能找到你想要的第一個元素。

class FindFirst{
        public static void main(String[] args) {
            List<Dish> dish = Dish.getDish();
            Optional<Dish> any = dish.stream().filter(Dish::isVegetarian).findFirst();
            System.out.println("any = " + any);
        }
    }
複製代碼

2.4 歸約 reduce

此類查詢須要將流中全部元素反覆結合起來,獲得一個值,好比一個 Integer 。這樣的查詢能夠被歸類爲歸約操做(將流歸約成一個值)。用函數式編程語言的術語來講,這稱爲摺疊(fold),由於你能夠將這個操 做當作把一張長長的紙(你的流)反覆摺疊成一個小方塊,而這就是摺疊操做的結果。

2.4.1 元素求和

public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3, 6, 8);
        //求list中的和,以0爲基數
        Integer reduce = integers.stream().reduce(0, (a, b) -> a + b);
        //Integer的靜態方法
        int sum = integers.stream().reduce(0, Integer::sum);
        System.out.println("reduce = " + reduce);
    }
複製代碼

2.4.2 最大值和最小值

public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3, 6, 8);
        Optional<Integer> min = integers.stream().reduce(Integer::min);
        System.out.println("min = " + min);
        Optional<Integer> max = integers.stream().reduce(Integer::max);
        System.out.println("max = " + max);
    }
複製代碼

2.5 收集器 Collectors

2.5.1 查找流中的最大值和最小值 minBy maxBy

public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        //建立一個Comparator來進行比較  比較菜的卡路里
        Comparator<Dish> dishComparator = Comparator.comparingInt(Dish::getCalories);
        //maxBy選出最大值
        Optional<Dish> collect = dish.stream().collect(Collectors.maxBy(dishComparator));
        System.out.println("collect = " + collect);
        //選出最小值
        Optional<Dish> collect1 = dish.stream().collect(Collectors.minBy(dishComparator));
        System.out.println("collect1 = " + collect1);
    }
複製代碼

2.5.2 彙總 summingInt

Collectors.summingInt 。它可接受一個把對象映射爲求和所需 int 的函數,並返回一個收集器。

public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        //計算總和
        int collect = dish.stream().collect(Collectors.summingInt(Dish::getCalories));
        System.out.println("collect = " + collect);
    }
複製代碼

2.5.3 平均數 averagingInt

public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        //計算平均數
        Double collect = dish.stream().collect(Collectors.averagingInt(Dish::getCalories));
        System.out.println("collect = " + collect);
    }
複製代碼

2.5.4 鏈接字符串 joining

public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        String collect = dish.stream().map(Dish::getName).collect(Collectors.joining());
        System.out.println("collect = " + collect);
    }
複製代碼

joining 工廠方法有一個重載版本能夠接受元素之間的分界符,這樣你就能夠獲得一個逗號分隔的菜餚名稱列表。

String collect = dish.stream().map(Dish::getName).collect(Collectors.joining(", "));
複製代碼

2.5.5 獲得流中的總數 counting

long howManyDishes = dish.stream().collect(Collectors.counting());
複製代碼

2.6 分組

2.6.1 分組 groupingBy

public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        //groupingBy接受一個function做爲參數
        Map<Dish.Type, List<Dish>> collect = dish.stream().collect(Collectors.groupingBy(Dish::getType));
        System.out.println("collect = " + collect);
    }
複製代碼

若是想用以分類的條件可能比簡單的屬性訪問器要複雜。例如,你可能想把熱量不到400卡路里的菜劃分爲「低熱量」(diet),熱量400到700卡路里的菜劃爲「普通」(normal),高於700卡路里的劃爲「高熱量」(fat)。因爲 Dish 類的做者沒有把這個操做寫成一個方法,你沒法使用方法引用,但你能夠把這個邏輯寫成Lambda表達式。

public static void main(String[] args) {
        List<Dish> dishList = Dish.getDish();
        Map<String, List<Dish>> collect = dishList.stream().collect(Collectors.groupingBy(dish->{
            if (dish.getCalories() <= 400) {
                return "DIET";
            } else if (dish.getCalories() <= 700) {
                return "NORMAL";
            } else {
                return "FAT";
            }
        }));
        System.out.println("collect = " + collect);
    }
複製代碼

2.6.2 多級分組

要實現多級分組,咱們可使用一個由雙參數版本的 Collectors.groupingBy 工廠方法建立的收集器,它除了普通的分類函數以外,還能夠接受 collector 類型的第二個參數。那麼要進行二級分組的話,咱們能夠把一個內層groupingBy 傳遞給外層 groupingBy ,並定義一個爲流中項目分類的二級標準。

public static void main(String[] args) {
        List<Dish> dish = Dish.getDish();
        Map<Dish.Type, Map<String, List<Dish>>> collect = dish.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.groupingBy(d -> {
            if (d.getCalories() <= 400) {
                return "DIET";
            } else if (d.getCalories() <= 700) {
                return "NORMAL";
            } else {
                return "FAT";
            }
        })));
        System.out.println("collect = " + collect);
    }
複製代碼

2.6.3 按子組收集數據

在上一面,咱們看到能夠把第二個 groupingBy 收集器傳遞給外層收集器來實現多級分組。但進一步說,傳遞給第一個 groupingBy 的第二個收集器能夠是任何類型,而不必定是另外一個 groupingBy 。

例如,要數一數菜單中每類菜有多少個,能夠傳遞 counting 收集器做爲groupingBy 收集器的第二個參數。

Map<Dish.Type, Long> typesCount = dish.stream().collect(groupingBy(Dish::getType, counting()));
複製代碼

普通的單參數 groupingBy(f) (其中 f 是分類函數)其實是 groupingBy(f,toList()) 的簡便寫法。

把收集器的結果轉換爲另外一種類型:

Collectors.collectingAndThen工廠方法

3 並行流

並行流就是一個把內容分紅多個數據塊,並用不一樣的線程分別處理每一個數據塊的流。這樣一來,你就能夠自動把給定操做的工做負荷分配給多核處理器的全部內核,讓它們都忙起來。

3.1 將順序流轉爲並行流

你能夠把流轉換成並行流,從而讓前面的函數歸約過程(也就是求和)並行運行——對順序流調用 parallel 方法:

public static long parallelSum(long n) {
        return Stream.iterate(1L, i -> i + 1)
                .limit(n)
                .parallel()
                .reduce(0L, Long::sum);
    }
複製代碼

Stream 在內部分紅了幾塊。所以能夠對不一樣的塊獨立並行進行概括操做,最後,同一個概括操做會將各個子流的部分概括結果合併起來,獲得整個原始流的概括結果。

在這裏插入圖片描述
相似地,你只須要對並行流調用 sequential 方法就能夠把它變成順序流。

配置並行流使用的線程池

看看流的 parallel 方法,你可能會想,並行流用的線程是從哪兒來的?有多少個?怎麼自定義這個過程呢?

並行流內部使用了默認的 ForkJoinPool (7.2節會進一步講到分支/合併框架),它默認的線 程 數 量 就是 你 的 處 理器 數 量 , 這個 值 是 由 Runtime.getRuntime().available-Processors() 獲得的。 但 是 你 可 以 通 過 系 統 屬 性 java.util.concurrent.ForkJoinPool.common.parallelism 來改變線程池大小,以下所示: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12"); 這是一個全局設置,所以它將影響代碼中全部的並行流。反過來講,目前還沒法專爲某個並行流指定這個值。通常而言,讓 ForkJoinPool 的大小等於處理器數量是個不錯的默認值,除非你有很好的理由,不然咱們強烈建議你不要修改它。

3.2 高效使用並行流

咱們聲稱並行求和方法應該比順序和迭代方法性能好。然而在軟件工程上,靠猜絕對不是什麼好辦法!特別是在優化性能時,你應該始終遵循三個黃金規則:測量,測量,再測量。

  • 若是有疑問,測量。把順序流轉成並行流垂手可得,但卻不必定是好事。咱們在本節中已經指出,並行流不老是比順序流快。此外,並行流有時候會和你的直覺不一致,因此在考慮選擇順序流仍是並行流時,第一個也是最重要的建議就是用適當的基準來檢查其性能。
  • 留意裝箱。自動裝箱和拆箱操做會大大下降性能。Java 8中有原始類型流( IntStream 、LongStream 、 DoubleStream )來避免這種操做,但凡是有可能都應該用這些流。
  • 有些操做自己在並行流上的性能就比順序流差。特別是 limit 和 findFirst 等依賴於元素順序的操做,它們在並行流上執行的代價很是大。例如, findAny 會比 findFirst 性能好,由於它不必定要按順序來執行。你老是能夠調用 unordered 方法來把有序流變成無序流。那麼,若是你須要流中的n個元素而不是專門要前n個的話,對無序並行流調用limit 可能會比單個有序流(好比數據源是一個 List )更高效。
  • 還要考慮流的操做流水線的總計算成本。設N是要處理的元素的總數,Q是一個元素經過流水線的大體處理成本,則N*Q就是這個對成本的一個粗略的定性估計。Q值較高就意味着使用並行流時性能好的可能性比較大。
  • 對於較小的數據量,選擇並行流幾乎歷來都不是一個好的決定。並行處理少數幾個元素的好處還抵不上並行化形成的額外開銷。
  • 要考慮流背後的數據結構是否易於分解。例如, ArrayList 的拆分效率比 LinkedList高得多,由於前者用不着遍歷就能夠平均拆分,然後者則必須遍歷。另外,用 range 工廠方法建立的原始類型流也能夠快速分解。
  • 流自身的特色,以及流水線中的中間操做修改流的方式,均可能會改變分解過程的性能。例如,一個 SIZED 流能夠分紅大小相等的兩部分,這樣每一個部分均可以比較高效地並行處理,但篩選操做可能丟棄的元素個數卻沒法預測,致使流自己的大小未知。
  • 還要考慮終端操做中合併步驟的代價是大是小(例如 Collector 中的 combiner 方法)。若是這一步代價很大,那麼組合每一個子流產生的部分結果所付出的代價就可能會超出經過並行流獲得的性能提高。

在這裏插入圖片描述

3.3 分支/合併框架

分支/合併框架的目的是以遞歸方式將能夠並行的任務拆分紅更小的任務,而後將每一個子任務的結果合併起來生成總體結果。它是 ExecutorService 接口的一個實現,它把子任務分配給線程池(稱爲 ForkJoinPool )中的工做線程。

3.3.1 使用RecursiveTask

要把任務提交到這個池,必須建立 RecursiveTask 的一個子類,其中 R 是並行化任務(以 及全部子任務)產生的結果類型,或者若是任務不返回結果,則是 RecursiveAction 類型(當 然它可能會更新其餘非局部機構)。

要定義 RecursiveTask, 只需實現它惟一的抽象方法compute :

protected abstract R compute();
複製代碼

這個方法同時定義了將任務拆分紅子任務的邏輯,以及沒法再拆分或不方便再拆分時,生成單個子任務結果的邏輯。

在這裏插入圖片描述

3.3.2 使用RecursiveTask求和

public class ForkJoinSumCalculator
        extends java.util.concurrent.RecursiveTask<Long> {
    private final long[] numbers;
    private final int start;
    private final int end;
    public static final long THRESHOLD = 10_000;

    public ForkJoinSumCalculator(long[] numbers) {
        this(numbers, 0, numbers.length);
    }

    private ForkJoinSumCalculator(long[] numbers, int start, int end) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length <= THRESHOLD) {
            return computeSequentially();
        }
        //建立一個子任務來爲數組得前一半求和
        ForkJoinSumCalculator leftTask =
                new ForkJoinSumCalculator(numbers, start, start + length / 2);
        //利 用 另 一 個ForkJoinPool線程異步執行新建立的子任務
        leftTask.fork();
        //建立一個子任務來爲數組得後一半求和
        ForkJoinSumCalculator rightTask =
                new ForkJoinSumCalculator(numbers, start + length / 2, end);
        //同步執行第二個子任務,有可能進一步遞歸
        Long rightResult = rightTask.compute();
        //讀取第一個任務得結構,未完成就等待
        Long leftResult = leftTask.join();
        return leftResult + rightResult;
    }

    private long computeSequentially() {
        long sum = 0;
        for (int i = start; i < end; i++) {
            sum += numbers[i];
        }
        return sum;
    }

    public static long forkJoinSum(long n) {
        long[] numbers = LongStream.rangeClosed(1, n).toArray();
        ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
        return new ForkJoinPool().invoke(task);
    }

    public static void main(String[] args) {
        long l = ForkJoinSumCalculator.forkJoinSum(5);
        System.out.println("l = " + l);
    }

}
複製代碼
相關文章
相關標籤/搜索