Java 8 Streams API 詳解

流式編程做爲Java 8的亮點之一,是繼Java 5以後對集合的再一次升級,能夠說Java 8幾大特性中,Streams API 是做爲Java 函數式的主角來設計的,誇張的說,有了Streams API以後,萬物皆可一行代碼。html

什麼是Stream

Stream被翻譯爲流,它的工做過程像將一瓶水導入有不少過濾閥的管道同樣,水每通過一個過濾閥,便被操做一次,好比過濾,轉換等,最後管道的另一頭有一個容器負責接收剩下的水。java

示意圖以下:sql

首先經過source產生流,而後依次經過一些中間操做,好比過濾,轉換,限制等,最後結束對流的操做。編程

Stream也能夠理解爲一個更加高級的迭代器,主要的做用即是遍歷其中每個元素。segmentfault

爲何須要Stream

Stream做爲Java 8的一大亮點,它專門針對集合的各類操做提供各類很是便利,簡單,高效的API,Stream API主要是經過Lambda表達式完成,極大的提升了程序的效率和可讀性,同時Stram API中自帶的並行流使得併發處理集合的門檻再次下降,使用Stream API編程無需多寫一行多線程的大門就能夠很是方便的寫出高性能的併發程序。使用Stream API可以使你的代碼更加優雅。api

流的另外一特色是可無限性,使用Stream,你的數據源能夠是無限大的。數組

在沒有Stream以前,咱們想提取出全部年齡大於18的學生,咱們須要這樣作:微信

List<Student> result=new ArrayList<>();
for(Student student:students){
 
    if(student.getAge()>18){
        result.add(student);
    }
}
return result;

使用Stream,咱們能夠參照上面的流程示意圖來作,首先產生Stream,而後filter過濾,最後歸併到容器中。數據結構

轉換爲代碼以下:多線程

return students.stream().filter(s->s.getAge()>18).collect(Collectors.toList());
  • 首先stream()得到流
  • 而後filter(s->s.getAge()>18)過濾
  • 最後collect(Collectors.toList())歸併到容器中

是否是很像在寫sql?

如何使用Stream

咱們能夠發現,當咱們使用一個流的時候,主要包括三個步驟:

  • 獲取流
  • 對流進行操做
  • 結束對流的操做

獲取流

獲取流的方式有多種,對於常見的容器(Collection)能夠直接.stream()獲取
例如:

  • Collection.stream()
  • Collection.parallelStream()
  • Arrays.stream(T array) or Stream.of()

對於IO,咱們也能夠經過lines()方法獲取流:

  • java.nio.file.Files.walk()
  • java.io.BufferedReader.lines()

最後,咱們還能夠從無限大的數據源中產生流:

  • Random.ints()

值得注意的是,JDK中針對基本數據類型的昂貴的裝箱和拆箱操做,提供了基本數據類型的流:

  • IntStream
  • LongStream
  • DoubleStream

這三種基本數據類型和普通流差很少,不過他們流裏面的數據都是指定的基本數據類型。

Intstream.of(new int[]{1,2,3});
Intstream.rang(1,3);

對流進行操做

這是本章的重點,產生流比較容易,可是不一樣的業務系統的需求會涉及到不少不一樣的要求,明白咱們能對流作什麼,怎麼作,才能更好的利用Stream API的特色。

流的操做類型分爲兩種:

  • Intermediate:中間操做,一個流能夠後面跟隨零個或多個intermediate操做。其目的主要是打開流,作出某種程度的數據映射/過濾,而後會返回一個新的流,交給下一個操做使用。這類操做都是惰性化的(lazy),就是說,僅僅調用到這類方法,並無真正開始流的遍歷。

    map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:終結操做,一個流只能有一個terminal操做,當這個操做執行後,流就被使用「光」了,沒法再被操做。因此這一定是流的最後一個操做。Terminal操做的執行,纔會真正開始流的遍歷,而且會生成一個結果,或者一個 side effect。

    forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

IntermediateTerminal徹底能夠按照上圖的流程圖理解,Intermediate表示在管道中間的過濾器,水會流入過濾器,而後再流出去,而Terminal操做即是最後一個過濾器,它在管道的最後面,流入Terminal的水,最後便會流出管道。

下面依次詳細的解讀下每個操做所能產生的效果:

中間操做

對於中間操做,全部的API的返回值基本都是Stream<T>,所以之後看見一個陌生的API也能經過返回值判斷它的所屬類型。

map/flatMap

map顧名思義,就是映射,map操做可以將流中的每個元素映射爲另外的元素。

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

能夠看到map接受的是一個Function,也就是接收參數,並返回一個值。

好比:

//提取 List<Student>  全部student 的名字 
List<String> studentNames = students.stream().map(Student::getName)
                                             .collect(Collectors.toList());

上面的代碼等同於之前的:

List<String> studentNames=new ArrayList<>();
for(Student student:students){
    studentNames.add(student.getName());
}

再好比:將List中全部字母轉換爲大寫:

List<String> words=Arrays.asList("a","b","c");
List<String> upperWords=words.stream().map(String::toUpperCase)
                                      .collect(Collectors.toList());

flatMap顧名思義就是扁平化映射,它具體的操做是將多個stream鏈接成一個stream,這個操做是針對相似多維數組的,好比容器裏面包含容器等。

List<List<Integer>> ints=new ArrayList<>(Arrays.asList(Arrays.asList(1,2),
                                          Arrays.asList(3,4,5)));
List<Integer> flatInts=ints.stream().flatMap(Collection::stream).
                                       collect(Collectors.toList());

能夠看到,至關於降維。


filter

filter顧名思義,就是過濾,經過測試的元素會被留下來並生成一個新的Stream

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

同理,咱們能夠filter接收的參數是Predicate,也就是推斷型函數式接口,接收參數,並返回boolean值。

好比:

//獲取全部大於18歲的學生
List<Student> studentNames = students.stream().filter(s->s.getAge()>18)
                                              .collect(Collectors.toList());

distinct

distinct是去重操做,它沒有參數

Stream<T> distinct();

sorted

sorted排序操做,默認是從小到大排列,sorted方法包含一個重載,使用sorted方法,若是沒有傳遞參數,那麼流中的元素就須要實現Comparable<T>方法,也能夠在使用sorted方法的時候傳入一個Comparator<T>

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

Stream<T> sorted();

值得一說的是這個ComparatorJava 8以後被打上了@FunctionalInterface,其餘方法都提供了default實現,所以咱們能夠在sort中使用Lambda表達式

例如:

//以年齡排序
students.stream().sorted((s,o)->Integer.compare(s.getAge(),o.getAge()))
                                  .forEach(System.out::println);;

然而還有更方便的,Comparator默認也提供了實現好的方法引用,使得咱們更加方便的使用:

例如上面的代碼能夠改爲以下:

//以年齡排序 
students.stream().sorted(Comparator.comparingInt(Student::getAge))
                            .forEach(System.out::println);;

或者:

//以姓名排序
students.stream().sorted(Comparator.comparing(Student::getName)).
                          forEach(System.out::println);

是否是更加簡潔。


peek

peek有遍歷的意思,和forEach同樣,可是它是一箇中間操做。

peek接受一個消費型的函數式接口。

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

例如:

//去重之後打印出來,而後再歸併爲List
List<Student> sortedStudents= students.stream().distinct().peek(System.out::println).
                                                collect(Collectors.toList());

limit

limit裁剪操做,和String::subString(0,x)有點先溝通,limit接受一個long類型參數,經過limit以後的元素只會剩下min(n,size)個元素,n表示參數,size表示流中元素個數

Stream<T> limit(long maxSize);

例如:

//只留下前6個元素並打印
students.stream().limit(6).forEach(System.out::println);

skip

skip表示跳過多少個元素,和limit比較像,不過limit是保留前面的元素,skip是保留後面的元素

Stream<T> skip(long n);

例如:

//跳過前3個元素並打印 
students.stream().skip(3).forEach(System.out::println);

終結操做

一個流處理中,有且只能有一個終結操做,經過終結操做以後,流才真正被處理,終結操做通常都返回其餘的類型而再也不是一個流,通常來講,終結操做都是將其轉換爲一個容器。

forEach

forEach是終結操做的遍歷,操做和peek同樣,可是forEach以後就不會再返回流

void forEach(Consumer<? super T> action);

例如:

//遍歷打印
students.stream().forEach(System.out::println);

上面的代碼和一下代碼效果相同:

for(Student student:students){
    System.out.println(sudents);
}

toArray

toArrayList##toArray()用法差很少,包含一個重載。

默認的toArray()返回一個Object[]

也能夠傳入一個IntFunction<A[]> generator指定數據類型

通常建議第二種方式。

Object[] toArray();

<A> A[] toArray(IntFunction<A[]> generator);

例如:

Student[] studentArray = students.stream().skip(3).toArray(Student[]::new);

max/min

max/min即便找出最大或者最小的元素。max/min必須傳入一個Comparator

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

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

count

count返回流中的元素數量

long count();

例如:

long  count = students.stream().skip(3).count();

reduce

reduce爲概括操做,主要是將流中各個元素結合起來,它須要提供一個起始值,而後按必定規則進行運算,好比相加等,它接收一個二元操做 BinaryOperator函數式接口。從某種意義上來講,sum,min,max,average都是特殊的reduce

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);

例如:

List<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
        
long count = integers.stream().reduce(0,(x,y)->x+y);

以上代碼等同於:

long count = integers.stream().reduce(Integer::sum).get();

reduce兩個參數和一個參數的區別在於有沒有提供一個起始值,

若是提供了起始值,則能夠返回一個肯定的值,若是沒有提供起始值,則返回Opeational防止流中沒有足夠的元素。


anyMatch allMatch noneMatch

測試是否有任意元素\全部元素\沒有元素匹配表達式

他們都接收一個推斷類型的函數式接口:Predicate

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

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

 boolean noneMatch(Predicate<? super T> predicate)

例如:

boolean test = integers.stream().anyMatch(x->x>3);

findFirst、 findAny

獲取元素,這兩個API都不接受任何參數,findFirt返回流中第一個元素,findAny返回流中任意一個元素。

Optional<T> findFirst();

Optional<T> findAny();

也有有人會問findAny()這麼奇怪的操做誰會用?這個API主要是爲了在並行條件下想要獲取任意元素,以最大性能獲取任意元素

例如:

int foo = integers.stream().findAny().get();

collect

collect收集操做,這個API放在後面將是由於它過重要了,基本上全部的流操做最後都會使用它。

咱們先看collect的定義:

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

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

能夠看到,collect包含兩個重載:

一個參數和三個參數,

三個參數咱們不多使用,由於JDK提供了足夠咱們使用的Collector供咱們直接使用,咱們能夠簡單瞭解下這三個參數什麼意思:

  • Supplier:用於產生最後存放元素的容器的生產者
  • accumulator:將元素添加到容器中的方法
  • combiner:將分段元素所有添加到容器中的方法

前兩個元素咱們都很好理解,第三個元素是幹嗎的呢?由於流提供了並行操做,所以有可能一個流被多個線程分別添加,而後再將各個子列表依次添加到最終的容器中。

↓ - - - - - - - - -

↓ --- --- ---

↓ ---------

如上圖,分而治之。

例如:

List<String> result = stream.collect(ArrayList::new, List::add, List::addAll);

接下來看只有一個參數的collect

通常來講,只有一個參數的collect,咱們都直接傳入Collectors中的方法引用便可:

List<Integer> = integers.stream().collect(Collectors.toList());

Collectors中包含不少經常使用的轉換器。toList(),toSet()等。

Collectors中還包括一個groupBy(),他和Sql中的groupBy同樣都是分組,返回一個Map

例如:

//按學生年齡分組
Map<Integer,List<Student>> map= students.stream().
                                collect(Collectors.groupingBy(Student::getAge));

groupingBy能夠接受3個參數,分別是

  1. 第一個參數:分組按照什麼分類
  2. 第二個參數:分組最後用什麼容器保存返回(當只有兩個參數是,此參數默認爲HashMap
  3. 第三個參數:按照第一個參數分類後,對應的分類的結果如何收集

有時候單參數的groupingBy不知足咱們需求的時候,咱們可使用多個參數的groupingBy

例如:

//將學生以年齡分組,每組中只存學生的名字而不是對象
Map<Integer,List<String>> map =  students.stream().
  collect(Collectors.groupingBy(Student::getAge,Collectors.mapping(Student::getName,Collectors.toList())));

toList默認生成的是ArrayList,toSet默認生成的是HashSet,若是想要指定其餘容器,能夠以下操做:

students.stream().collect(Collectors.toCollection(TreeSet::new));

Collectors還包含一個toMap,利用這個API咱們能夠將List轉換爲Map

Map<Integer,Student> map=students.stream().
                           collect(Collectors.toMap(Student::getAge,s->s));

值得注意的一點是,IntStreamLongStream,DoubleStream是沒有collect()方法的,由於對於基本數據類型,要進行裝箱,拆箱操做,SDK並無將它放入流中,對於基本數據類型流,咱們只能將其toArray()


優雅的使用Stream

瞭解了Stream API,下面詳細介紹一下若是優雅的使用Steam

  • 瞭解流的惰性操做

    前面說到,流的中間操做是惰性的,若是一個流操做流程中只有中間操做,沒有終結操做,那麼這個流什麼都不會作,整個流程中會一直等到遇到終結操做操做纔會真正的開始執行。

    例如:

    students.stream().peek(System.out::println);

    這樣的流操做只有中間操做,沒有終結操做,那麼無論流裏面包含多少元素,他都不會執行任何操做。

  • 明白流操做的順序的重要性

    Stream API中,還包括一類Short-circuiting,它可以改變流中元素的數量,通常這類API若是是中間操做,最好寫在靠前位置:

    考慮下面兩行代碼:

    students.stream().sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      limit(3).              
                      collect(Collectors.toList());
    students.stream().limit(3).
                      sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      collect(Collectors.toList());

    兩段代碼所使用的API都是相同的,可是因爲順序不一樣,帶來的結果都很是不同的,

    第一段代碼會先排序全部的元素,再依次打印一遍,最後獲取前三個最小的放入list中,

    第二段代碼會先截取前3個元素,在對這三個元素排序,而後遍歷打印,最後放入list中。

  • 明白Lambda的侷限性

    因爲Java目前只能Pass-by-value,所以對於Lambda也和有匿名類同樣的final的侷限性。

    具體緣由能夠參考Java 乾貨之深人理解內部類

    所以咱們沒法再lambda表達式中修改外部元素的值。

    同時,在Stream中,咱們沒法使用break提早返回。

  • 合理編排Stream的代碼格式

    因爲可能在使用流式編程的時候會處理不少的業務邏輯,致使API很是長,此時最後使用換行將各個操做分離開來,使得代碼更加易讀。

    例如:

    students.stream().limit(3).
                      sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      collect(Collectors.toList());

    而不是:

    students.stream().limit(3).sorted(Comparator.comparingInt(Student::getAge)).peek(System.out::println).collect(Collectors.toList());

    同時因爲Lambda表達式省略了參數類型,所以對於變量,儘可能使用完成的名詞,好比student而不是s,增長代碼的可讀性。

    儘可能寫出敢在代碼註釋上留下你的名字的代碼!

總結

總之,Stream是Java 8 提供的簡化代碼的神器,合理使用它,能讓你的代碼更加優雅。

尊重勞動成功,轉載註明出處

參考連接:

《Effective Java》3th

Java 8 中的 Streams API 詳解

Java Stream API進階篇

java8 stream groupby後的數據結構是否能夠重構


若是以爲寫得不錯,歡迎關注微信公衆號:逸遊Java ,天天不定時發佈一些有關Java乾貨的文章,感謝關注

相關文章
相關標籤/搜索