java8-stream
Stream API
Stream 是java8中處理集合的關鍵抽象概念,他可以制定你希望對集合進行的操作,但是將執行操作的時間交給具體實現來決定。
要點
- 可以從集合、數組、生成器或者迭代器中創建Stream。
- 使用filter來選擇元素,使用map來改變元素。
- 其他改變Stream的操作包括limit、distinct、sorted。
- 要從Stream中獲得結果,請使用reduction操作符,例如count、max、min、findFirst或findAny、其中一些方法會返回一個Optional值。
- Optional類型的目的是為了安全的替代使用null值。要想安全的使用,需要借助ifPresent和orElse方法。
- 可以收集集合、數組、字符串或map中的Stream結果。
- Collectors類的groupingBy和partitioningBy允許對Stream中的內容分組,並獲取每個組的結果。
- java8對原始類型int、long和double提供了專門的Stream。
- 當使用並行Stream時,請確保不帶有副作用,並考慮放棄排序約束。
Stream Example:
@Test//*不使用Stream的方法*
public void testCount(){
List<String> words = getWords();
int count = 0;
for (String w: words) {
if(w.length()>10)count++;
}
System.out.println(count);
}
@Test//*使用Stream*
public void testStream(){
List<String> words = getWords();
long count=0;
count =words.stream().filter(w->w.length()>10).count();
System.out.println(count);
}
public List<String> getWords(){
List<String> words = new ArrayList<String>();
for (int i = 0; i < 20; i++) {
words.add("aaabbbccc"+i);
}
return words;
}
可以看出來Stream給我們帶來了很大的方便性和可讀性
當使用Stream時,通過三個階段來建立一個操作流水線
1. 創建一個Stream
2. 在一個或多個步驟中,制定將初始Stream轉換為另一個Stream的中間操作。
3. 使用一個終止操作來產生一個結果。該操作會強制他之前的延遲操作立即執行。在這之后,該Stream就不會在被使用了。
創建Stream
java8在Collection接口中添加了stream方法,可以將任何一個集合轉化為一個Stream。
Stream<String> words = Stream.of(contents.split(","));
//split方法返回一個String[]數組
//of方法接受可變長度的參數,因此可以構造一個含有任何個參數的Stream
Stream<String> words = Stream.of("hello","world","java8","lambda");
使用Arrays.stream(array,from,to)方法將數組的一部分轉化為Stream
如果要創建一個不含任何元素的Stream,可以使用靜態的Stream.empty方法:
Stream<String> words = Stream.empty();
Stream接口有兩個用來創建無限Stream的靜態方法。generate方法接受一個無參函數,
@Test
public void testInit(){
Stream<String> echos = Stream.generate(()->"Echo");
Stream<Double> randoms= Stream.generate(Math::random);
}
Java8中添加了許多能夠產生Stream的方法。比如 Pattern類添加了一個splitAsStream方法。
Stream<String> words = Pattern.complie(",").splitAsStream(contents);
filter、map和flatMap方法
- filter方法的參數是一個Predicate對象–即一個從T到boolean的函數。
Stream<String> words = getWords().stream().filter(w->w.length()>10);
- 當對一個流中的值進行某種轉換時,使用map方法。
//將單詞轉換為大寫 Stream<String> words = getWords().stream().map(String::toUpperCase); //獲得單詞的第一個字母 Stream<Character> wordsFirstChar = getWords().stream().map(s->s.charAt(0));
- flatMap
@Test public void testMapAndFlatMap(){ Stream<Stream<Character>> chars = getWords().stream().map(w->getCharacter(w)); //這里可以用flatMap //flatMap 會對每個對象調用getCharacter()方法,並展開結果 Stream<Character> charss=getWords().stream().flatMap(w->getCharacter(w)); } public Stream<Character> getCharacter(String s){ List<Character> chars = new ArrayList<Character>(); for (char c: s.toCharArray()) { chars.add(c); } return chars.stream(); }
limit()、skip()和concat():提取子流和組合流
- Stream.limit(n)會返回一個包含n個元素的新流如果原始流長度小於n,則返回原始流
//返回1個具有100個隨機數的流 Stream<Double> randoms100 = Stream.generate(Math::random).limit(100);
- Stream.skip(n)會跳過n個元素。
//跳過第一個單詞組成流 Stream<String> wordsWithoutFirst = getWords().stream().skip(1); System.out.println(wordsWithoutFirst.count());//打印為 19 略過了第一個元素
- Stream.concat() 靜態方法。合並兩個流。
合並流的第一個應該為無限狀態
Stream<String> total = Stream.concat(getWords().stream().skip(10), getWords().stream()); System.out.println(total.count());
distinct()、Sorted():有狀態轉換
上面的流轉換都是無狀態的。也就是獲得的結果不依賴前面的元素。java8也提供了有狀態的轉換。例如distinct()
distinct()方法需要記錄已存儲的單詞。然后判斷重復及是否添加
Stream<String> unique = Arrays.asList("todo","todo","todo","today").stream().distinct();
System.out.println(unique.count());//2
sorted方法排序必須遍歷整個流。並在產生任何元素之前對他進行排序。
Stream<String> sortWords = getWords().stream().sorted(Comparator.comparing(String::length).reversed());
Collections.sort方法會對原有集合進行排序,Stream.sorted方法會返回一個新的已排序的流;
聚合方法–終止流操作
聚合方法都是終止操作。當一個流應用了終止操作以后,他就不能在進行其他操作了。
聚合方法主要有:count(),max(),min(),findFirst,findAny,allMatch,noneMatch等
//找到排序最大的單詞 min同理
Optional<String> max = getWords().stream().max(String::compareToIgnoreCase);
if(max.isPresent()){
System.out.println(max.get());
}
//找到所有的以a開頭的單詞
Optional<String> words = getWords().stream().filter(s->s.startsWith("a")).findAny();
//找到所有的以a開頭的第一個單詞
Optional<String> wordFirst = getWords().stream().filter(s->s.startsWith("a")).findFirst();
allMatch,noneMatch分別表示所有元素和沒有元素匹配predicate返回true。
Optional類型
- Optional對象或者是對一個T類型對象的封裝,或表示不是任何對象。他比一般指向T類型的引用更安全,因為他不會返回null–但也是在正確的使用前提下。
如果存在被封裝的對象,那么 get方法會返回該對象,否則會拋出一個NoSuchElementException.所以,可以用isPresend()來判斷一下。
Optional<String> max = getWords().stream().max(String::compareToIgnoreCase); if(max.isPresent()){ //do something }
- 高效使用Optional的關鍵在於,使用一個或接受正確值、或者返回另一個替代值的方法
String result = max.orElse("");
如果沒有值則用“”替代。
此外orElse還有兩個方法
orElseGet(Lambda):可以調用代碼計算值
orElseThrow(Exception):拋出異常 - 創建optional
利用Optional的靜態方法可以創建對象,比如Optional.of(result)或者Optional.empty();
ofNullable(obj)方法會跟據obj的值來進行創建optional,如果obj==null,那么這個方法間接等於Optional.empty(),
如果obj!=null 那么 間接等於Optional.of(obj);
+收集結果
可以通過iterator方法生成一個迭代器,用來訪問結果的元素,或者使用stream.toArray(),他會返回一個Object[]數組。如果想獲得具體類型的。
可以把類型傳遞給數組的構造函數
String[] result = words.toArray(String[]::new);
如果要把結果收集到一個HashSet中,如果這個過程是並行的,那么不能將元素放到一個單獨的Hashset中,因為HashSet不是線程安全的。所以要使用collect方法,他接受三個參數
1. 一個能創建目標類型實例的方法,例如hashset的構造函數。
2. 一個江元素添加到目標中的方法,例如一個add方法。
3. 一個將兩個對象整合到一起的方法,例如addALl方法。
HashSet<String> result = stream.collect(HashSet::new,HashSet::add,HashSet::addAll);
在實際中,通常不需要這么做,因為Collector借口已經提供了這三個方法,
List<String> result = getWords().stream().collect(Collectors.toList()); List<String> result = getWords().stream().collect(Collectors.toSet()); TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new));
Collectors還有許多方法,可以自己去看一下api
原始流類型
主要有IntStream、DoubleStream、LongStream等。
IntStream:short、char、byte、boolean
DoubleStream:float
LongStream:long
IntStream intStream = IntStream.of(1,2,3,4);
intStream = Arrays.stream(value,from,to); //value 為一個int[]數組
- 原始流和對象流轉換
當有一個對象流的時候,可以通過mapToInt、mapToLong或者mapToDouble方法將他們轉換為一個原始流。
Stream<String> words = getWords();
IntStream length = words.mapToInt(String::length);
將一個原始流轉換為對象流時,可以使用boxed方法。
Stream<Integer> integers = IntStream.range(0,100).boxed();
並行流
默認情況下,流操作會創建一個串行流,方法Collection.parallelStream()除外。parallel方法可以將任意一個串行流變為一個並行流。
Stream<String> words = getWords().stream().parallel();
只要在終止方法執行時,流處於並行模式,那么所有的延遲執行的流操作就都會被並行執行。
當並行運行流時,他應返回與串行運行時相同的結果。這些操作都是無狀態的,因此可以以任意順序被執行
下面是一個反例,用來提示不應該這么做,假設你需要績一個字符串流中所有短單詞的總數:
int[] shortWords = new int[11];
Stream<String> words = Arrays.asList("java","c++","javac","eclipse","tomcat","helloworld").stream();
words.parallel().forEach(s->{if(s.length()<5)shortWords[s.length()]++;});
System.out.println(Arrays.toString(shortWords));
傳遞給foreach的函數會在多個線程中並發運行,來更新一個共享數組,這是一個經典的條件競爭。如果多運行幾次,很有可能會得到不同的總數,並且沒有一個是對的。
使用並行流操作的函數都應該是線程安全的。
當執行一個流(串行和並行)操作時,並不會修改流底層的集合,流不會收集他自己的數據–這些數據總是存在另一個集合中。如果想要修改原有的集合,那么就無法定義流操作的輸出。
下面這么做是可以的。
List<String> words = ...;
Stream<String> wordStream=words.stream();
words.add("end");
long n = wordStream.distinct().count();
下面是不可以的,因為干擾了原集合
Stream<String> wordStream = words.stream();
wordStream.forEach(s->if(s.length<12)words.remove(s));