流操做是Java8提供一個重要新特性,它容許開發人員以聲明性方式處理集合,其核心類庫主要改進了對集合類的 API和新增Stream操做。Stream類中每個方法都對應集合上的一種操做。將真正的函數式編程引入到Java中,能 讓代碼更加簡潔,極大地簡化了集合的處理操做,提升了開發的效率和生產力。java
同時stream不是一種數據結構,它只是某種數據源的一個視圖,數據源能夠是一個數組,Java容器或I/O channel等。在Stream中的操做每一次都會產生新的流,內部不會像普通集合操做同樣馬上獲取值,而是惰性 取值,只有等到用戶真正須要結果的時候纔會執行。而且對於如今調用的方法,自己都是一種高層次構件,與線程模型無關。所以在並行使用中,開發者們無需再去操 心線程和鎖了。Stream內部都已經作好了。sql
若是剛接觸流操做的話,可能會感受不太舒服。其實理解流操做的話能夠對比數據庫操做。把流的操做理解爲對數據庫中 數據的查詢操做 集合 = 數據表 元素 = 表中的每條數據 屬性 = 每條數據的列 流API = sql查詢
Stream流接口中定義了許多對於集合的操做方法,總的來講能夠分爲兩大類:中間操做和終端操做。數據庫
中間操做:會返回一個流,經過這種方式能夠將多箇中間操做鏈接起來,造成一個調用鏈,從而轉換爲另外 一個流。除非調用鏈後存在一個終端操做,不然中間操做對流不會進行任何結果處理。編程
終端操做:會返回一個具體的結果,如boolean、list、integer等。數組
一、篩選數據結構
對於集合的操做,常常性的會涉及到對於集中符合條件的數據篩選,Stream中對於數據篩選兩個常見的API: filter(過濾)、distinct(去重)app
1.1基於filter()實現數據過ide
該方法會接收一個返回boolean的函數做爲參數,終返回一個包括全部符合條件元素的流。函數式編程
案例:獲取全部年齡20歲如下的學生函數
/** * @author 我是七月呀 * @date 2020/12/22 */ public class FilterDemo { public static void main(String[] args) { //獲取全部年齡20歲如下的學生 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三","M",true)); students.add(new Student(1,18,"李四","M",false)); students.add(new Student(1,21,"王五","F",true)); students.add(new Student(1,20,"趙六","F",false)); students.stream().filter(student -> student.getAge()<20); } }
源碼解析
此處能夠看到filter方法接收了Predicate函數式接口。
首先判斷predicate是否爲null,若是爲null,則拋出NullPointerException;構建Stream,重寫opWrapsink方法。參數flags:下一個sink的標誌位,供優化使用。參數sink:下一個sink,經過此參數將sink構形成單鏈。此時流已經構建好,可是由於begin()先執行,此時是沒法肯定流中後續會存在多少元素的,因此傳遞-1,表明沒法肯定。最後調用Pridicate中的test,進行條件判斷,將符合條件數據放入流中。
1.2基於distinct實現數據去重
/** * @author 我是七月呀 * @date 2020/12/22 */ public class DistinctDemo { public static void main(String[] args) { List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2); integers.stream().distinct().collect(Collectors.toList()); } }
源碼解析
根據其源碼,咱們能夠知道在distinct()內部是基於LinkedHashSet對流中數據進行去重,並終返回一個新的流。
二、切片
2.1基於limit()實現數據截取
該方法會返回一個不超過給定長度的流
案例:獲取數組的前五位
/** * @author 我是七月呀 * @date 2020/12/22 */ public class LimitDemo { public static void main(String[] args) { //獲取數組的前五位 List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2); integers.stream().limit(5); } }
源碼解析:
對於limit方法的實現,它會接收截取的長度,若是該值小於0,則拋出異常,不然會繼續向下調用 SliceOps.makeRef()。該方法中this表明當前流,skip表明須要跳過元素,比方說原本應該有4個元素,當跳過元素 值爲2,會跳過前面兩個元素,獲取後面兩個。maxSize表明要截取的長度
在makeRef方法中的unorderedSkipLimitSpliterator()中接收了四個參數Spliterator,skip(跳過個數)、limit(截取 個數)、sizeIfKnown(已知流大小)。若是跳過個數小於已知流大小,則判斷跳過個數是否大於0,若是大於則取截取 個數或已知流大小-跳過個數的二者小值,不然取已知流大小-跳過個數的結果,做爲跳過個數。
後對集合基於跳過個數和截取個數進行切割。
2.2基於skip()實現數據跳過
案例:從集合第三個開始截取5個數據
/** * @author 我是七月呀 * @date 2020/12/22 */ public class LimitDemo { public static void main(String[] args) { //從集合第三個開始截取5個數據 List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2); List<Integer> collect = integers.stream().skip(3).limit(5).collect(Collectors.toList()); collect.forEach(integer -> System.out.print(integer+" ")); } }
結果4 4 5 5 6
案例:先從集合中截取5個元素,而後取後3個
/** * @author 我是七月呀 * @date 2020/12/22 */ public class LimitDemo { public static void main(String[] args) { //先從集合中截取5個元素,而後取後3個 List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2); List<Integer> collect = integers.stream().limit(5).skip(2).collect(Collectors.toList()); collect.forEach(integer -> System.out.print(integer+" ")); } }
結果:3 4 4
源碼分析:
在skip方法中接收的n表明的是要跳過的元素個數,若是n小於0,拋出非法參數異常,若是n等於0,則返回當前 流。若是n小於0,纔會調用makeRef()。同時指定limit參數爲-1.
此時能夠發現limit和skip都會進入到該方法中,在肯定limit值時,若是limit<0,則獲取已知集合大小長度-跳過的長度。最終進行數據切割。
三、映射
在對集合進行操做的時候,咱們常常會從某些對象中選擇性的提取某些元素的值,就像編寫sql同樣,指定獲取表 中特定的數據列
#指定獲取特定列 SELECT name FROM student
在Stream API中也提供了相似的方法,map()。它接收一個函數做爲方法參數,這個函數會被應用到集合中每個 元素上,並終將其映射爲一個新的元素。
案例:獲取全部學生的姓名,並造成一個新的集合
/** * @author 我是七月呀 * @date 2020/12/22 */ public class MapDemo { public static void main(String[] args) { //獲取全部學生的姓名,並造成一個新的集合 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三","M",true)); students.add(new Student(1,18,"李四","M",false)); students.add(new Student(1,21,"王五","F",true)); students.add(new Student(1,20,"趙六","F",false)); List<String> collect = students.stream().map(Student::getName).collect(Collectors.toList()); collect.forEach(s -> System.out.print(s + " ")); } }
結果:張三 李四 王五 趙六
源碼解析:
內部對Function函數式接口中的apply方法進行實現,接收一個對象,返回另一個對象,並把這個內容存入當前 流中,後返回
四、匹配
在平常開發中,有時還須要判斷集合中某些元素是否匹配對應的條件,若是有的話,在進行後續的操做。在 Stream API中也提供了相關方法供咱們進行使用,如anyMatch、allMatch等。他們對應的就是&&和||運算符。
4.1基於anyMatch()判斷條件至少匹配一個元素
anyMatch()主要用於判斷流中是否至少存在一個符合條件的元素,它會返回一個boolean值,而且對於它的操做, 通常叫作短路求值
案例:判斷集合中是否有年齡小於20的學生
/** * @author 我是七月呀 * @date 2020/12/22 */ public class AnyMatchDemo { public static void main(String[] args) { //判斷集合中是否有年齡小於20的學生 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三","M",true)); students.add(new Student(1,18,"李四","M",false)); students.add(new Student(1,21,"王五","F",true)); students.add(new Student(1,20,"趙六","F",false)); if(students.stream().anyMatch(student -> student.getAge() < 20)){ System.out.println("集合中有年齡小於20的學生"); }else { System.out.println("集合中沒有年齡小於20的學生"); } } }
根據上述例子能夠看到,當流中只要有一個符合條件的元素,則會馬上停止後續的操做,當即返回一個布爾值,無需遍歷整個流。
源碼解析:
內部實現會調用makeRef(),其接收一個Predicate函數式接口,並接收一個枚舉值,該值表明當前操做執行的是 ANY。
若是test()抽象方法執行返回值==MatchKind中any的stopOnPredicateMatches,則將stop中斷置爲true,value 也爲true。並終進行返回。無需進行後續的流操做。
4.2基於allMatch()判斷條件是否匹配全部元素
allMatch()的工做原理與anyMatch()相似,可是anyMatch執行時,只要流中有一個元素符合條件就會返回true, 而allMatch會判斷流中是否全部條件都符合條件,所有符合纔會返回true
案例:判斷集合全部學生的年齡是否都小於20
/** * @author 我是七月呀 * @date 2020/12/22 */ public class AllMatchDemo { public static void main(String[] args) { //判斷集合全部學生的年齡是否都小於20 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三","M",true)); students.add(new Student(1,18,"李四","M",false)); students.add(new Student(1,21,"王五","F",true)); students.add(new Student(1,20,"趙六","F",false)); if(students.stream().allMatch(student -> student.getAge() < 20)){ System.out.println("集合全部學生的年齡都小於20"); }else { System.out.println("集合中有年齡大於20的學生"); } } }
源碼解析:與anyMatch相似,只是其枚舉參數的值爲ALL
五、查找
對於集合操做,有時須要從集合中查找中符合條件的元素,Stream中也提供了相關的API,findAny()和 findFirst(),他倆能夠與其餘流操做組合使用。findAny用於獲取流中隨機的某一個元素,findFirst用於獲取流中的 第一個元素。至於一些特別的定製化需求,則須要自行實現。
5.1基於findAny()查找元素
案例:findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束
/** * @author 我是七月呀 * @date 2020/12/22 */ public class FindAnyDemo { public static void main(String[] args) { //findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三1","M",true)); students.add(new Student(1,18,"張三2","M",false)); students.add(new Student(1,21,"張三3","F",true)); students.add(new Student(1,20,"張三4","F",false)); students.add(new Student(1,20,"張三5","F",false)); students.add(new Student(1,20,"張三6","F",false)); Optional<Student> student1 = students.stream().filter(student -> student.getSex().equals("F")).findAny(); System.out.println(student1.toString()); } }
結果:Optional[Student{id=1, age=21, name='張三3', sex='F', isPass=true}]
此時咱們將其循環100次
/** * @author 我是七月呀 * @date 2020/12/22 */ public class FindAnyDemo { public static void main(String[] args) { //findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三1","M",true)); students.add(new Student(1,18,"張三2","M",false)); students.add(new Student(1,21,"張三3","F",true)); students.add(new Student(1,20,"張三4","F",false)); students.add(new Student(1,20,"張三5","F",false)); students.add(new Student(1,20,"張三6","F",false)); for (int i = 0; i < 100; i++) { Optional<Student> student1 = students.stream().filter(student -> student.getSex().equals("F")).findAny(); System.out.println(student1.toString()); } } }
結果:
因爲數量較大,只截取了部分截圖,所有都是同樣的,不行的小夥伴能夠本身測試一下
這時候咱們改成串行流在執行一下
/** * @author 我是七月呀 * @date 2020/12/22 */ public class FindAnyDemo { public static void main(String[] args) { //findAny用於獲取流中隨機的某一個元素,而且利用短路在找到結果時,當即結束 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三1","M",true)); students.add(new Student(1,18,"張三2","M",false)); students.add(new Student(1,21,"張三3","F",true)); students.add(new Student(1,20,"張三4","F",false)); students.add(new Student(1,20,"張三5","F",false)); students.add(new Student(1,20,"張三6","F",false)); for (int i = 0; i < 100; i++) { Optional<Student> student1 = students.parallelStream().filter(student -> student.getSex().equals("F")).findAny(); System.out.println(student1.toString()); } } }
結果:
如今咱們經過源碼解析來分析下這是爲何?
根據這一段源碼介紹,findAny對於同一數據源的屢次操做會返回不一樣的結果。可是,咱們如今的操做是串行的, 因此在數據較少的狀況下,通常會返回第一個結果,可是若是在並行的狀況下,那就不能確保返回的是第一個了。 這種設計主要是爲了獲取更加高效的性能。並行操做後續會作詳細介紹。
傳遞參數,指定沒必要須獲取第一個元素
在該方法中,主要用於判斷對於當前的操做執行並行仍是串行。
在該方法中的wrapAndCopyInto()內部作的會判斷流中是否存在符合條件的元素,若是有的話,則會進行返回。結 果終會封裝到Optional中的IsPresent中。
總結:當爲串行流且數據較少時,獲取的結果通常爲流中第一個元素,可是當爲並流行的時 候,則會隨機獲取。
5.2基於findFirst()查找元素
findFirst使用原理與findAny相似,只是它不管串行流仍是並行流都會返回第一個元素,這裏不作詳解
六、歸約
到如今截止,對於流的終端操做,咱們返回的有boolean、Optional和List。可是在集合操做中,咱們常常會涉及 對元素進行統計計算之類的操做,如求和、求大值、小值等,從而返回不一樣的數據結果。
6.1基於reduce()進行累積求和
案例:對集合中的元素求和
/** * @author 我是七月呀 * @date 2020/12/22 */ public class ReduceDemo { public static void main(String[] args) { List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2); Integer reduce = integers.stream().reduce(0, (integer1, integer2) -> integer1 + integer2); System.out.println(reduce); } }
結果:53
在上述代碼中,在reduce裏的第一個參數聲明爲初始值,第二個參數接收一個lambda表達式,表明當前流中的兩 個元素,它會反覆相加每個元素,直到流被歸約成一個終結果
Integer reduce = integers.stream().reduce(0,Integer::sum);
優化成這樣也是能夠的。固然,reduce還有一個不帶初始值參數的重載方法,可是要對返回結果進行判斷,由於若是流中沒有任何元素的話,可能就沒有結果了。具體方法以下所示
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2); Optional<Integer> reduce = integers.stream().reduce(Integer::sum); if(reduce.isPresent()){ System.out.println(reduce); }else { System.out.println("數據有誤"); }
源碼解析:兩個參數的reduce方法
在上述方法中,對於流中元素的操做,當執行第一個元素,會進入begin方法,將初始化的值給到state,state就 是後的返回結果。並執行accept方法,對state和第一個元素根據傳入的操做,對兩個值進行計算。並把終計 算結果賦給state。
當執行到流中第二個元素,直接執行accept方法,對state和第二個元素對兩個值進行計算,並把終計算結果賦 給state。後續依次類推。
能夠按照下述代碼進行理解
T result = identity; for (T element : this stream){ result = accumulator.apply(result, element) } return result;
源碼解析:單個參數的reduce方法
在這部分實現中,對於匿名內部類中的empty至關因而一個開關,state至關於結果。
對於流中第一個元素,首先會執行begin()將empty置爲true,state爲null。接着進入到accept(),判斷empty是否 爲true,若是爲true,則將empty置爲false,同時state置爲當前流中第一個元素,當執行到流中第二個元素時, 直接進入到accpet(),判斷empty是否爲true,此時empty爲false,則會執行apply(),對當前state和第二個元素進 行計算,並將結果賦給state。後續依次類推。
當整個流操做完以後,執行get(), 若是empty爲true,則返回一個空的Optional對象,若是爲false,則將後計算 完的state存入Optional中。
能夠按照下述代碼進行理解:
boolean flag = false; T result = null; for (T element : this stream) { if (!flag) { flag = true; result = element; }else{ result = accumulator.apply(result, element); } } return flag ? Optional.of(result) : Optional.empty();
6.2獲取流中元素的最大值、最小值
案例:獲取集合中元素的最大值、最小值
/** * @author 我是七月呀 * @date 2020/12/22 */ public class MaxDemo { public static void main(String[] args) { List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2); /** * 獲取集合中的最大值 */ //方法一 Optional<Integer> max1 = integers.stream().reduce(Integer::max); if(max1.isPresent()){ System.out.println(max1); } //方法二 Optional<Integer> max2 = integers.stream().max(Integer::compareTo); if(max2.isPresent()){ System.out.println(max2); } /** * 獲取集合中的最小值 */ //方法一 Optional<Integer> min1 = integers.stream().reduce(Integer::min); if(min1.isPresent()){ System.out.println(min1); } //方法二 Optional<Integer> min2 = integers.stream().min(Integer::compareTo); if(min2.isPresent()){ System.out.println(min2); } } }
結果:
Optional[8] Optional[8] Optional[1] Optional[1]
七、收集器
經過使用收集器,可讓代碼更加方便的進行簡化與重用。其內部主要核心是經過Collectors完成更加複雜的計算 轉換,從而獲取到終結果。而且Collectors內部提供了很是多的經常使用靜態方法,直接拿來就能夠了。比方說: toList。
/** * @author 我是七月呀 * @date 2020/12/22 */ public class CollectDemo { public static void main(String[] args) { ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三","M",true)); students.add(new Student(1,18,"李四","M",false)); students.add(new Student(1,21,"王五","F",true)); students.add(new Student(1,20,"趙六","F",false)); //經過counting()統計集合總數 方法一 Long collect = students.stream().collect(Collectors.counting()); System.out.println(collect); //結果 4 //經過count()統計集合總數 方法二 long count = students.stream().count(); System.out.println(count); //結果 4 //經過maxBy求最大值 Optional<Student> collect1 = students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge))); if(collect1.isPresent()){ System.out.println(collect1); } //結果 Optional[Student{id=1, age=21, name='王五', sex='F', isPass=true}] //經過max求最大值 Optional<Student> max = students.stream().max(Comparator.comparing(Student::getAge)); if(max.isPresent()){ System.out.println(max); } //結果 Optional[Student{id=1, age=21, name='王五', sex='F', isPass=true}] //經過minBy求最小值 Optional<Student> collect2 = students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge))); if(collect2.isPresent()){ System.out.println(collect2); } //結果 Optional[Student{id=1, age=18, name='李四', sex='M', isPass=false}] //經過min求最小值 Optional<Student> min = students.stream().min(Comparator.comparing(Student::getAge)); if(min.isPresent()){ System.out.println(min); } //結果 Optional[Student{id=1, age=18, name='李四', sex='M', isPass=false}] //經過summingInt()進行數據彙總 Integer collect3 = students.stream().collect(Collectors.summingInt(Student::getAge)); System.out.println(collect3); //結果 78 //經過averagingInt()進行平均值獲取 Double collect4 = students.stream().collect(Collectors.averagingInt(Student::getAge)); System.out.println(collect4); //結果 19.5 //經過joining()進行數據拼接 String collect5 = students.stream().map(Student::getName).collect(Collectors.joining()); System.out.println(collect5); //結果 張三李四王五趙六 //複雜結果的返回 IntSummaryStatistics collect6 = students.stream().collect(Collectors.summarizingInt(Student::getAge)); double average = collect6.getAverage(); long sum = collect6.getSum(); long count1 = collect6.getCount(); int max1 = collect6.getMax(); int min1 = collect6.getMin(); } }
八、分組
在數據庫操做中,常常會經過group by對查詢結果進行分組。同時在平常開發中,也常常會涉及到這一類操做, 如經過性別對學生集合進行分組。若是經過普通編碼的方式須要編寫大量代碼且可讀性很差。
對於這個問題的解決,java8也提供了簡化書寫的方式。經過 Collectors。groupingBy()便可。
//經過性別對學生進行分組 Map<String, List<Student>> collect = students.stream().collect(Collectors.groupingBy(Student::getSex));
結果 { F=[Student{id=1, age=21, name='王五', sex='F', isPass=true}, Student{id=1, age=20, name='趙六', sex='F', isPass=false}], M=[Student{id=1, age=19, name='張三', sex='M', isPass=true}, Student{id=1, age=18, name='李四', sex='M', isPass=false}] }
8.1多級分組
剛纔已經使用groupingBy()完成了分組操做,可是隻是經過單一的sex進行分組,那如今若是需求發生改變,還要 按照是否及格進行分組,可否實現?答案是能夠的。對於groupingBy()它提供了兩個參數的重載方法,用於完成這 種需求。
這個重載方法在接收普通函數以外,還會再接收一個Collector類型的參數,其會在內層分組(第二個參數)結果,傳 遞給外層分組(第一個參數)做爲其繼續分組的依據。
//現根據是否經過考試對學生分組,在根據性別分組 Map<String, Map<Boolean, List<Student>>> collect1 = students.stream().collect(Collectors.groupingBy(Student::getSex, Collectors.groupingBy(Student::getPass)));
結果: { F={ false=[Student{id=1, age=20, name='趙六', sex='F', isPass=false}], true=[Student{id=1, age=21, name='王五', sex='F', isPass=true}] }, M={ false=[Student{id=1, age=18, name='李四', sex='M', isPass=false}], true=[Student{id=1, age=19, name='張三', sex='M', isPass=true}]} }
8.2多級分組變形
在平常開發中,咱們頗有可能不是須要返回一個數據集合,還有可能對數據進行彙總操做,比方說對於年齡18歲 的經過的有多少人,未及格的有多少人。所以,對於二級分組收集器傳遞給外層分組收集器的能夠任意數據類型, 而不必定是它的數據集合。
//根據年齡進行分組,獲取並彙總人數 Map<Integer, Long> collect2 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.counting())); System.out.println(collect2);
結果:{18=1, 19=1, 20=1, 21=1}
//要根據年齡與是否及格進行分組,並獲取每組中年齡的學生 Map<Integer, Map<Boolean, Student>> collect3 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.groupingBy(Student::getPass, Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Student::getAge)), Optional::get)))); System.out.println(collect3.toString());
結果:{ 18={false=Student{id=1, age=18, name='李四', sex='M', isPass=false}}, 19={true=Student{id=1, age=19, name='張三', sex='M', isPass=true}}, 20={false=Student{id=1, age=20, name='趙六', sex='F', isPass=false}}, 21={true=Student{id=1, age=21, name='王五', sex='F', isPass=true}}}