一起來學Java8(七)——Stream(中)

一起來學Java8(七)——Stream(上)中我們瞭解到了Stream對象的常用方法以及用法。現在一起來深入瞭解下Stream.collect()方法的使用

collect基本用法

collect意思爲收集,它是對Stream中的元素進行收集和歸納,返回一個新的集合對象。先來看一個簡單例子:

public class CollectTest {

	@Data
	@AllArgsConstructor
	static class Goods {
		private String goodsName;
		private int price;
	}
	
	public static void main(String[] args) {
		List<Goods> list = Arrays.asList(
				new Goods("iphoneX", 4000)
				, new Goods("mate30 pro", 5999)
				, new Goods("redmek20", 2999)
				);
		List<String> nameList = list.stream()
			.map(Goods::getGoodsName)
			.collect(Collectors.toList());
	}

}

在這個例子中,通過map方法返回商品名稱,然後把所有的商品名稱放到了List對象中。

查看源碼發現,collect方法由兩個重載方法組成。

  • 方法1:
<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);
  • 方法2:
<R, A> R collect(Collector<? super T, A, R> collector);

其中用的最多的是方法2,這個方法可以看做是方法1的快捷方式,因爲Collector中同樣提供了Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner這三個參數,不難猜測其底層還是要用到方法1對應的實現。

我們可以先從collect(Collector<? super T, A, R> collector)開始入手,通過這個再去慢慢了解方法1的用法。

Collectors

Stream.collect(Collector<? super T, A, R> collector)方法的參數Collector對象主要由Collectors類提供。Collectors類裏面包含了一系列的靜態方法,用來返回Collector對象,常用的方法如下列表所示:

方法名稱 描述
averagingXX 求平均數
counting 求集合中元素個數
groupingBy 對集合進行分組
joining 對集合元素進行拼接
mapping 可在分組的過程中再次進行值的映射
maxBy 求最大值
minBy 求最小值
partitioningBy 對元素進行分區
reducing 歸納
summarizingXX 彙總
toCollection 轉換成集合對象
toConcurrentMap 轉換成ConcurrentMap
toList 轉換成List
toMap 轉換成Map
toSet 轉換成Set

下面依次來講解下每個方法的用處。

averagingXX

averagingXX包括averagingDouble,averagingInt,averagingLong。它們表示求平均值。

double averagingInt = Stream.of(1, 2, 3)
		.collect(Collectors.averagingInt(val -> val));
System.out.println("averagingInt:" + averagingInt);

double averagingLong = Stream.of(10L, 21L, 30L)
		.collect(Collectors.averagingLong(val -> val));
System.out.println("averagingLong:" + averagingLong);

double averagingDouble = Stream.of(0.1, 0.2, 0.3)
		.collect(Collectors.averagingDouble(val -> val));
System.out.println("averagingDouble:" + averagingDouble);

它們的參數是一個函數式接口,可以使用Lambda表達式編寫,其中Lambda表達式中的參數爲Stream中的元素,返回的是待求平均的數值。下面這則列子是求商品的平均值:

List<Goods> list = Arrays.asList(
				new Goods("iphoneX", 4000)
				, new Goods("mate30 pro", 5999)
				, new Goods("redmek20", 2999)
				);
		
double avgPrice = list.stream()
	.collect(Collectors.averagingInt(goods -> goods.getPrice()));
System.out.println("商品的平均價格:" + avgPrice);

summingXX

與averagingXX類似,summingXX方法用來求集合中的元素值的總和。

double summingInt = Stream.of(1, 2, 3)
		.collect(Collectors.summingInt(val -> val));
System.out.println("summingInt:" + summingInt);

double summingLong = Stream.of(10L, 21L, 30L)
		.collect(Collectors.summingLong(val -> val));
System.out.println("summingLong:" + summingLong);

double summingDouble = Stream.of(0.1, 0.2, 0.3)
		.collect(Collectors.summingDouble(val -> val));
System.out.println("summingDouble:" + summingDouble);

打印:

summingInt:6.0
summingLong:61.0
summingDouble:0.6

counting()

counting()返回集合中元素個數。

long count = Stream.of(1,2,3,4,5)
		.collect(Collectors.counting());
System.out.println("count:" + count); // 5

summarizingXX

上面講到了averagingXX(求平均)、summingXX(求和)、counting(求總數),如果我要同時獲取這三個數該怎麼辦呢,可以用summarizingXX。

IntSummaryStatistics summarizingInt = Stream.of(1, 2, 3)
				.collect(Collectors.summarizingInt(val -> val));
System.out.println("平均值:" + summarizingInt.getAverage());
System.out.println("總個數:" + summarizingInt.getCount());
System.out.println("總和:" + summarizingInt.getSum());
System.out.println("最大值:" + summarizingInt.getMax());
System.out.println("最小值:" + summarizingInt.getMin());

打印:

平均值:2.0
總個數:3
總和:6
最大值:3
最小值:1

summarizingInt將統計結果放到了一個IntSummaryStatistics對象裏面,在對象中可以獲取不同的統計信息。

groupingBy()

groupingBy()是對集合中的元素進行分組,由三個重載方法組成

  • 重載1: groupingBy(Function)
  • 重載2: groupingBy(Function, Collector)
  • 重載3: groupingBy(Function, Supplier, Collector)

其中重載1調用了重載2,重載2調用重載3,因此最終都會執行到重載3中來。

首先看下重載1groupingBy(Function)的用法,這個方法默認分組到新的List中,下面這個例子對商品類型進行分組,同樣的類型的商品放到一個List中。

@Data
@AllArgsConstructor
static class Goods {
	private String goodsName;
	// 類型,1:手機,2:電腦
	private int type;
	@Override
	public String toString() {
		return goodsName;
	}
}

public static void main(String[] args) {
	List<Goods> list = Arrays.asList(
			new Goods("iphoneX", 1)
			, new Goods("mate30 pro", 1)
			, new Goods("thinkpad T400", 2)
			, new Goods("macbook pro", 2)
			);
	
	Map<Integer, List<Goods>> goodsListMap = list.stream()
		.collect(Collectors.groupingBy(Goods::getType));
	goodsListMap.forEach((key, value) -> {
		System.out.println("類型" + key + ":" + value);
	});
}

打印:

類型1:[iphoneX, mate30 pro]
類型2:[thinkpad T400, macbook pro]

上面說到了groupingBy(Function)實際上是調用了groupingBy(Function, Collector),其中第二個參數Collector決定了轉換到哪裏,默認是toList(),參見groupingBy(Function)的源碼:

public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier) {
        return groupingBy(classifier, toList());
    }

因此我們可以調用groupingBy(Function, Collector)手動指定Collector,假設我們要把轉換後的元素放到Set當中,可以這樣寫:

Map<Integer, Set<Goods>> goodsListMap = list.stream()
        .collect(Collectors.groupingBy(Goods::getType, Collectors.toSet()));

查看重載2方法源碼,發現其調用了重載3:

public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        return groupingBy(classifier, HashMap::new, downstream);
    }

其中Goods::getType對應classifier,Collectors.toSet()對應downstream。中間那個參數HashMap::new意思很明顯了,即返回的Map的具體實現類是哪個,如果要改成LinkedHashMap,可以這樣寫:

LinkedHashMap<Integer, Set<Goods>> goodsListMap = list.stream()
        .collect(Collectors.groupingBy(Goods::getType, LinkedHashMap::new, Collectors.toSet()));
        

這正是重載3的使用方式。

Collectors中的groupingByConcurrent方法正是基於重載3而來,中間的代碼改成了ConcurrentHashMap::new而已。

public static <T, K>
    Collector<T, ?, ConcurrentMap<K, List<T>>>
    groupingByConcurrent(Function<? super T, ? extends K> classifier) {
        return groupingByConcurrent(classifier, ConcurrentHashMap::new, toList());
    }

groupingBy方法中的Collector參數不僅僅只可以toList(),toSet(),它還有更加靈活的用法,之前我們轉換的都是Map<Integer, List<Goods>>形式,value中存放的是集合對象,如果不想要那麼多屬性,只想要對象裏面的商品名稱,,也就是說我們想得到Map<Integer, List<String>>,其中key爲商品類型,value爲商品名稱集合。

這個時候Collectors.mapping()就派上用場了,我們使用groupingBy(Function, Collector)方法,第二參數傳Collectors.mapping()

Map<Integer, List<String>> goodsListMap = 
list.stream()
	.collect(
		 Collectors.groupingBy(
		    Goods::getType, 
		    Collectors.mapping(Goods::getGoodsName, Collectors.toList())
		 )
	);

mapping()方法有兩個參數,第一參數指定返回的屬性,第二個參數指定返回哪種集合。

joining

joining方法可以把Stream中的元素拼接起來。

List<String> list = Arrays.asList("hello", "world");
String str = list.stream().collect(Collectors.joining());
System.out.println(str); // 打印:helloworld

還可以指定分隔符:

List<String> list = Arrays.asList("hello", "world");
String str = list.stream().collect(Collectors.joining(","));
System.out.println(str); // 打印:hello,world

除此之外,String類提供了一個join方法,功能是一樣的

String str2 = String.join(",", list);
System.out.println(str2);

maxBy&minBy

  • maxBy:找出Stream中最大的元素
@Data
@AllArgsConstructor
static class Goods {
	private String goodsName;
	private int price;
}

public static void main(String[] args) {
	List<Goods> list = Arrays.asList(
			new Goods("iphoneX", 4000)
			, new Goods("mate30 pro", 5999)
			, new Goods("redmek20", 2999)
			);
	
	Goods maxPriceGoods = list.stream()
		.collect(
			Collectors.maxBy(
				Comparator.comparing(Goods::getPrice)
			)
		)
		.orElse(null);
	System.out.println("最貴的商品:" + maxPriceGoods);
}

上面的例子演示了查找最貴的商品,Collectors.maxBy()方法需要傳入一個比較器,需要根據商品的價格來比較。

同理,找到最便宜的商品只需把maxBy替換成minBy即可。

partitioningBy

partitioningBy方法表示分區,它將根據條件將Stream中的元素分成兩部分,並分別放入到一個Map當中,Map的key爲Boolean類型,key爲true部分存放滿足條件的元素,key爲false存放不滿足條件的元素。

{
    true -> 符合條件的元素
    false -> 不符合條件的元素
}

partitioningBy方法由兩個重載方法組成

  • 重載1:partitioningBy(Predicate)
  • 重載2:partitioningBy(Predicate, Collector)

其中重載1會調用重載2,因此最終還是調用了重載2方法,我們先看下重載1方法。

下面這個例子根據商品類型,將商品劃分爲手機類商品和非手機類商品。

@Data
@AllArgsConstructor
static class Goods {
	private String goodsName;
	// 類型,1:手機,2:電腦
	private int type;
	@Override
	public String toString() {
		return goodsName;
	}
}

public static void main(String[] args) {
	List<Goods> list = Arrays.asList(
			new Goods("iphoneX", 1)
			, new Goods("mate30 pro", 1)
			, new Goods("thinkpad T400", 2)
			, new Goods("macbook pro", 2)
			);
	
	// 手機歸爲一類,非手機商品歸爲一類
	// true -> 手機類商品
	// false -> 非手機類商品
	Map<Boolean, List<Goods>> goodsMap = list.stream()
		.collect(
		    Collectors.partitioningBy(goods -> goods.getType() == 1)
		);
	// 獲取手機類商品
	List<Goods> mobileGoods = goodsMap.get(true);
	System.out.println(mobileGoods);
}

partitioningBy(Predicate, Collector)方法的第二個參數可以用來指定集合元素,默認使用的List存放,如果要使用Set存放,可以這樣寫:

Map<Boolean, Set<Goods>> goodsMap = list.stream()
	.collect(
		Collectors.partitioningBy(
			goods -> goods.getType() == 1
			// 指定收集類型
			, Collectors.toSet())
	);

toList & toSet & toCollection

toList和toSet可以將Stream中的元素轉換成List、Set集合,這是用的比較多的兩個方法。

Stream<Goods> stream = Stream.of(
		new Goods("iphoneX", 4000)
		, new Goods("mate30 pro", 5999)
		, new Goods("redmek20", 2999)
		);

List<Goods> list = stream.collect(Collectors.toList());
Set<Goods> set = stream.collect(Collectors.toSet());

默認情況下,toList返回的是ArrayList,toSet返回的是HashSet,如果要返回其它類型的集合比如LinkedList,可以使用toCollection,它可以讓開發者自己指定需要哪種集合。

LinkedList<Goods> linkedList = stream.collect(Collectors.toCollection(LinkedList::new));

toConcurrentMap

toConcurrentMap方法是將Stream轉換成ConcurrentMap,它由三個重載方法組成

  • 重載1:toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)
  • 重載2:toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
  • 重載3:toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)

其中重載1調用重載2,重載2調用重載3,最終都會執行到重載3方法上來。

先看重載1,提供了兩個參數

  • keyMapper:指定ConcurrentMap中的key值
  • valueMapper:指定key對應的value

下面這個例子是將商品的名稱作爲key,價格作爲value

List<Goods> list = Arrays.asList(
		new Goods("iphoneX", 4000)
		, new Goods("mate30 pro", 5999)
		, new Goods("redmek20", 2999)
);
ConcurrentMap<String, Integer> goodsMap = list.stream()
		.collect(
				Collectors.toConcurrentMap(Goods::getGoodsName, Goods::getPrice)
		);
System.out.println(goodsMap);

打印:

{mate30 pro=5999, iphoneX=4000, redmek20=2999}

注意:這個方法要求key不能重複,如果有重複的key,會拋IllegalStateException異常,如果有key重複,需要使用toConcurrentMap(Function, Function, BinaryOperator),即重載2

再來看下重載2:toConcurrentMap(Function, Function, BinaryOperator),這個方法前兩個參數跟重載1一樣,第三個參數用來處理key衝突的情況,讓開發者選擇一個value值返回。

List<Goods> list = Arrays.asList(
		new Goods("iphoneX", 4000)
		, new Goods("mate30 pro", 5999)
		, new Goods("mate30 pro", 6000) // 這裏有兩個衝突了
		, new Goods("redmek20", 2999)
);
ConcurrentMap<String, Integer> goodsMap = list.stream()
		.collect(
				Collectors.toConcurrentMap(Goods::getGoodsName, Goods::getPrice, new BinaryOperator<Integer>() {
					@Override
					public Integer apply(Integer price1, Integer price2) {
						// 選擇價格貴的返回
						return Math.max(price1, price2);
					}
				})
		);
System.out.println(goodsMap);

打印:{mate30 pro=6000, iphoneX=4000, redmek20=2999}

這個例子中mate30 pro作爲key重複了,在BinaryOperator中,我們選擇價格高的那一條數據返回。

最後看下重載3,相比於重載2,又多了一個參數Supplier,它可以讓開發者指定返回一種ConcurrentMap

重載2調用重載3,默認使用的是ConcurrentMap::new

注意:第四個參數必須是ConcurrentMap或ConcurrentMap的子類

小節

本篇主要講解了Stream.collect的用法,以及Collectors類中靜態方法的使用,在下一篇文章中,我們將詳細講解關於reduce的相關用法。

歡迎關注作者微信公衆號:猿敲月下碼,第一時間獲得技術分享
微信公衆號:猿敲月下碼

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