《Java8实战》笔记(05):使用流
篩選和切片
Filtering
用謂詞Predicate篩選-filter
List<Dish> vegetarianMenu = menu.stream()//.filter(Dish::isVegetarian)//Predicate<T>做參數.collect(toList());vegetarianMenu.forEach(System.out::println);篩選各異的元素-去重-distinct
// Filtering unique elements List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4); numbers.stream()//.filter(i -> i % 2 == 0)//.distinct()//.forEach(System.out::println);截短流-limit
// Truncating a stream List<Dish> dishesLimit3 = menu.stream()//.filter(d -> d.getCalories() > 300)//.limit(3)//.collect(toList());dishesLimit3.forEach(System.out::println);跳過元素-skip
List<Dish> dishesSkip2 = menu.stream()//.filter(d -> d.getCalories() > 300)//.skip(2)//.collect(toList());dishesSkip2.forEach(System.out::println);映射-map
對流中每一個元素應用函數-map
// map List<String> dishNames = Dish.menu.stream().map(Dish::getName).collect(toList()); System.out.println(dishNames);// map List<String> words = Arrays.asList("Hello", "World"); List<Integer> wordLengths = words.stream().map(String::length).collect(toList()); System.out.println(wordLengths);流的扁平化-flatMap
Mapping
PS.多重map壓扁
任務
給定單詞列表["Hello","World"], 想要返回列表["H","e","l","o","W","r","d"]第一版本
words.stream().map(word -> word.split("")) //返回Stream<String[]>.distinct().collect(toList());這個方法的問題在于,傳遞給map方法的Lambda為每個單詞返回了一個String[](String列 表 )。 因 此 , map 返 回 的 流 實 際 上 是 Stream<String[]> 類 型 的 。 你 真 正 想 要 的 是 用Stream來表示一個字符串流。
解決之道
1.嘗試使用map和Array.stream()
String[] arrayOfWords = {"Goodbye", "World"}; Stream<String> streamOfwords = Arrays.stream(arrayOfWords);//返回的并不是想要的List<String> List<Stream<String>> list = words.stream().map(word -> word.split(""))// 返回Stream<String[]>.map(Arrays::stream)// 返回Stream<Stream<String>> .distinct().collect(toList());2.使用flatMap
List<String> uniqueCharacters =words.stream().map(w -> w.split(""))// 返回Stream<String[]>.flatMap(Arrays::stream)// 返回Stream<String>,把Stream<Stream<String>> 壓成 Stream<String>.distinct().collect(Collectors.toList());一言以蔽之,flatmap方法讓你把一個流中的每個值都換成另一個流,然后把所有的流連接起來成為一個流。
PS. flatmap 能把Stream<Stream> 壓成 Stream
//簡化了一些 words.stream().flatMap((String line) -> Arrays.stream(line.split(""))).distinct().forEach(System.out::println);更多流的扁平化例子
0.給定一個數字列表,如何返回一個由每個數的平方構成的列表呢?例如,給定[1, 2, 3, 4, 5],應該返回[1, 4, 9, 16, 25]
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> squares = numbers.stream().map(n -> n * n).collect(toList());1.給定兩個數字列表,如何返回所有的數對呢?例如,給定列表[1, 2, 3]和列表[3, 4],應該返回[[1, 3], [1, 4], [2, 3], [2, 4], [3, 3], [3, 4]]。為簡單起見,你可以用有兩個元素的數組來代表數對。
List<Integer> numbers1 = Arrays.asList(1, 2, 3); List<Integer> numbers2 = Arrays.asList(3, 4); List<int[]> pairs = numbers1.stream().flatMap(i -> numbers2.stream().map(j -> new int[]{i, j})).collect(toList());2.如何擴展前一個例子,只返回總和能被3整除的數對呢?例如[2, 4]和[3, 3]是可以的。
List<Integer> numbers1 = Arrays.asList(1, 2, 3); List<Integer> numbers2 = Arrays.asList(3, 4); List<int[]> pairs =numbers1.stream().flatMap(i ->numbers2.stream().filter(j -> (i + j) % 3 == 0).map(j -> new int[]{i, j})).collect(toList());查找和匹配-find-match
Finding
檢查謂詞是否至少匹配一個元素-anyMatch
private static boolean isVegetarianFriendlyMenu() {return Dish.menu.stream().anyMatch(Dish::isVegetarian); }檢查謂詞是否匹配所有元素-allMatch
private static boolean isHealthyMenu() {return Dish.menu.stream().allMatch(d -> d.getCalories() < 1000); }檢查謂詞是否不匹配所有元素-noneMatch
private static boolean isHealthyMenu2() {return Dish.menu.stream().noneMatch(d -> d.getCalories() >= 1000); }anyMatch、allMatch和noneMatch這三個操作都用到了所謂的短路,這就是大家熟悉的Java中&&和||運算符短路在流中的版本
查找元素-findAny
Optional<Dish> dish =menu.stream().filter(Dish::isVegetarian).findAny();Optional一覽
Optional<T>類(java.util.Optional)是一個容器類,代表一個值存在或不存在。在上面的代碼中, findAny可能什么元素都沒找到。 Java 8的庫設計人員引入了Optional<T>,這樣就不用返回眾所周知容易出問題的null了。
Optional里面幾種可以迫使你顯式地檢查值是否存在或處理值不存在的情形的方法也不錯。
- isPresent()將在Optional包含值的時候返回true, 否則返回false。
- ifPresent(Consumer<T> block)會在值存在的時候執行給定的代碼塊。我們在第3章
介紹了Consumer函數式接口;它讓你傳遞一個接收T類型參數,并返回void的Lambda表達式。 - T get()會在值存在時返回值,否則拋出一個NoSuchElement異常。
- T orElse(T other)會在值存在時返回值,否則返回一個默認值。
menu.stream().filter(Dish::isVegetarian).findAny().ifPresent(d -> System.out.println(d.getName());
查找第一個元素-findFirst
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> firstSquareDivisibleByThree =someNumbers.stream().map(x -> x * x).filter(x -> x % 3 == 0).findFirst();何時使用findFirst和findAny
你可能會想,為什么會同時有findFirst和findAny呢?答案是并行。找到第一個元素在并行上限制更多。如果你不關心返回的元素是哪個,請使用findAny,因為它在使用并行流時限制較少。
歸約-reduce
Reducing
元素求和
int sum = numbers.stream().reduce(0, (a, b) -> a + b);//orint sum = numbers.stream().reduce(0, Integer::sum);reduce接受兩個參數:
- 一個初始值,這里是0;
- 一個BinaryOperator來將兩個元素結合起來產生一個新值,這里我們用的是lambda (a, b) -> a + b
無初始值
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));為什么它返回一個Optional呢?考慮流中沒有任何元素的情況。reduce操作無法返回其和,因為它沒有初始值。這就是為什么結果被包裹在一個Optional對象里,以表明和可能不存在。
元素求積
int product = numbers.stream().reduce(1, (a, b) -> a * b);最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max); Optional<Integer> min = numbers.stream().reduce(Integer::min);//當然也可以寫成Lambda (x, y) -> x < y ? x : y而不是Integer::min,不過后者比較易讀總數
int count = menu.stream().map(d -> 1).reduce(0, (a, b) -> a + b);long count = menu.stream().count();歸約方法的優勢與并行化
相比于前面寫的逐步迭代求和,使用reduce的好處在于,這里的迭代被內部迭代抽象掉了,這讓內部實現得以選擇并行執行reduce操作。而迭代式求和例子要更新共享變量sum,這不是那么容易并行化的。如果你加入了同步,很可能會發現線程競爭抵消了并行本應帶來的性能提升!這種計算的并行化需要另一種辦法:將輸入分塊,分塊求和,最后再合并起來。但這樣的話代碼看起來就完全不一樣了。
使用流來對所有的元素并行求和時,代碼幾乎不用修改:stream()換成了parallelStream()。
int sum = numbers.parallelStream().reduce(0, Integer::sum);流操作:無狀態和有狀態
諸如map或filter等操作會從輸入流中獲取每一個元素,并在輸出流中得到0或1個結果。這些操作一般都是無狀態的:它們沒有內部狀態(假設用戶提供的Lambda或方法引用沒有內部可變狀態)。但諸如reduce、sum、max等操作需要內部狀態來累積結果。在上面的情況下,內部狀態很小。在我們的例子里就是一個int或double。不管流中有多少元素要處理,內部狀態都是有界的。
相反,諸如sort或distinct等操作一開始都和filter和map差不多——都是接受一個流,再生成一個流(中間操作),但有一個關鍵的區別。從流中排序和刪除重復項時都需要知道先前的歷史。例如,排序要求所有元素都放入緩沖區后才能給輸出流加入一個項目,這一操作的存儲要求是無界的。要是流比較大或是無限的,就可能會有問題(把質數流倒序會做什么呢?它應當返回最大的質數,但數學告訴我們它不存在)。我們把這些操作叫作有狀態操作。
中間操作和終端操作小結
| filter | 中間 | Stream<T> | Predicate<T> | T->boolean |
| distinct | 中間 (有狀態-無界) | Stream<T> | - | - |
| skip | 中間 (有狀態-有界) | Stream<T> | long | - |
| limit | 中間 (有狀態-有界) | Stream<T> | long | - |
| map | 中間 | Stream<R> | Function<T,R> | T->R |
| flatMap | 中間 | Stream<R> | Function<T,Stream<R>> | T->Stream<R> |
| sorted | 中間 (有狀態-無界) | Stream<T> | Comparator<T> | (T,T)->int |
| anyMatch | 終端 | boolean | Predicate<T> | T->boolean |
| noneMatch | 終端 | boolean | Predicate<T> | T->boolean |
| allMatch | 終端 | boolean | Predicate<T> | T->boolean |
| findAny | 終端 | Optional<T> | - | - |
| findFirst | 終端 | Optional<T> | - | - |
| forEach | 終端 | void | Consumer<T> | T->void |
| collect | 終端 | R | Collector<T,A,R> | - |
| reduce | 終端 (有狀態-有界) | Optional<T> | BinaryOperator<T> | (T,T)->T |
| count | 終端 | long | - | - |
付諸實踐
執行交易的交易員
領域:交易員和交易
Trader
Transaction
解答
PuttingIntoPractice
1.找出2011年的所有交易并按交易額排序(從低到高)
List<Transaction> tr2011 = transactions.stream().filter(transaction -> transaction.getYear() == 2011).sorted(comparing(Transaction::getValue)).collect(toList());2.交易員都在哪些不同的城市工作過
List<String> cities = transactions.stream().map(transaction -> transaction.getTrader().getCity()).distinct().collect(toList());//orSet<String> cities =transactions.stream().map(transaction -> transaction.getTrader().getCity()).collect(toSet());3.查找所有來自于劍橋的交易員,并按姓名排序
List<Trader> traders = transactions.stream().map(Transaction::getTrader).filter(trader -> trader.getCity().equals("Cambridge")).distinct().sorted(comparing(Trader::getName)).collect(toList());4.返回所有交易員的姓名字符串,按字母順序排序
String traderStr = transactions.stream().map(transaction -> transaction.getTrader().getName()).distinct().sorted().reduce("", (n1, n2) -> n1 + n2);String traderStr =transactions.stream().map(transaction -> transaction.getTrader().getName()).distinct().sorted().collect(joining());5.有沒有交易員是在米蘭工作的
boolean milanBased = transactions.stream().anyMatch(transaction -> transaction.getTrader().getCity().equals("Milan")); System.out.println(milanBased);6.打印生活在劍橋的交易員的所有交易額
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())).map(Transaction::getValue).forEach(System.out::println);7.所有交易中,最高的交易額是多少
Optional<Integer> highestValue =transactions.stream().map(Transaction::getValue).reduce(Integer::max);8.找到交易額最小的交易
Optional<Transaction> smallestTransaction =transactions.stream().reduce((t1, t2) ->t1.getValue() < t2.getValue() ? t1 : t2);Optional<Transaction> smallestTransaction =transactions.stream().min(comparing(Transaction::getValue));數值流
NumericStreams
可以使用reduce方法計算流中元素的總和.
例如,你可以像下面這樣計算菜單的熱量:
int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);這段代碼的問題是,它有一個暗含的裝箱成本。每個Integer都必須拆箱成一個原始類型,再進行求和。要是可以直接像下面這樣調用sum方法,豈不是更好?
int calories = menu.stream().map(Dish::getCalories).sum();//這里不能編譯,Streams接口沒有定義sum方法但這是不可能的。問題在于map方法會生成一個Stream。雖然流中的元素是Integer類型,但Streams接口沒有定義sum方法。
為什么沒有呢?比方說,你只有一個像menu那樣的Stream,把各種菜加起來是沒有任何意義的。
但不要擔心,Stream API還提供了原始類型流特化,專門支持處理數值流的方法。
原始類型流特化
Java 8引入了三個原始類型特化流接口來解決這個問題:IntStream、DoubleStream和LongStream,分別將流中的元素特化為int、long和double,從而避免了暗含的裝箱成本。每個接口都帶來了進行常用數值歸約的新方法,比如對數值流求和的sum,找到最大元素的max。此外還有在必要時再把它們轉換回對象流的方法。
要記住的是,這些特化的原因并不在于流的復雜性,而是裝箱造成的復雜性——即類似int和Integer之間的效率差異。
映射到數值流-mapToXXX
將流轉換為特化版本的常用方法是mapToInt、 mapToDouble和mapToLong。
int calories = menu.stream().mapToInt(Dish::getCalories)//返回一個IntStream,不是Stream<Integer>.sum();請注意,如果流是空的,sum默認返回0。IntStream還支持其他的方便方法,如max、min、average等。
轉換回對象流-boxed
同樣,一旦有了數值流,你可能會想把它轉換回非特化流。
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); Stream<Integer> stream = intStream.boxed();默認值-OptionalInt
如果你要計算IntStream中的最大元素,就得換個法子了,因為0是錯誤的結果。如何區分沒有元素的流和最大值真的是0的流呢?
Optional可以用Integer、String等參考類型來參數化。對于三種原始流特化,也分別有一個Optional原始類型特化版本:OptionalInt、OptionalDouble和OptionalLong。
例如,要找到IntStream中的最大元素,可以調用max方法,它會返回一個OptionalInt:
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();現在,如果沒有最大值的話,你就可以顯式處理OptionalInt去定義一個默認值了:
int max = maxCalories.orElse(1);數值范圍-range
Java 8引入了兩個可以用于IntStream和LongStream的靜態方法,幫助生成這種范圍:range和rangeClosed。這兩個方法都是第一個參數接受起始值,第二個參數接受結束值。但range是不包含結束值的,而rangeClosed則包含結束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100)//范圍[1,100],IntStream.range(1, 100)范圍為[1,100).filter(n -> n % 2 == 0); System.out.println(evenNumbers.count());數值流應用:勾股數
Pythagorean
勾股數
a^2+b^2=c^2| 3 | 4 | 5 |
| 5 | 12 | 13 |
| 6 | 8 | 10 |
| 7 | 24 | 25 |
表示三元數
new int[]{3, 4, 5};//來表示勾股數(3, 4, 5)篩選成立的組合
怎么知道它是否能形成一組勾股數呢?你需要測試a * a + b * b的平方根是不是整數,也就是說它沒有小數部分——在Java里可以使用expr % 1表示。如果它不是整數,那就是說c不是整數。
filter(b -> Math.sqrt(a*a + b*b) % 1 == 0);生成三元組
stream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0).map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});生成b值
IntStream.rangeClosed(1, 100).filter(b -> Math.sqrt(a*a + b*b) % 1 == 0).mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});生成值
//符合形成直角三角形 Stream<int[]> pythagoreanTriples = IntStream.rangeClosed(1, 100).boxed()flatMap用到泛型,所以不能使用基本類型.flatMap(a ->IntStream.rangeClosed(a, 100).filter(b -> Math.sqrt(a*a + b*b) % 1 == 0).mapToObj(b ->new int[]{a, b, (int)Math.sqrt(a * a + b * b)}));運行代碼
pythagoreanTriples.limit(5).forEach(t ->System.out.println(t[0] + ", " + t[1] + ", " + t[2]));更上一層樓
目前的解決辦法并不是最優的,因為你要求兩次平方根。讓代碼更為緊湊的一種可能的方法是,先生成所有的三元數(a*a, b*b, a*a+b*b),然后再篩選符合條件的
Stream<double[]> pythagoreanTriples2 =IntStream.rangeClosed(1, 100).boxed().flatMap(a ->IntStream.rangeClosed(a, 100).mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)}).filter(t -> t[2] % 1 == 0));構建流
BuildingStreams
由值創建流
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); stream.map(String::toUpperCase).forEach(System.out::println);//你可以使用empty得到一個空流,如下所示: Stream<String> emptyStream = Stream.empty();由數組創建流
int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();由文件生成流
long uniqueWords = 0;//流會自動關閉 try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count(); } catch(IOException e){ }由函數生成流:創建無限流
Stream API提供了兩個靜態方法來從函數生成流:Stream.iterate和Stream.generate。
這兩個操作可以創建所謂的無限流:不像從固定集合創建的流那樣有固定大小的流。由iterate和generate產生的流會用給定的函數按需創建值,因此可以無窮無盡地計算下去!
一般來說,應該使用limit(n)來對這種流加以限制,以避免打印無窮多個值。
迭代-iterate
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);iterate方法接受一個初始值(在這里是0),還有一個依次應用在每個產生的新值上的Lambda(UnaryOperator<T>類型)。
迭代-斐波納契數列
//序列(0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21)... Stream.iterate(new int[]{0, 1},t -> new int[]{t[1], t[0]+t[1]}).limit(20).forEach(t -> System.out.println("(" + t[0] + "," + t[1] +")"));//只想打印正常的斐波納契數列 Stream.iterate(new int[]{0, 1},t -> new int[]{t[1],t[0] + t[1]}).limit(10).map(t -> t[0]).forEach(System.out::println);生成
Stream.generate(Math::random).limit(5).forEach(System.out::println);我們使用的供應源(指向Math.random的方法引用)是無狀態的:它不會在任何地方記錄任何值,以備以后計算使用。但供應源不一定是無狀態的。
你可以創建存儲狀態的供應源,它可以修改狀態,并在為流生成下一個值時使用。
舉個例子,接下來將展示如何利用generate創建斐波納契數列,這樣你就可以和用iterate方法的辦法比較一下。
但很重要的一點是,在并行代碼中使用有狀態的供應源是不安全的。因此下面的代碼僅僅是為了內容完整,應盡量避免使用
IntStream.generate(() -> 1).limit(5).forEach(System.out::println);IntStream twos = IntStream.generate(new IntSupplier(){public int getAsInt(){return 2;} });生成-斐波納契數列
IntSupplier fib = new IntSupplier(){private int previous = 0;private int current = 1;public int getAsInt(){int oldPrevious = this.previous;int nextValue = this.previous + this.current;this.previous = this.current;this.current = nextValue;return oldPrevious;} };IntStream.generate(fib).limit(10).forEach(System.out::println);前面的代碼創建了一個IntSupplier的實例。此對象有可變的狀態:它在兩個實例變量中記錄了前一個斐波納契項和當前的斐波納契項。getAsInt在調用時會改變對象的狀態,由此在每次調用時產生新的值。
相比之下,使用iterate的方法則是純粹不變的:它沒有修改現有狀態,但在每次迭代時會創建新的元組。
請注意,因為你處理的是一個無限流,所以必須使用limit操作來顯式限制它的大小;否則,終端操作(這里是forEach)將永遠計算下去。
同樣,你不能對無限流做排序或歸約,因為所有元素都需要處理,而這永遠也完不成!
小結
- Streams API可以表達復雜的數據處理查詢。常用的流操作總結在表5-1中。
- 使用filter、distinct、skip和limit對流做篩選和切片。
- 使用map和flatMap提取或轉換流中的元素。
- 使用findFirst 和findAny 方法查找流中的元素。你可以用allMatch 、
noneMatch和anyMatch方法讓流匹配給定的謂詞。這些方法都利用了短路:找到結果就立即停止計算;沒有必要處理整個流。 - 你可以利用reduce方法將流中所有的元素迭代合并成一個結果,例如求和或查找最大
元素。 - filter和map等操作是無狀態的,它們并不存儲任何狀態。reduce等操作要存儲狀態才能計算出一個值。sorted和distinct等操作也要存儲狀態,因為它們需要把流中的所有元素緩存起來才能返回一個新的流。這種操作稱為有狀態操作。
- 流有三種基本的原始類型特化:IntStream、DoubleStream和LongStream。它們的操作也有相應的特化。
- 流不僅可以從集合創建,也可從值、數組、文件以及iterate與generate等特定方法創建。
- 無限流是沒有固定大小的流。
總結
以上是生活随笔為你收集整理的《Java8实战》笔记(05):使用流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 剑指offer_01
- 下一篇: Java设计模式(4 / 23):单例模