第7章:Lambda和Stream

第42條 Lambda優先於匿名類

42.1 匿名內部類

//1. 匿名內部類適合於需要函數對象的經典面向對象設計模式,特別是策略模式
//2. 爲什麼下面例子是策略模式的應用呢,因爲sort方法,可以根據傳入Comparator對象的不同,擁有不同的行爲,讓算法的變化,獨立於使用算法的客戶
//3. Comparator接口,將排序的行爲封裝起來,代表抽象策略,而具體的實現叫做具體策略
//4. 但匿名內部類的寫法太繁瑣,直接導致在Java中進行函數式編程太複雜
Collections.sort(words, new Comparator<String>() {
	public int compare(String s1, String s2) {
		return Integer.compare(s1.length(), s2.length());
	}
});

42.2 Lambda表達式

  1. Java8中,爲了全力支撐函數式編程,首先建立了一個概念,即帶有單個抽象方法的接口(函數式接口)是特殊的,應該特殊對待,因此建立了特殊的語法糖,簡化它的創建代碼,就形成了Lambda表達式
  2. 代碼
//整個Lambda表達式代表的類型Comparator,以及s1、s2的類型String,還有重寫的compare方法的返回值類型int,都沒有自己定義,都是編譯器利用上下文關係,自動進行類型推導得出的
//應儘量在定義時刪除Lambda參數的類型,讓編譯器自己去進行類型推導,除非它能讓程序變的更清晰,或者編譯器確實無法推導出該類型
//類型推導的大部分信息是從傳入的參數的泛型中獲取到的信息,如果類型推導得到的信息不夠多,就需要自己定義類型信息,因此爲了方便,應儘可能使用泛型。例如下面方法中words如果是由原生態類型List定義的,那麼編譯器不知道s1是String類型,s1.length編譯報錯,必須自己定義s1爲String,因此應使用帶泛型的List<String>定義集合
Collections.sort(words,
				(s1, s2) -> Integer.compare(s1.length(), s2.length()));
  1. lambda表達式傳給枚舉值的實例域(稱爲枚舉實例域,實際上思路與策略枚舉模式相同)從而優化代碼
import java.util.function.DoubleBinaryOperator;

public enum Operation {
	PLUS("+", (x, y) -> x + y), MINUS("-", (x, y) -> x - y), TIMES("*", (x, y) -> x * y), DIVIDE("/", (x, y) -> x / y);
	private final String symbol;
	private final DoubleBinaryOperator op;
	//DoubleBinaryOperator爲Java8自帶的函數式接口,其一般代表兩個double值的運算
	Operation(String symbol, DoubleBinaryOperator op) {
		this.symbol = symbol;
		this.op = op;
	}

	@Override
	public String toString() {
		return symbol;
	}

	public double apply(double x, double y) {
		return op.applyAsDouble(x, y);
	}
}
  1. 枚舉實例域與特定於常量的類主體區別
    1. Lambda沒有名稱和文檔,如果一個計算並不是一目瞭然的(因爲缺文檔,太複雜看不懂,還沒文檔描述),或行數太多(一般要求3行),就不應該放在Lambda中
    2. 枚舉構造器是無法訪問枚舉的實例成員的,因此這個傳入構造器的Lambda表達式,也無法訪問枚舉中的實例成員

42.3 構造器引用

Collections.sort(words, comparingInt(String::length));

42.4 Lambda表達式與匿名內部類對比

42.4.1 聯繫
  1. Lambda和匿名內部類,都相當於一個非靜態內部類
  2. 非靜態內部類中不允許定義靜態成員,因此匿名內部類中無法定義static成員,而Lambda表達式本身就沒有位置定義成員變量
  3. 非靜態內部類定義在非靜態方法a中時,爲了方便的訪問調用這個a方法的對象B,因此在該類內部會持有B對象的引用(在靜態方法b中時,不會持有),被序列化時,會優先序列化對象B,而如果B沒有實現Serializable接口,序列化會失敗
  4. Lambda或匿名內部類的實例都無法被可靠的序列化和反序列化,因此儘可能不要序列化、反序列化他們,如果想序列化函數式接口的對象,例如Comparator的對象,那麼先定義一個private static內部類,實現該接口,最後對該內部類的對象序列化、反序列化
42.4.2 區別
  1. Lambda表達式,只能創建函數式接口的對象
  2. Lambda表達式中,無法獲取對自身的引用,即Lambda表達式中的this,指的是該表達式所在的方法的調用者。而匿名內部類中,this表示該匿名內部類對象本身
import java.io.Serializable;

@FunctionalInterface
interface TestInterface extends Serializable{
	public void test(int a);
}
class TestClass{
	public static void main(String[] args) {
		TestInterface o = new TestInterface() {
			@Override
			public void test(int a) {
				//打印:TestClass$1@2a098129,this表示當前匿名內部類的實例
				System.out.println(this);
			}
		};
		o.test(1);
		TestClass aa = new TestClass();
		aa.test();
	}
	public void test() {
		TestInterface o1 = a->{
			//打印:TestClass@387c703b,this爲調用test方法的對象
			System.out.println(this);
		};
		o1.test(12);
	}
}

42.5 最佳實踐

  1. 永遠不要爲函數式接口使用Lambda表達式

43 方法引用優先於Lambda

  1. 方法引用可以減少沒必要的形參列表導致的樣板代碼
  2. 方法引用和Lambda,一般誰更簡潔,更可讀,就選用誰
  3. 方法引用比Lambda可讀的場景
//1. Java8新增方法,當map中包含key對應的這個鍵,將原value與第二個參數1,用第三個參數提供的方案,進行組合,成新的value
//2. count、incr兩個參數的書寫,實際上沒有價值,這個Lambda表達時只是想告訴你,該函數,返回的是兩個參數的和
map.merge(key, 1, (count, incr) -> count + incr);
//3. 因此可以使用方法引用替換,表達取兩個參數的和
map.merge(key, 1, Integer::sum);
  1. Lambda比方法引用可讀的場景
@FunctionalInterface
public interface FunctionWu {
    void accept();
}
public class GoshThisClassNameIsHumongous {
	public static void main(String[] args) {
		GoshThisClassNameIsHumongous a = new GoshThisClassNameIsHumongous();
		//1. 有些情況下,參數的名字,對於閱讀代碼有幫助,這回導致Lambda更可讀
		//2. 當Lambda表達式內所調用的方法,與Lambda表達式在同一個類中,比如Lambda表達式() -> action()與其使用的action方法都在類GoshThisClassNameIsHumongous中,明顯Lambda寫起來更簡明
		//3. Lambda更簡明是因爲,一是該lambda表達式中沒有形參列表,同時方法引用必須傳入類名,而Lambda不用
		a.test(()->action());
		//4. 注意,action函數的返回值類型、形參列表,必須與函數式接口FunctionWu 中抽象方法accept的完全相同。具體原因參照方法引用與lambda表達式對應關係
		//5. 書中講了Function接口的靜態方法identity的使用,不如直接用其lambda表達式,但我認爲跟本條無關,這隻能說明某些時候,靜態方法不如Lambda表達式簡單清晰,而不能說明方法引用不如Lambda表達式
		a.test(GoshThisClassNameIsHumongous::action);
	}
	
	public static void action() {
		System.out.println("成功");
	}
	
	public void test(FunctionWu a) {
		a.accept();
	}
}
//1. 在其他類中時,還是方法引用更復雜
public class OtherClass {
	public static void main(String[] args) {
		GoshThisClassNameIsHumongous a = new GoshThisClassNameIsHumongous();
		a.test(()->GoshThisClassNameIsHumongous.action());
		a.test(GoshThisClassNameIsHumongous::action);
	}
}

第44條 堅持使用標準函數式接口

44.1 函數式接口改進模板模式

  1. 模板方法
//1. LinkedHashMap中有如下方法簽名,它實際上是一個模板方法,默認返回false,當LinkedHashMap的對象調用put方法時,會調用這個方法,如果這個方法返回true,會刪除最早放入該LinkedHashMap中的元素。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
  1. 覆蓋模板方法
//LinkedHashMap本來無法作爲緩存,因爲其內的元素會永遠增加,不會釋放,但我們可以覆蓋removeEldestEntry方法,讓其內元素永遠保留在100個以內
//此時
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
	return size() > 100;
}
  1. 自定義函數式接口替代removeEldestEntry
//1. 新增函數式接口
import java.util.Map;

@FunctionalInterface
interface EldestEntryRemovalFunction<K, V> {
	//注意,此處不能像原removeEldestEntry方法一樣,只傳入一個代表最先放入的鍵值對的Map.Entry<K,V> eldest對象
	//因爲原方法中直接可以調用Map對象的size方法,是因爲原方法本身就是Map對象的一個實例方法,它能直接訪問它所在的Map,但本方法不行,因此需要加入一個Map<K, V> map參數
	boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
//1. 爲LinkedHashMap添加函數式接口類型的新屬性,用於存放傳入的函數式接口,修改LinkedHashMap的構造器,或創建其對象的靜態工廠方法,爲他們傳入新增的函數式接口對象
//2. 假設我們修改LinkedHashMap接口如下
//新增屬性
EldestEntryRemovalFunction<Map<K, V> , Map.Entry<K, V> > func;
...
//修改構造器
public LinkedHashMap(EldestEntryRemovalFunction func) {
    super();
    accessOrder = false;
    this.func = func;
}
...
//修改原本調用removeEldestEntry方法的位置
//removeEldestEntry(first)
func.remove(this,first)
//修改客戶端代碼
LinkedHashMap a = new LinkedHashMap((map, mapentry) -> map.size() > 100);
  1. Jdk自帶函數式接口替代自定義
//1. 沒有必要自定義一個函數式接口EldestEntryRemovalFunction,Jdk本身就有可以接收兩個不同類型參數,並返回boolean值的函數式接口BiPredicate,BiPredicate接口中,第一個泛型表示其第一個參數的類型,第二個泛型表示第二個參數的類型
//2. 使用自定義函數式接口,可以減少使用API的人的學習工作量
//修改LinkedHashMap中定義函數式接口類型屬性的代碼,其他代碼省略
BiPredicate<<Map<K, V> , Map.Entry<K, V> > func;

44.2 Jdk自帶的標準函數式接口

java.util.Function中共43個接口,其中有6個基礎接口,可以通過這6個基礎接口,推斷出其餘接口

44.2.1 基礎接口
接口 函數簽名 函數類型 範例
UnaryOperator<T> T apply(T t) 一個參數,返回值類型與參數類型一致 String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) 兩個參數,返回值類型與參數類型一致 BigInteger::add
Predicate<T> boolean test(T t) 一個參數,且返回布爾類型 Collection::isEmpty
Function<T,R> R apply(T t) 一個參數,返回值類型與參數類型不一致 Arrays::asList
Supplier<T> T get() 無參數,且返回(提供)一個值 Instant::now
Consumer<T> void accept(T t) 一個參數,不返回(消耗)任何值 System.out::println
44.2.2 表示參數類型/返回值類型爲基本類型的18種變體
  1. 命名方式:任何基礎接口名前加int、long、double,共18個
  2. 例IntPredicate,它實際上和Predicate<int>,效果相同,但由於泛型不支持基本類型,而函數式接口的對象,會被多次使用,如果使用Predicate<Integer>,那麼其方法,相當於boolean test(Integer value),那麼每次調用Predicate對象的該方法, 相當於都要創建一個Integer類型的value,非常浪費資源,而int就不怎麼耗資源
  3. 除了IntSupplier,是因爲其方法沒有參數列表,因此是其返回值爲int型,其他都是方法的參數列表中,爲int類型
  4. 除了IntFunction,是因爲它表示返回值類型和參數列表類型不同的函數,所以它參數列表中,參數類型爲int,但仍需一個表示其返回值類型的泛型IntFuntion<R>,其他變體,都不帶泛型
44.2.3 表示傳入一個基本類型/Object,返回另一種基本類型的Function的9種變體
  1. 表示傳入int、long、double類型參數,得到非傳入的類型的Function,共6(3*2*1)個,以"傳入類型To+返回值類型+Function"命名,例如IntToLongFunction,其方法簽名爲long applyAsLong(int value)
  2. 表示傳入Object類型參數,返回基本類型的Function,共3個,“To返回值類型Function”,例如ToIntFunction,方法簽名爲int applyAsInt(T value)
  3. 表示傳入
44.2.4 表示兩個參數的Predicate、Function、Consumer的3種變體
  1. 使用Bi+基礎類型表示:例BiPredicate
44.2.5 表示特殊的BiFunction、BiConsumer的6種變體
  1. To+基本類型+BiFunction:表示傳入不同的兩個對象,返回一個基本類型的BiFunction,例:ToIntBiFunction,共3種
  2. Obj+基本類型+Consumer:表示可以消耗一個Object類型,和一個基本類型的Consumer,例:ObjLongConsumer,共3種
44.2.6 表示返回boolean類型的Supplier的1種變體
  1. BooleanSupplier

44.3 不要用泛型爲包裝類型的基礎接口,替代表示基本類型的變體接口

  1. 例:不能用Consumer<Long>替代LongConsumer
  2. 每次調用Consumer<Long>對象的accept(Long t)方法,就會創建一個裝箱類型Long的對象,會產生性能問題

44.4 需要自定義函數式接口的場景

  1. 沒有任何的標準的函數接口,能滿足需求,例如要一個帶有三個參數的Predicate接口
  2. 需要一個拋出checked(必須被try-catch,或throws)異常的接口
  3. 需要一個通用,且能受益於它的描述性的名稱,例如一看Comparator,就知道要用到一個比較器
  4. 需要創建的接口的實例,有着嚴格的條件限制(沒看懂)
  5. 需要接口中提供大量好用的default方法

44.5 @FunctionalInterface註釋

  1. 該註釋在編譯期,對接口進行檢查,如果不是有且只有一個抽象方法,編譯報錯。可以有效避免後續維護人員,不小心給該接口增加抽象方法
  2. Comparator接口中,雖然除了compare這個抽象方法,還提供了另一個抽象方法boolean equals(Object obj),但這個抽象方法,是用於覆蓋Object中的方法,因此不計入抽象方法總數。這樣做的目的,就是強制實現Comparator的接口/類,都必須重寫Object的equals方法

44.6 不要在相同參數位置,提供不同的函數接口來造成重載

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		//對於同一個submit方法,根據傳入的函數式接口對象的不同,調用不同的方法,會引起客戶端的歧義
		//1. 必須加人爲的轉換,才使用Future<?> submit(Runnable task)方法
		service.submit((Runnable) () -> test());
		//2. 不加轉換時,使用的是<T> Future<T> submit(Callable<T> task)方法
		service.submit(() -> test());
	}

	public static int test() {
		return 0;
	}
}

第45條 謹慎使用Stream

45.1 迭代與pipeline寫法

  1. 迭代寫法:也不知道爲什麼叫迭代,可能因爲pipeline這種,是一條下來的,而迭代就是指來回傳遞一些變量,無法用一條語句完成
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

public class Anagrams {
	public static void main(String[] args) throws IOException {
		//文本內容
		//abc
		//cba
		//bca
		//acb
		//1234
		//4321
		File dictionary = new File("C:\\Users\\含低調\\Desktop\\換位詞測試.txt");
		int minGroupSize = Integer.parseInt("1");
		Map<String, Set<String>> groups = new HashMap<>();
		try (Scanner s = new Scanner(dictionary)) {
			while (s.hasNext()) {
				String word = s.next();
				//1. java8新增方法,第一個參數爲key值,如果在該map中存在,返回其value,如果不存在,將第一個參數作爲key,將第一個參數以第二個參數對應的函數進行計算,作爲value,並返回value
				//2. 該方法將讀取到的字符串abc,按順序排序,然後以它作爲groups這個map的key,然後新建一個TreeSet作爲它的value,然後將字符串abc加入到這個TreeSet中
				//3. 當讀取到下一個abc的換位詞時cba,以abc爲key,發現groups這個map中存在該key,然後獲取其value,也就是之前那個TreeSet,然後將cba也加入到這個TreeSet中
				groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
			}
		}
		//4. 將所有的TreeSet取出,並循環,TreeSet中存放的換位詞數如果大於定義的最小值,就打印該TreeSet大小,以及TreeSet中所有換位詞
		for (Set<String> group : groups.values())
			if (group.size() >= minGroupSize)
				System.out.println(group.size() + ": " + group);
	}
	//5. 將字符串排序後返回
	private static String alphabetize(String s) {
		char[] a = s.toCharArray();
		Arrays.sort(a);
		return new String(a);
	}
}
  1. 濫用Stream:容易造成難以讀懂和維護
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import static java.util.stream.Collectors.*;

public class Anagrams {
	public static void main(String[] args) throws IOException {
		Path dictionary = Paths.get("C:\\Users\\含低調\\Desktop\\換位詞測試.txt");
		int minGroupSize = Integer.parseInt("2");
		//1. try-with-resources語句,保證Stream可以被關閉
		//2. 此處,將文件中,每一行,作爲一個元素,構成一個Stream<String>流words
		try (Stream<String> words = Files.lines(dictionary)) {
			//3. 將流轉換成一個Map<String,List<String>>類型的map,map的key值爲,words流中,每個元素,按字符順序排序後,轉爲StringBuilder,再轉爲String,而value爲一個list,這個list中,存放輸入這個key的所有元素
			words.collect(groupingBy(word -> word.chars().sorted()
					.collect(StringBuilder::new, (sb, c) -> sb.append((char) c), StringBuilder::append).toString()))
			//4. 之後將map的每一個value(即那個list),作爲一個元素,構成新的流Stream<List<String>>,並過濾掉其元素長度,小於minGroupSize的元素,之後,將這個元素的長度+":"+元素本身,作爲新元素,構成新的流Stream<String>。最後調用forEach方法打印流中所有元素
					.values().stream().filter(group -> group.size() >= minGroupSize)
					.map(group -> group.size() + ": " + group).forEach(System.out::println);
		}
	}
}
  1. 恰當使用Stream
import static java.util.stream.Collectors.groupingBy;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Stream;

public class Anagrams {
	public static void main(String[] args) throws IOException {
		Path dictionary = Paths.get("C:\\Users\\含低調\\Desktop\\換位詞測試.txt");
		int minGroupSize = Integer.parseInt("2");
		try (Stream<String> words = Files.lines(dictionary)) {
			//1. 之前groupingBy內,表示鍵的一系列操作太繁瑣,改爲使用自定義的alphabetize函數,將排序後的字符串作爲key
			//2. 這樣做將具體的實現細節剝離出主程序,可以增加可讀性,由於pipelines缺乏明確的類型信息,和可以命名的臨時變量,因此這種輔助方法,對於其可讀性的提升,比迭代式代碼中,更加明顯
			words.collect(groupingBy(word -> alphabetize(word))).values().stream()
					.filter(group -> group.size() >= minGroupSize)
					//2. 之前通過map方法,將list的長度+":"+list本身,作爲新流的元素,也是過於複雜,不如直接修改forEach中的打印方法來的簡便
					//3. 此處這個g,應該命名爲group,但這樣命名對於這個例子來講,有點長
					//4. 由於lambda表達式,沒有參數的具體類型,因此lambda表達式中參數的命名,對於使用stream pipelines時,其代碼可讀性,至關重要
					.forEach(g -> System.out.println(g.size() + ": " + g));
		}
	}

	private static String alphabetize(String s) {
		char[] a = s.toCharArray();
		Arrays.sort(a);
		return new String(a);
	}
}

45.2 Stream處理char的問題

  1. 儘量不用Stream處理char值
public class TestChar {
	public static void main(String[] args) {
		//1. chars方法,會將字符串中的字符的int值,作爲元素,組成流   打印結果:721011081081113211911111410810033
		"Hello world!".chars().forEach(System.out::print);
		//2. 必須強制轉換,才能打印字符 打印結果:Hello world!
		"Hello world!".chars().forEach(x->System.out.print((char)x));
	}
}

45.3 Stream和迭代的選擇

Stream pipeline利用函數對象,來描述重複的計算。迭代版代碼中,使用代碼塊描述重複的計算

45.3.1 迭代
  1. 需要代碼塊具備,讀取或修改範圍內的任意局部變量。因爲Lambda本質上是匿名內部類,因此只能讀取final,或有效的final變量,而不能修改它
  2. 需要代碼塊具備,爲外層方法返回值(不是爲自己返回值),break、continue外層的循環,拋出該方法定義的任何checked異常
45.3.2 Stream
  1. 用一種規則轉換元素
  2. 過濾元素
  3. 利用單個操作(添加、連接、計算最小值),合併元素
  4. 將元素存放到集合中
  5. 查找滿足某些條件的元素

45.4 顛倒映射,訪問最初階段的元素

  1. 一個Stream可以有多箇中間操作,這些中間操作執行過程中,會丟失調最初始的Stream中的元素,需要顛倒映射,獲取最初的元素
import static java.math.BigInteger.ONE;
import static java.math.BigInteger.TWO;

import java.io.IOException;
import java.math.BigInteger;
import java.util.stream.Stream;

public class Anagrams {
	public static void main(String[] args) throws IOException {
		// 3. 梅森素數:2的素數次方-1也是素數,那麼這個素數就是梅森素數
		// 4. 此處,將所有素數n,進行2的n次方-1,然後用isProbablePrime(50)判斷該值是否爲素數,如果是,就代表它一定也是梅森素數,保留20個,並打印
		// 5. 最初始時,Stream中存放的是從小到大的素數,後來經過map操作,以及filter操作,導致已經不知道最初始時Stream中元素都是什麼
		// 6. 由於我們知道最後的Stream中的元素,是由2的最初始元素次方-1得到的,因此你完全可以,用最後的元素+1再對2開放,得到最初始的p值
		// 7. BigInteger類中恰好提供了這種計算,就是bitLength函數,因此你完全可以根據這個函數獲得最初的Stream中元素值,這就叫做映射顛倒
		primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)).filter(mersenne -> mersenne.isProbablePrime(50))
				.limit(20).forEach(mp -> System.out.println(mp.bitLength() + ":" + mp));
	}

	// 2.即該方法返回所有素數,方法名primes譯爲"素數們",Stream中的方法其實都應該採用這種,可以根據方法名就推測出Stream中元素信息的這種命名習慣
	static Stream<BigInteger> primes() {
		// 1.iterate方法,表示流中第一個元素爲方法中第一個參數,第二個元素爲,第一個參數,經過第二個參數所代表的函數,而得到的值即BigInteger.TWO.nextProbablePrime,以此類推
		return Stream.iterate(TWO, BigInteger::nextProbablePrime);
	}
	
}

45.5 Stream與迭代差不多的情況

模擬撲克牌的初始化

  1. 迭代
private static List<Card> newDeck() {
	List<Card> result = new ArrayList<>();
	//1. Suit和Rank都是枚舉類,Card代表撲克牌,Suit代表撲克牌的花色,Rank代表撲克牌的大小(1/2/3../K)
	//2. 將所有的花色Suit與所有的大小值Rank做笛卡爾積,並按順序排列,其實就是對撲克牌初始化了
	for (Suit suit : Suit.values())
		for (Rank rank : Rank.values())
			result.add(new Card(suit, rank));
	return result;
}
  1. Stream
private static List<Card> newDeck() {
	//1. flatMap也叫做扁平化處理函數,是將流中的元素,以其內函數的方式,轉化爲流,最後將所有元素轉化出的流中元素進行合併。例如可以將一個字符串元素HAN,轉爲一個,以它每個字母作爲元素的流
	//2. 本方法中,將Suit中的每個枚舉值,轉化爲,一個新的流,這個流中元素爲以Suit這個枚舉值爲suit,以Rank中隨機枚舉值作爲rank,而構成的所有Card對象
	//3. 即每個Suit枚舉值,對應一個流,最後將這些流中元素進行合併,得到一個新的流,並轉爲一個list集合
	return Stream.of(Suit.values()).flatMap(suit -> Stream.of(Rank.values()).map(rank -> new Card(suit, rank)))
			.collect(toList());
}

45.6 最佳實踐

  1. 具體使用哪種方式實現,沒有特定的要求,完全取決於個人偏好、編程環境、代碼可讀性、維護性、簡潔性的總和考慮
  2. 如果不確定選哪種好,可以兩種都嘗試下

第46條 優先選擇Stream中無副作用函數

意思就是Stream中,使用的函數,都應該遵循純函數的要求,這樣可以提升系統可讀性、性能等

46.1 Stream模型

  1. 爲了獲得Stream帶來的描述性、速度、並行性,你應該採用Stream模型編程
  2. 純函數:
    1. 一個函數的結果,只依賴於它的輸入,而不依賴於一個多變的狀態,也不修改任何狀態
    2. 無明顯副作用:函數執行過程中,會與外部發生交互,就叫做副作用,可能包括,I/O 操作,修改函數入參或函數外部的變量,拋出異常等
private static int num = 3;
//該函數,結果不止依賴於其輸入的number,還依賴於一個狀態num,即當num改變,傳入同樣的number,結果不同,因此不是純函數
private static int plus(int number) {
	return number+num;
}
  1. Stream模型最重要的功能就是,將一系列的計算,構造成一系列的轉換,通過純函數計算上一次結果得到下一次的結果
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
	//1. forEach爲Stream一個操作,函數內修改了外部的對象(freq)的狀態,因此不是純函數,這也導致沒有使用Stream的模板,也因此導致了可讀性、性能等變差
	//2. 正常來說,forEach應該只負責展示Stream中元素,而不是進行計算。有時也需要利用forEach將Stream計算後的結果,插入到已經存在的集合中
	words.forEach(word -> {
		freq.merge(word.toLowerCase(), 1L, Long::sum);
	});
}
//1. 改進後的代碼,以流中的元素的toLowerCase方法作爲鍵,數量作爲值,構成一個Map類型的freq
Map<String, Long> freq;
//2. Scanner轉爲Steam爲Java9才新增的方法
try (Stream<String> words = new Scanner(file).tokens()) {
	freq = words.collect(groupingBy(String::toLowerCase, counting()));
}

46.2 Collectors與Collector

  1. Stream.collect方法,需要傳入一個Collector對象
  2. Collector對象,爲收集器,可以看做一個封裝了可以將Stream多個元素合併到單個對象中的方法,的一個不透明的對象。收集器對象產生的對象一般是一個集合
  3. Collectors爲Collector的工具類,用於生成不同類型的Collector對象
46.2.1 Collectors中的方法
  1. toList、toSet、toCollection:將Stream中元素收集到集合中
//1. 將元素集中到Collection中的收集器:收集成一個List的方法toList()、收集成Set的方法toSet()、收集成一個指定Collection的toCollection(collectionFactory)
//2. toSet、toList、toCollection都是Collectors的靜態方法, 因此一般靜態導入Collectors類中所有方法。而comparing爲Comparator的靜態方法
//3. 該方法,將freq這個Map的鍵取出來,形成Stream,並按freq的值(上個方法中,該Map的值應該爲單詞數量),倒序排列,取前十個,最後將這些個鍵,轉爲List
List<String> topTen = freq.keySet().stream().sorted(comparing(freq::get).reversed()).limit(10)
		.collect(toList());
  1. toMap:將Stream中元素收集到映射中,每個Stream元素都有一個關聯的鍵和值,多個Stream元素可以關聯同一個鍵
//1. toMap(keyMapper,valueMapper),keyMapper爲將元素映射成鍵的函數,valueMapper爲將元素映射爲值的函數
//2. 以下函數,將枚舉值,轉爲一個Stream,然後以枚舉值的toString函數的結果作爲Map的鍵,枚舉值本身作爲Map的值,構建出一個名爲stringToEnum 的Map
//3. 使用該方法,如果多個元素映射到同一個鍵,會拋出IllegalStateException
private static final Map<String, Operation> stringToEnum = Stream.of(values())
			.collect(toMap(Object::toString, e -> e));
  1. toMap更復雜形式、groupingBy:解決多個元素同一個key問題
//1. toMap(keyMapper,valueMapper,mergeFunction):第三個函數,用於將Map的值,以該方式進行合併,即如果第三個函數爲乘法,那麼Map的值,就會是同一個鍵對應的所有值的乘積
//2. Artist爲藝術家,Album爲唱片,該方法,將所有唱片轉換成Stream,然後以唱片的artist方法返回的藝術家作爲鍵、唱片本身作爲值,如果遇到同一個藝術家多張唱片,值保留髮行數量(sales)最多的那張唱片
//3. maxBy爲BinaryOperator的靜態方法,它可以將Comparator轉換爲BinaryOperator,用於計算指定比較器產生的最大值
//4. comparing方法,返回Comparator對象,該對象使用其內的函數,來進行判斷
Map<Artist, Album> topHits = albums.collect(
		toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
//5. 帶有三個函數的toMap,一般還可以用於保留最後的符合鍵值的Stream元素
toMap(keyMapper, valueMapper, (oldv1, newv1) -> newv1)
//6. toMap(keyMapper,valueMapper,mergeFunction,mapFactory):第四個參數可以用於指定想要的映射實現類型,例如EnumMap、TreeMap,三個參數時,默認爲HashMap
//7. toConcurrentMap也有三種重載,可以生成ConcurrentHashMap實例

//1. groupingBy(classifier):返回一個可以生成映射的收集器,根據分類函數classifier,將Stream中元素進行分類,該分類函數,會被傳入Stream的元素,並返回值代表該元素所屬的類別,也就是映射的key,而Stream中對應該key的所有值,會被放入一個list中,作爲這個映射的value
//2. 以字符串中所有字母自然順序,建立新字符串,作爲收集的映射的key,同一個key的字符串,都放入一個list中,作爲其value
words.collect(groupingBy(word -> alphabetize(word)))
//3. groupingBy(classifier,downstream):downstream爲下游收集器,可以將同一個key對應的所有value值,轉換成一個值
//a. 例如可以傳入toSet,表示將所有值放入Set中,而不是一個參數時所放入的list中。
//b. 也可以傳入toCollection(collectionFactory),將元素放入自己想要的任何集合中。
//c. 有時也能傳入counting()作爲下游收集器,其value爲該key的數量
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
//4. groupingBy(classifier,mapFactory,downstream):mapFactory可以指定收集器,所收集出的映射的類型,例如TreeMap
//5. groupingByConcurrent方法同樣也提供了3中重載,生成ConcurrentHashMap實例
//6. partitioningBy(predicate)、partitioningBy(predicate,downstream):以boolean值,作爲映射的鍵
  1. counting:只作爲下游收集器,不應直接作爲收集器
//1. 例如如下用法,想返回一個long型值,表示Stream中元素總和,就完全沒有必要,因爲可以直接使用Stream的count方法,獲得相同的效果
  1. 下游收集器:summing、averaging、summarizing開頭的9個方法。Stream上就有相應的功能。reducing、filtering、mapping、flatMapping、collectingAndThen。這些收集器視圖部分複製Stream功能,專門爲了做爲下游收集器
  2. minBy、maxBy:接收一個比較器,返回根據比較器得到的Strea中的最小或最大的元素。他們內部實際上是調用了BinaryOperator的minBy和maxBy方法,他們的功能實際上就是Stream中min和max方法的粗略概括
  3. joining:只用於元素爲CharSequence的Stream,例如字符串(字符串繼承了CharSequence)
//1. 將所有Stream中元素,以指定分隔符分隔,返回一個String對象,但注意元素本身不能包含",",否則引起歧義
joining(delimiter)
//2. 以分隔符分隔,並加上前綴與後綴,例如Stream中包含元素came、saw、suffix,傳入的參數分別爲",","[","]",結果爲[came,saw,conquered]
joining(delimiter,prefix,suffix)
發佈了36 篇原創文章 · 獲贊 0 · 訪問量 1059
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章