流式編程做爲Java 8的亮點之一,是繼Java 5
以後對集合的再一次升級,能夠說Java 8
幾大特性中,Streams API
是做爲Java 函數式的主角來設計的,誇張的說,有了Streams API
以後,萬物皆可一行代碼。html
Stream
被翻譯爲流,它的工做過程像將一瓶水導入有不少過濾閥的管道同樣,水每通過一個過濾閥,便被操做一次,好比過濾,轉換等,最後管道的另一頭有一個容器負責接收剩下的水。java
示意圖以下:sql
首先經過source
產生流,而後依次經過一些中間操做,好比過濾,轉換,限制等,最後結束對流的操做。編程
Stream
也能夠理解爲一個更加高級的迭代器,主要的做用即是遍歷其中每個元素。segmentfault
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
?
咱們能夠發現,當咱們使用一個流的時候,主要包括三個步驟:
獲取流的方式有多種,對於常見的容器(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
Intermediate
和Terminal
徹底能夠按照上圖的流程圖理解,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();
值得一說的是這個Comparator
在Java 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
toArray
和List##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個參數,分別是
- 第一個參數:分組按照什麼分類
- 第二個參數:分組最後用什麼容器保存返回(當只有兩個參數是,此參數默認爲
HashMap
)- 第三個參數:按照第一個參數分類後,對應的分類的結果如何收集
有時候單參數的
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));
值得注意的一點是,
IntStream
,LongStream
,DoubleStream
是沒有collect()
方法的,由於對於基本數據類型,要進行裝箱,拆箱操做,SDK並無將它放入流中,對於基本數據類型流,咱們只能將其toArray()
瞭解了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
java8 stream groupby後的數據結構是否能夠重構
若是以爲寫得不錯,歡迎關注微信公衆號:逸遊Java ,天天不定時發佈一些有關Java乾貨的文章,感謝關注