Stream基礎知識

Stream API

Stream是Java8中處理集合的關鍵抽象概念,它能夠指定你但願對集合進行的操做,可是將執行操做的時間交給具體實現來決定。例如,若是你但願計算某個方法的平均值,你能夠在每一個元素上指定調用的方法,從而得到全部值的 平均值。你可使用Stream API來並行執行操做,使用過多線程來計算每一段的總和與數量,再將結果彙總起來。正則表達式

一個Stream表面上看與一個集合很相似,容許你改變和獲取數據。可是實際上它與集合是有很大區別的:數組

  1. Stream本身不會存儲元素。元素可能被存儲在底層的集合中,或者根據須要產生出來。
  2. Stream操做符不會改變源對象。相反,它們會返回一個持有結果的新Stream。
  3. Stream操做符多是延遲執行的。這意味者它們會等到須要結果的時候才執行。例如你只想要前5個長單詞,而不須要統計全部長字符,那麼filter方法將會在5次匹配後中止過濾。所以,甚至能夠有無限的Stream。
List<String> words = ...;
//Java8在Collection接口中新添加的stream方法,能夠將任何集合轉化爲一個Stream
long count = words.stream().filter(w -> w.length() > 12).count();

//將stream方法改爲parallel Stream方法,就可讓StreamAPI並行執行過濾和統計操做。
long count = words.vStream().filter(w -> w.length() > 12).count();

Stream遵循「作什麼,而不是怎麼去作」的原則。上面的代碼中,描述了須要作什麼:獲的長單詞並對它們的個數進行統計。咱們沒有制定按照什麼順序,或者在哪一個線程中作,它們都是理所應當發生的。相反,循環在一開始就須要指定如何進行運算,所以就是去了優化的機會。
當你使用Stream時,你會經過三個階段來創建一個操做流水線。安全

  1. 建立一個Stream
  2. 在一個或多個步驟中,指定將初始Stream轉換爲另外一個Stream的中間操做。
  3. 使用一個終止操做來產生一個結果。該操做會強制它以前的延遲操做當即執行。在這以後,該Stream就不會再被使用了。

在上面的代碼中,經過stream或parallelStream方法來建立Stream,在經過filter方法對其進行轉換,而count方法就是終止操做。多線程

建立Stream

  1. Java8在Collection接口中新添加的stream方法,能夠將任何集合轉化爲一個Stream併發

  2. 若是是一個數組,能夠用靜態的Stream.of方法將它轉化爲一個Stream;若是須要將數組的一部分轉化爲Stream,可使用Arrays.stream()dom

     Stream<String> words = Stream.of(String[] array);
     Stream<String> words = Stream.of(String ...args);
     Stream<String> words = Arrays.stream(array, from, to);
  3. 要建立一個不含任何元素的Stream,可使用靜態的Stream.empty()ide

  4. 建立無限Stream的靜態方法。函數

    • generate方法接受一個無參的函數(從技術上說,是一個Supplier <T>接口的對象)。當須要一個Stream值時,就能夠調用該方法來產生一個值。優化

      Stream<String> echos = Stream.generate( () -> "Echo");//建立一個含有常量值的Stream
      Stream<Double> randoms = Stream.generate( Math::random );
    • iteratre方法接受一個"種子(seed)"值和一個函數(從技術上講,是一個UnaryOperator<T>接口的對象)做爲參數,而且會對以前的值重複應用該函數。例如:ui

      Stream<BigInterger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInter.ONE));

      在Java8中,添加了許多可以產生Stream的方法。例如,Pattern類添加了一個splitAsStream的方法,可以按照正則表達式對CharSequence(接口,String、StringBuilder和StringBuffer都實現該接口)對象進行分隔。

      Stream<String> words = Pattern.compile(",").splitAsStream("abc,def");

Stream的轉換

Stream轉換是指從一個流中讀取數據,並將轉換後的數據寫入另外一流中。

  1. filter、map、flatMap方法

    • filter()
      filter方法的參數是一個Predicate<T>對象——即一個從T到Boolean的函數。

      List<String> wordList = ...;
      Stream<String> words = wordList.stream();
      Stream<String> longWOrds = words.filter(w -> w.length() > 12);
    • map()
      咱們常常須要對一個流中的值進行某種形式的轉換。這時能夠考慮使用map方法,並傳遞給它一個執行轉換的函數。例如

      //使用了帶有一個方法引用的map方法
      Stream<String> lovercaseWords = words.map(String::toLowerCase);
      //一般會使用一個lambda表達式來代替方法表達式
      Stream<Character> firstChars = words.map(s -> charAt(0)); //該方法產生的流會包含每一個單詞的第一個字符
    • flatMap()
      假設如今有一個函數,以下:

      public static Stream<Character> characterStream(String s) {
          List<Character> result = new ArrayList<>();
          for (char c : s.toCharArray()) result.add(c);
          return result.stream();
      }        

      調用該方法,如characterStream("boat")會返回流['b', 'o', 'a', 't']。假設將該方法映射到一個字符串流上:
      Stream<Stream<Character>> result = words.mapo(w -> characterStream(w));
      將會獲得一個包含多個流的流,如:[..., ['b', 'o', 'a', 't'], ...]。若是要將其展開爲一個只包含字符串的流[..., 'b', 'o', 'a', 't', ...],則須要使用flatMap方法,而不是map方法:

      Stream<Character> letters = words.flatMap(w -> characterStream(w));
  2. 提取子流和組合流

    • limit()
      Stream.limit(n)會返回一個包含n個元素的新流(若是原始長度小於m,則返回原始的流)。這個方法特別適用於裁剪指定長度的流。例如產生一個包含100個隨機數字的流:
      Stream<Double> randoms = Stream.generate(Math::random).limit(100);
    • skip()
      Stream.skip(n)正好相反,它會丟掉前面的n個元素。
      Stream<String> words = Stream.of("abc", "cdf","ghi").skip(1); //會丟棄掉"abc"
    • concat()
      Stream.concat(Stream, Stream)將兩個流鏈接到一塊兒:
      Stream<Character> combined = Stream.concat(Stream.of('H', 'e', 'l', 'l', 'o'), Stream.of('W', 'o', 'r', 'l', 'd'));
      //產生一個新流['H', 'e', 'l', 'l', 'o','W', 'o', 'r', 'l', 'd']
      注意,第一個流的長度不該該是無限的,不然第二個流舊永遠沒有機會被添加到第一個流後面。
  3. 有狀態的轉換
    以前介紹的流轉換都是無狀態的,即當從一個已過濾或已映射的流中獲取某個元素時,結果並不依賴以前的元素。除此以外,Java8也提供了有狀態的轉換。

    • distinct()
      distinct方法會根據原始流中的元素返回一個具備相同順序、抑制了重複元素的新流。顯然,該流必須記住以前已讀取的元素。
      Stream<String> uniqueWords = Stream.of("merrily", "merrily", "gently").distinct();
      // 只獲取一個"merrily"
    • sorted()
      sorted方法必須遍歷整個流,並在產生任何元素以前對它進行排序。顯然,沒法對一個無限流進行排序。
      Stream<String> longestFirst = words.sorted(Comparator.comparing(String::length).reversed());

Stream終止操做

前面已經瞭解任何創###Stream終止操做建流和轉換流,接下來將介紹: 如何從數據流數據中找到「答案」。

  1. 聚合方法
    在本節中介紹的方法統稱聚合方法。它們將流聚合爲一個值,以便在程序中使用。聚合方法都是終止操做。當一個流應用了終止操做後,它就不能再應用其它操做了。

    • count()
      返回流中元素總數。

      long count = Stream.of("abc", "bcd", "cde").count(); //count的結果爲 3。
    • max(), min()
      max方法返回流中最大值。min方法返回流中最小值。
      須要注意的是,它們返回的是一個Optional<T>值,它可能會封裝返回值,也可能表示沒有返回(當流爲空時)。之前在這種狀況一般會返回null,程序拿到該返回值後,進行下一步操做,可能會致使拋出空指針異常。在Java8中,Optional類型時一種更好的表示缺乏返回值的方式。
      下面是一個如何得到最大值的示例:

      Optional<String> largest = Stream.of("abc", "bcd", "cde").max(String::compareToIgnoreCase);
      if (largest.isPresent())
          System.out.println("largest: " + largest.get());
    • findFirst(), findAny()
      findFirst方法會返回非空集合中的第一個值(一般與filter方法結合使用);若是想找到全部匹配的元素中的任意一個,那麼可使用findAny方法,這個方法在對流進行並行執行時十分有效,由於只要在任何片斷中發現第一個匹配元素,都會結束整個計算。

      Optional<String> startsWithQ = Stream.of("abc", Qabc1", "bcd", "Qabc2").parallel().filter(s -> s.startWith("Q")).findAny();
    • allMatch(), anyMatch()
      allMatch方法和anyMatch方法,它們分別在全部元素和沒有元素匹配predicate時返回true。雖然這些方法老是會檢查整個流,可是仍能夠經過並行執行來提升速度。

  2. Optional類型
    Optional<T>對象或者是對一個對象的封裝,或者表示不是任何對象。它通常比指向T類型的引用更安全,由於它不會反回null。
    若是存在被封裝的對象,那麼get方法會返回該對象,不然會返回一個NoSuchElementException。所以,

    Optional<T> optionalValue = ...;
    optionalValue.get().someMethod();

    並不比下面的方式更安全:

    T value = ...;
    value.someMethod();

    isPresent方法會反映出一個Optional<T>對象是否有值。一樣的:

    if (optionalValue.isPresent()) optionalValue.get().someMethod();

    並不比下面的方式更簡單:

    if (value != null) value.someMethod();

    下面開始瞭解如何真正使用Optional值。

    • 使用Optional
      高效使用Optional的關鍵在於,使用一個或者接受正確值、或者返回另外一個替代值的方法。
      • ifPresent()
        ifPresent方法能夠接受一個函數。若是存在可選值,那麼它會將該值傳遞給函數,不然不會進行任何操做。例如,若是但願在當有值存在時將它添加到一個集合中,能夠調用:
        optionalValue.ifPresent(v -> results.add(v));

        //或者

        optionalValue.ifPresent(results::add);
      • map()
        當調用ifPresent方法時,不會反回任何值。若是你但願對結果進行處理,可使用map方法。
        Optional<Boolean> added = optionalValue.map(results::add);

        由於results.add()返回值是boolean類型,如今added有多是如下三種值:被封裝到Optional中的true或者false,或者是一個空的可選值。

      • orElse()
        已經瞭解了當一個可選值存在時應該如何對它優雅地進行處理。另外一種使用可選值的方式是,當沒有值存在時,產生一個替代值。一般,當沒有可匹配項時,會但願使用一個默認值,例如一個空字符串:
        String result = optionalString.orElse("");
        // 若是封裝的字符串爲空的話,則使用給定的空字符串""
      • orElseGet()
        當沒有值存在時,還能夠調用代碼來計算
        String result = optionalString.orElseGet(() -> System.getProperty("user.dir"));
      • orElseThrow()
        當沒有值存在時,會拋出一個指定的異常。
        String result = optionalString.orElseThrow(NoSuchElementException::new);
        //須要提供一個產生異常對象的方法
    • 建立可選值
      以前已經討論瞭如何處理一個已存在的Optional對象。接下來講明如何建立一個Optional對象。
      • Optional.of(obj),Optional.empty()
        of方法建立一個封裝了obj的Optional對象,empty方法建立一個」空"的Optional對象。
      • Optional.ofNullable()
        ofNullable方法被設計爲null值和可選值之間的一座橋樑。若是obj不爲null,那麼Optional.ofNullable(obj)會返回Optional.of(obj),不然會返回Optional.empty()。
    • 使用flatMap來組合可選值函數
      -- 暫略
  3. 聚合操做

  4. 收集結果
    當你處理完流以後,一般只是想看一下結果,而不是將它們聚合爲一個值。

    • iterator()
      該方法會生成一個傳統風格的迭代器,用於訪問元素。

    • toArray()
      因爲沒法在運行時建立一個泛型數組,因此表達式stream.toArray()會返回一個Object[]數組。若是但願獲得一個正確類型的數組,能夠將類型傳遞給數組的構造函數:

      String[] result = String.of("abc", "bcd", "cde").toArray(String[]::new);
    • collect()

      • collect(Supplier, BiConsumer, BiConsumer)
        該方法接收三個參數:
        1. 一個能建立目標類型實例的方法,例如HashSet的構造方法
        2. 一個將元素添加到目標中的方法,例如一個add方法
        3. 一個將兩個對象整合到一塊兒的方法,例如addAll方法

      下面是如何使用HashSet的collect方法的示例:

      HashSet<String> result = String.of("abc", "bcd", "abc").collect(HashSet::new, HashSet::add, HashSet::addAll);
      • collect(Collector)
        上面介紹了須要接收三個參數的collect方法,在實際中,並不須要這麼作,由於Collector接口已經爲咱們提供了這三個方法,而且Collectors類還爲經常使用的手機類型提供了各個工廠方法。
        要將一個流收集到一個list或者set中,只須要調用:
        List<String> result = String.of("abc", "bcd").collect(Collectors.toList());
        Set<String> result = String.of("abc", "bcd", "abc").collect(Collectors.toSet());

        若是但願控制獲得的set類型,可使用以下方式:

        TreeSet<String> result = String.of("abc", "bcd", "abc").collect(Collectors.toCollection(TreeSet::new));
        假設有一個Steam<Person>對象,而且但願將其中的元素收集到一個map中,這樣隨後能夠經過它們的id來查找。Collectors.toMap方法有兩個函數參數,分別用來生成map的鍵和值。例如:
        Map<Integer, String> idToName = persons.collect(Collectors.toMap(Person::getId, Person::getName));
        Map<Integer, Person> idToPerson = persons.collect(Collectors.toMap(Person::getId, Function.identity()));
    • forEach(), forEachOrdered()
      當只要將它們打印出來,或逐個遍歷它們,那麼可使用forEach方法,以下:

      Stream.of("abc", "bcd", "cdf").forEach(System.out::println);

      向該方法傳遞的函數會被應用到流中的每一個元素上。須要注意的是,在一個並行流上,要確保該函數能夠被併發執行。

    在一個並行流上,可能會以任意的順序來訪問元素。若是但願按照流的順序來執行它們,那麼可使用forEachOrdered方法。

    forEach方法和forEachOrdered方法都是終止操做。所以在調用它們以後,就不能再使用這個流了。若是但願還能繼續使用這個流,請使用peek方法。

其餘

    1. 原始類型流
      假設有一個整型數組,並將該數組收集到一個Stream<Integer>的流中,不過獎每一個證書包裝成Integer對象顯然是一個低效的作法,對於其餘原始類型double、float、long、short、char、byte及boolean也是同樣。爲此,Stream API提供了IntStream和LongStream和DoubleStream三種類型,專門用來直接存儲原始類型值,沒必要使用包裝,而Stream API設計者認爲不須要爲其它5種原始類型都添加對應的專門類型。

      要建立一個IntStream,能夠調用IntStream.of和Arrays.stream方法:

      IntStream stream = IntStream.of(1, 3, 5, 7);
      IntStream stream = Arrays.stream(values, from, to); // values是一個int[]數組
    2. 並行流
      流使得並行計算變得容易。默認狀況下,流操做會建立一個串行流,方法Collection.parallelStream()除外。parallel方法能夠將任意的串行流轉換爲一個並行流。例如:

      Stream<String> parallelWords = Stream.of(wordArray).parallel();

      一個並行流,只要在終止方法執行前,流處於並行模式,那麼全部延遲執行的流操做就都會被並行執行。

      當並行運行流操做時,須要確保傳遞給並行流操做的函數都是線程安全的,應當返回與串行運行時相同的結果。很重要的一點是,這些操做都是無狀態的,所以能夠以任意順序被執行。

      默認狀況下,從有序集合、範圍值、生成器及迭代器,或者調用Stream.storted所產生的流,都是有序的。有序並不會妨礙並行,例如,當計算stream.map(fun)時,流能夠被分爲n段,每一段都會被併發處理。而後再按順序將結果組合起來。

      當不考慮有序時,一些操做能夠更有效地並行運行。調用Stream.unordered方法能夠不關心順序。好比,能夠放棄有序來加快limit方法的速度。若是隻須要一個流中的任意n個元素,並不關心具體內容時,能夠調用:

      Stream<String> stream = stringStream.parallel().unordered().limit(n);
    3. 函數式接口
      在前文中,你已經瞭解了許多參數爲函數的操做。例如,Stream.filter方法就將一個函數做爲參數,filter方法的描述以下:

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

      filter方法的使用例子,以下:

      Stream<String> longWords = words.filter( s -> s.length() >= 12 );
      //words是一個含有多個字符串的流

      查看文檔能夠知道,Predicate是一個接口,只含有一個返回boolean值的非默認方法:

      public interface Predicate {
          boolean test(T argument);
      }

      在實際開發中,開發人員可能會常常傳一個lambda表達式或者方法引用,因此方法名並不重要。重要的部分是返回boolean值。當查閱文檔時,只須要記住Predicate是一個返回boolean值的函數就好了。

      下表總結了可以做爲Stream和Collectors方法參數的函數式接口。

      函數式接口 參數類型 返回類型 描述
      Supplier<T> T 提供一個T類型的值
      Consumer<T> T void 處理一個T類型的值
      BiConsumer<T, U> T, U void 處理T類型和U類型的值
      Predicate<T> T boolean 一個 計算Boolean值的函數
      ToIntFunction<T>
      ToLongFunction<T>
      ToDoubleFunction<T>
      T int
      long
      double
      分別計算int、long、double值的函數
      IntFunction<R>
      LongFunction<R>
      DoubleFunction<R>
      int
      long
      double
      R 參數分別爲int、long、double類型的函數
      Function<T, R> T R 一個參數類型爲T的函數
      BiFunction<T, U, R> T, U R 一個參數類型爲T和U的函數
      UnaryOperator<T> T T 對類型T進行的一元操做
      BinaryOperator<T> T, T T 對類型T進行的二元操做
相關文章
相關標籤/搜索