Stream是Java8中處理集合的關鍵抽象概念,你可以指定對集合的操作,但是真正執行時間由具體實現決定。
stream遵循的是“做什麼”,而不是“怎麼做”。使用迭代循環一開始就需要指定如何計算,失去了優化的機會。
2.1 從迭代器到stream操作
一個基本的操作:
List<String> list=new ArrayList<>();
list.add("asgjasiljgoa");
list.add("agsd");
list.add("agarhgadfhadfhahafhga");
long n=list.stream().filter(s->s.length()>10).count();
System.out.println(n);
集合調用stream方法得到Stream對象,filter方法進行元素選擇,返回新的流對象,count方法是執行和終止流操作,返回元素個數。
Stream與集合的區別:
a,stream自己不存儲元素,元素可能存儲在底層集合中,或者根據需要產生。
b,stream操作符不改變源對象,返回一個新的stream。
c,操作符可能是延遲執行的,需要結果的時候才執行。
將上面的stream方法改變一下,即可使用並行流:
long n=list.parallelStream().filter(s->s.length()>10).count();
stream的使用:
a,創建一個Stream
b,指定將Stream轉換到新流的操作
c,指定終止操作產生一個結果。該操作使前面的延遲操作立即執行。
2.2 創建Stream
對於集合來說,調用stream方法即可。
對於數組,使用靜態方法Stream.of()或Arrays.stream()
int[] a =new int[]{1,2,3,4,5};
long x=Stream.of(a).count();
long y=Arrays.stream(a, 0, a.length).count();
of接收可變參數,可以一個個輸入多個值,以上x=1,y=5。第二個方法將數組的一部分轉換爲stream
要創建一個空的Stream,
Stream<String> s=Stream.empty();
要創建無限流:
Stream.generate(()->"hehe");
Stream.iterate(1, e->e+1);
以上創建的是一個常量流和一個1,2,3,4...序列。
如想按照正則匹配切割一個字符串:返回切割後的流
Pattern.compile(regex).splitAsStream(content);
要將文件所有行轉換爲一個流:
try {
Files.lines(Paths.get(url));
} catch (IOException e1) {
e1.printStackTrace();
}
2.3 filter,map和flatMap方法
在前面代碼中已經提到過filter方法,它接收Predicate類型參數,即一種過濾規則,根據測試條件返回true or false
map:對Stream中的元素執行某種映射操作,接收一個Function類型參數。如:
list.stream().map(String::toUpperCase)
它將裏面的字母變成大寫後返回新的流。
flatMap:和map類似,不過它會合並結果,即Stream<T>,如果T也是一個Stream,那麼它會展開裏面的stream然後合併成一個stream。如下:
static Stream<Character> fun(String s){
List<Character> list=new ArrayList<>();
for(Character c:s.toCharArray())
list.add(c);
return list.stream();
}
Stream<Stream<Character>> s1=list.stream().map(s->fun(s));
Stream<Character> s2=list.stream().flatMap(s->fun(s));
2.4 提取子流和組合流
以下產生一個流,使用limit,skip,concat,peek等方法對流進行操作:
static void fun2(double d) {
System.out.println(d);
}
Stream.generate(Math::random).limit(10).forEach(e -> fun2(e));
Stream.generate(Math::random).limit(10).skip(5).forEach(e -> fun2(e));
Stream.concat(Stream.generate(Math::random).limit(5),Stream.generate(Math::random)
.limit(5)).forEach(e->fun2(e));
Stream.iterate(1,e->e*2).peek(e->System.out.println(e)).limit(10).toArray();
limit:返回一個限制元素個數的新流
skip:返回一個忽略流中前面若干元素的新流
concat:靜態方法,將2個流拼接。注意第一個流不要是無限流,否則永遠不會返回。
peek:返回和原始流相同的新流,但是每次獲取一個元素時,都會調用其中的方法,這樣可以測試無限流是否被延遲處理。在上面的代碼中,如果不使用limit方法,那麼會一直輸出0。如果你通過流執行某些操作,但你還想繼續進行其他操作,使用peek是正確的做法。
2.5 有狀態的轉換
所謂有狀態,就是指流的操作對於後面的元素進行操作時,需要考慮前面的元素,無狀態則跟前面的元素無關。比如去重操作,必須知道前面是否出現過相同的元素。
distinct方法去除流中重複的元素到1個爲止
Stream.of("hehe","haha","hehe").distinct().peek(System.out::println).count();
以上輸出hehe,haha。
sotred方法對流進行排序,Collections.sort方法排序是會改變原來的集合的,而流的操作都是返回新流。
Stream.of("hehe","haha","heihei").sorted().peek(System.out::println).count();
Stream.of("hehe1","haha","hehe11").sorted(Comparator.comparing(String::length)
.reversed()).peek(System.out::println).count();
以上也可以提供自定義比較器進行比較。
2.6 簡單的聚合方法
前面提到過count方法,它會返回流中元素總數(long類型)。
還有max,min方法,分別返回流中最大,最小值(Optional類型)。
List<String> list = new ArrayList<>();
list.add("asgjasiljgoa");
list.add("agsd");
list.add("agarhgadfhadfhahafhga");
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
Optional<String> op2=list.stream().min(String::compareToIgnoreCase);
System.out.println(op1.get()+" "+op2.get());
在java8中,Optional是一種更好的表示缺少返回值的情況。即使沒有返回一個元素給它,也不會拋出異常。
findFirst:返回流中第一個元素。通常與filter方法結合使用
String s=Stream.of("hehe","haha","heihei").filter(e->e.startsWith("ha"))
.findFirst().get();
System.out.println(s);//haha
findAny:返回所有元素。
anyMatch:接收參數同filter,即Predicate類型。返回類型boolean。判斷是否有符合條件的元素存在。
boolean b=list.parallelStream().anyMatch(e->e.startsWith("h"));
System.out.println(b);//true
allMatch:如果所有元素都滿足規則,返回true。
noneMatch:沒有一個元素滿足規則時返回true。
以上帶有any,all,none的方法都可以並行執行。
2.7 Optional類型
Optional<T>或者是一個對T類型對象的封裝,或者表示不是任何對象。它比一般指向T類型的引用更加安全,因爲它不會返回null
如果直接調用它的get方法,如果不存在值時,會拋出NoSuchElementException異常
當然你可以這麼使用:
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
if(op1.isPresent())
System.out.println(op1.get());
但這不會比非null判斷更簡單。
2.7.1 使用Optional值
ifPresent方法另一種形式是接收一個函數,如果存在值,將值傳遞給函數,否則不進行任何操作。
上面代碼可以變成這樣:
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
op1.ifPresent(System.out::println);
op1.ifPresent(list::add);
如果想對結果進行處理,可以使用map方法:
Optional<Boolean> option= op1.map(list::add);
option可能有3種值:封裝true或者false的Optional,或者一個空的可選值。
當然,有存在時則進行操作的方法,也有不存在時進行操作的方法orElse,orElseGet,orElseThrow:
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
String s=op1.orElse("");
op1.orElseGet(()->System.getProperty("driver.className"));
op1.orElseThrow(NoSuchElementException::new);
以上第一個方法提供默認值,第2,3個方法Supplier類型參數,即返回相應類型的方法。
2.7.2 創建可選值
使用Optional.of或者Optional.empty創建一個Optional對象,還可以使用Optional.ofNullable創建可選值。如果值存在,跟調用of一樣,不存在跟empty一樣。
2.7.3 使用flatMap組合可選函數
根據前面的內容知道,如果函數返回的類型與調用者持有的類型不同,使用map是不恰當的,特別是返回與調用者相同類型的時候,此時使用flatMap可以將中間結果展開然後合併。
假設一個函數f返回Optional<T>,類型T裏面有另一個函數g返回Optional<U>,如下:
如果調用s.f().g() 得到的是Optional<Optional<U>>
package chapter2;
import java.util.Optional;
public class Demo2 {
static Optional<Double> inverse(double x) {
return x == 0 ? Optional.empty() : Optional.of(1.0 / x);
}
static Optional<Double> sqrt(double x) {
return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));
}
public static void main(String[] args) {
Optional<Double> op1=inverse(-4.0).flatMap(Demo2::sqrt);
Optional<Double> op = Optional.of(-4.0).flatMap(Demo2::inverse).flatMap(Demo2::sqrt);
op.ifPresent(System.out::println);
}
}
以上實現了返回類型爲Optional的函數的組合。
2.8 聚合操作
如下,通過reduce方法求一個整數流中所有元素的和。其中第二種方式提供了初始sum值,即使流爲空也會返回Integer類型值
Stream<Integer> stream=Stream.iterate(1, e->2+e).limit(100);
stream.reduce((x,y)->x+y).ifPresent(System.out::println);
Integer i=stream.reduce(0,(x,y)->x+y);
System.out.println(i);
如果想對String類型的流求所有字符串的長度總和,不能使用上面的簡單形式,因爲你用lambda表達式求出2個字符串的長度和爲int類型,並不是String類型。所以要通過另一種方式:
Stream<String> stream3=Stream.of("ajgag","agafgag","asdfgageh");
//參數:初始值+函數1+函數2
//函數要求 an associative, non-interfering, statelessfunction
//第一個單詞意思是滿足結合律
//第2,3個單詞是互不干擾的和無狀態的意思。說的都是無狀態性
//函數1:某線程對流部分的計算結果
//函數2:將各線程結果合併
int l=stream3.reduce(0,(total,word)->total+word.length(),(t1,t2)->t1+t2);
int l2=stream3.mapToInt(String::length).sum();
System.out.println(l);
上面第二種方式是映射到數字流,更爲簡單。
2.9 收集結果
可以使用Iterator方法返回一個迭代器,也可以使用toArray方法返回Object數組,如果想得到帶類型的數組,還是可以使用數組構造器引用方式:
String[] str=stream3.toArray(String[]::new);
要將結果收集起來,使用collect方法。使用方式:
a,一個能創建目標類型實例的方法。如HashSet的構造函數
b,一個將元素添加到目標中的方法。
c,一個將2個目標對象合併的方法。如HashSet的addAll方法
HashSet<String> set1=stream3.collect(HashSet::new,HashSet::add,HashSet::addAll);
其實Collectors提供了這3個方法。所以可以這麼做:
List<String> list=stream3.collect(Collectors.toList());//toSet
//特定類型
Set<String> set2=stream3.collect(Collectors.toCollection(TreeSet::new));
//將字符串全部拼接
String result=stream3.collect(Collectors.joining());
//使用,進行拼接
String result2=stream3.collect(Collectors.joining(","));
//元素不是String時
String result3=stream3.map(Object::toString).collect(Collectors.joining(","));
對於一些統計操作,我們可以生成一個統計工具來實現一些統計方法:
IntSummaryStatistics intSummary=stream3.collect(Collectors.summarizingInt(String::length));
intSummary.getSum();
intSummary.getAverage();
intSummary.getMax();
intSummary.getMin();
這樣可以獲取最大最小值,平均值,總和等信息。
2.10 將結果收集到map中
假設有一個Person類,現在想將Stream<Person>收集到map中,如下:
List<Person> list = new ArrayList<>();
list.add(new Person(1,"heihei"));
list.add(new Person(2,"hehe"));
list.add(new Person(3,"haha"));
Stream<Person> s=list.stream();
Map<Integer, String> map1=s.collect(Collectors.toMap(Person::getId, Person::getName));
Map<Integer, Person> map2=s.collect(Collectors.toMap(Person::getId, Function.identity()));
以上第二種方式使用Person自身作爲value。
下面是地區語言和國家信息構成的map。
Stream<Locale> locales=Stream.of(Locale.getAvailableLocales());
//第3個參數解決key衝突,可選,不選有衝突時拋出異常
//第4個參數提供具體類型,可選,不提供使用hashmap
Map<String,String> map3=locales.collect(Collectors.toMap(
l->l.getDisplayLanguage(),
l->l.getDisplayLanguage(l),
(oldvalue,newvalue)->oldvalue,
TreeMap::new));
//key衝突時,添加新的語言到原來的集合中
Map<String,Set<String>> map4=locales.collect(Collectors.toMap(
l->l.getDisplayCountry(),
l->Collections.singleton(l.getDisplayLanguage()),
(a,b)->{
Set<String> set=new HashSet<>(a);
set.addAll(b);
return set;
}
));
另外,每個toMap對應併發情況都有一個toConcurrentMap方法。
2.11 分組和分片
使用groupingBy和partitioningBy可以簡化上面操作,實現數據分類
partitioningBy將數據二分,要麼是符合條件,要麼不符合
Map<String, List<Locale>> map5 = locales.collect(Collectors.groupingBy(Locale::getCountry));
Map<Boolean, List<Locale>> map6 = locales.collect(Collectors.partitioningBy(l -> l.getLanguage().equals("en")));
groupingBy方法還可以提供第二個參數,類型爲Collector。
以下第二個參數使用的是Collectors的靜態方法,使用了靜態導入。
Map<String, Set<Locale>> map7 = locales.collect(Collectors.groupingBy(
Locale::getCountry, toSet()));
//每個國家的語言(Locale)數
Map<String, Long> map8 =
locales.collect(Collectors.groupingBy(Locale::getCountry, counting()));
除了counting,還有summing(Int/Double/Long),maxBy/minBy
//每個州下所有城市的總人數
Map<String,Integer> map9=cities.collect(Collectors.groupingBy(
City::getState,
summingInt(City::getPopulation)));
//每個州人口最多的城市
Map<String,City> map10=cities.collect(Collectors.groupingBy(
City::getState,
maxBy(Comparator.comparing(City::getPopulation))));
mapping方法將一個函數應用到Collector結果上,並且需要另一個收集器來處理結果。
//根據城市所屬州分組,對城市名字長度取最大返回
Map<String,Optional<String>> map11=cities.collect(Collectors.groupingBy(
City::getState,
mapping(
City::getName,
maxBy(Comparator.comparing(String::length))
)
));
有了mapping後我們可以更簡單的獲取每個國家對應的語言:
Map<String,Set<String>> map12=locales.collect(groupingBy(
l->l.getDisplayCountry(),
mapping(l->l.getDisplayLanguage(),
toSet())
));
如果grouping或mapping函數返回int,double或long類型,可以將元素收集到一個summary statistics對象中
Map<String,IntSummaryStatistics> map13=cities.collect(groupingBy(
City::getState,
summarizingInt(City::getPopulation)
));
以下通過2種方式將流中字符串分類後拼接:
Map<String,String> map14=cities.collect(groupingBy(
City::getState,
reducing("",City::getName,(s,t)->s.length()==0?t:s+","+t)
));
Map<String,String> map15=cities.collect(groupingBy(
City::getState,
mapping(City::getName,
joining(","))
));
以上reducing有3種格式:一個參數(二元運算),2個參數(加一個identity),3個參數(再加一個mapper)。有了Stream的reduce方法,Collectors的reducing方法很少會用到了。
2.12 原始類型流
爲了高效,不可能老是將基本類型包裝,所以提供了IntStream等,它們直接存儲基本類型值,不進行包裝。boolean,byte,char,short,int使用IntStream,float類型使用DoubleStream
IntStream i1=IntStream.of(1, 2, 3, 4, 5);
IntStream i2=Arrays.stream(new int[]{1,2,3,4,5},0,3);
IntStream.range(0, 100);//0~99
IntStream.rangeClosed(0, 100);//0~100
Random rand=new Random();
IntStream i3=rand.ints();
以上是IntStream的創建,其他類似。也可以將其轉換成包裝類的Stream:
Stream<Integer> s1=IntStream.range(0, 100).boxed();
前面提到過,Stream<String>類型可以使用如下轉換成IntStream:
s.mapToInt(String::length);
原始類型流和對象流上調用方法的區別:
a,toArray方法返回一個原始類型數組
b,產生Optional結果的方法會返回OptionalInt/Long/Double等,他們沒有get方法而是getAsInt等方法
c,方法sum,average,max,min可以直接使用
d,summaryStatistics方法會產生一個IntSummaryStatistics
2.13 並行流
parallel方法可以將一個Stream轉換成一個並行流。
當不考慮有序時,一些操作會得到優化。
調用Stream.unordered方法可以不關心順序。對於distinct、limit等可以加快執行順序。
Collectors.groupingByConcurrent天然是無序的,因爲需要考慮效率。
2.14 函數式接口
就一個圖。