(二)Stream API

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 函數式接口

就一個圖。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章