前些年 Scala 大肆流行,打出來 Java 顛覆者的旗號,究其底氣來源,無非是函數式和面向對象的“完美結合”,各式各樣的“語法糖”,但其過高的學習門檻,又給了新來者當頭一棒。
隨著 Java8 的發(fā)布,Lambda 特性的引入,之前的焦灼局面是否有所轉變,讓我們一起揭開 Java 函數式編程的面紗:
- 面向對象 VS 函數式
- FunctionalInterface 和 Lambda
- 類庫的升級改造(默認方法、靜態(tài)方法、Stream、Optional)
- Lambda 下模式的進化
- Lambda 下并發(fā)程序
1. 面向對象 VS 函數式編程
一句話總結兩種的關系:面向對象編程是對數據進行抽象;而函數式編程是對行為進行抽象。
在現(xiàn)實世界中,數據和行為并存,程序也應如此,可喜可賀的是在 Java 世界中,兩者也開啟了融合之旅。
首先思考一個問題, 在 Java 編程中,我們如何進行行為傳遞,例如我們需要打印線程名稱和當前時間,并將該任務提交到線程池中運行,會有哪些方法?
方法 1:新建 class Task 實現(xiàn) Runnable 接口
public class Task implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");
}
}
executorService.submit(new Task());
方法 2:匿名內部類實現(xiàn) Runnable 接口
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");
}
});
方法 3:使用 Lambda 表達式
executorService.submit(()-> System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms"));
方法 4:使用方法引用
private void print(){
System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");
}
{
executorService.submit(this::print);
}
通過上面不同的行為傳遞方式,能夠比較直觀的體會到隨著函數式特性的引入,行為傳遞少了很多樣板代碼,增加了一絲靈活;可見Lambda表達式是一種緊湊的、傳遞行為的方式。
2. FunctionalInterface 和 Lambda
Java 函數式編程,只有兩個核心概念:
FunctionalInterface(函數接口)是只有一個抽象方法的接口,用作 Lambda 表達式的類型。
Lambda 表達式,及要傳遞的行為代碼,更像是一個匿名函數(當然 java 中并沒有這個概念),將行為像數據那樣進行傳遞。
換個好理解但是不正規(guī)的說法,F(xiàn)unctionalInterface 為類型,Lambda 表達式為值;我們可以將一個 Lambda 表達式賦予一個符合 FunctionalInterface 要求的接口變量(局部變量、方法參數)。
2.1. Lambda 表達式
先看幾個 Lambda 表達式的例子:
// 不包含參數,用()表示沒有參數
// 表達式主體只有一個語句,可以省略{}
Runnable helloWord = () -> System.out.println("Hello World");
// 表達式主體由多個語句組成,不能省略{}
Runnable helloWords = () -> {
System.out.println("Hello");
System.out.println("Word");
System.out.println("Word");
};
// 表達式中只有一個參數,可以省略()
Consumer<String> infoConsumer = msg -> System.out.println("Hello " + msg);
// 表達式由多個參數組成,不可省略()
BinaryOperator<Integer> add1 = (Integer i ,Integer j) -> i + j;
// 編譯器會進行類型推斷,在沒有歧義情況下可以省略類型聲明,但是不可省略()
BinaryOperator<Integer> add2 = (i, j) -> i + j;
綜上可見,一個 Lambda 表達式主要由三部分組成:
- 參數列表
- 箭頭分隔符(->)
- 主體,單個表達式或語句塊
我們在使用匿名內部類時有一些限制:引用方法中的變量時,需要將變量聲明為 final,不能為其進行重新賦值,如下:
final String msg = "World";
Runnable print = new Runnable() {
@Override
public void run() {
System.out.println("Hello" + msg);
}
};
在 Java8 中放松了這個限制,可以引用非 final 變量,但是該變量在既成事實上必須是 final 的,雖然無需將變量聲明為 final,在 Lambda 表達式中,也無法用作非最終態(tài)變量,及只能給該變量賦值一次(與用 final 聲明變量效果相同)。
2.2 FunctionalInterface
FunctionalInterface,只有一個抽象方法的接口就是函數式接口,接口中單一方法命名并不重要,只要方法簽名與 Lambda 表達式的類型匹配即可。
Java 內置了常用函數接口如下:
1. Predicate<T>
參數類型:T
返回值:boolean
示例:Predicate<String> isAdmin = name -> "admin".equals(name);
2. Consumer<T>
參數:T
返回值:void
示例:Consumer<String> print = msg -> System.out.println(msg);
3. Function<T,R>
參數:T
返回值:R
示例:Function<Long, String> toStr = value -> String.valueOf(value);
4. Supplier<T>
參數:none
返回值:T
示例:Supplier<Date> now = () -> new Date();
5. UnaryOperator<T>
參數:T
返回值:T
示例:UnaryOperator<Boolean> negation = value -> !value.booleanValue();
6. BinaryOperator<T>
參數:(T, T)
返回值:T
示例:BinaryOperator<Integer> intDouble = (i, j) -> i + j;
7. Runnable
參數:none
返回值:void
示例:Runnable helloWord = () -> System.out.println("Hello World");
8. Callable<T>
參數:nont
返回值:T
示例:Callable<Date> now1 = () -> new Date();
當然我們也可以根據需求自定義函數接口,為了保證接口的有效性,可以在上面添加 @FunctionalInterface 注解,該注解會強制 javac 檢測一個接口是否符合函數式接口的規(guī)范,例如:
@FunctionalInterface
interface CustomFunctionalInterface{
void print(String msg);
}
CustomFunctionalInterface cfi= msg -> System.out.println(msg);
2.3 方法引用
Lambda 表達式一種常用方法便是直接調用其他方法,針對這種情況,Java8 提供了一個簡寫語法,及方法引用,用于重用已有方法。
凡是可以使用 Lambda 表達式的地方,都可以使用方法引用。
方法應用的標準語法為 ClassName::methodName,雖然這是一個方法,但不需要再后面加括號,因為這里并不直接調用該方法。
Function<User, String> f1 = user->user.getName();
Function<User, String> f2 = User::getName;
Supplier<User> s1 = ()->new User();
Supplier<User> s2 = User::new;
Function<Integer, User[]> sa1 = count -> new User[count];
Function<Integer, User[]> sa2 = User[]::new;
方法引用主要分為如下幾種類型:
- 靜態(tài)方法引用:className::methodName
- 實例方法引用:instanceName::methodName
- 超類實體方法引用:supper::mehtodName
- 構造函數方法引用:className::new
- 數組構造方法引用:ClassName[]::new
2.4 類型推斷
類型推斷,是 Java7 就引入的目標類型推斷的擴展,在 Java8 中對其進行了改善,程序員可以省略 Lambda 表達式中的所有參數類型,Javac 會根據 Lambda 表達式式上下文信息自動推斷出參數的正確類型。
大多數情況下 javac 能夠準確的完成類型推斷,但由于 Lambda 表達式與函數名無關,只與方法簽名相關,因此會出現(xiàn)類型對推斷失效的情況,這時可以使用手工類型轉換幫助 javac 進行正確的判斷。
// Supplier<String>, Callable<String> 具有相同的方法簽名
private void print(Supplier<String> stringSupplier){
System.out.println("Hello " + stringSupplier.get());
}
private void print(Callable<String> stringCallable){
try {
System.out.println("Hello " + stringCallable.call());
} catch (Exception e) {
e.printStackTrace();
}
}
{
// Error, 因為兩個print同時滿足需求
print(()->"World");
// 使用類型轉換,為編譯器提供更多信息
print((Supplier<String>) ()->"World");
print((Callable<String>) ()-> "world");
}
3. 類庫的升級改造
Java8 另一個變化是引入了 默認方法 和接口的 靜態(tài)方法 ,自此以后 Java 接口中方法也可以包含代碼體了。
3.1 默認方法
默認方法允許接口方法定義默認實現(xiàn),而所有子類都將擁有該方法及實現(xiàn)。使其能夠在不改變子類實現(xiàn)的情況下(很多時候我們無法拿到子類的源碼),為所有子類添加新的功能,從而最大限度的保證二進制接口的兼容性。
默認方法的另一個優(yōu)勢是該方法是可選的,子類可以根據不同的需求 Override 默認實現(xiàn),為其提供擴展性保證。
其中 Collection 中的 forEach,stream 功能都是通過該技術統(tǒng)一添加到接口中的。
// Collection 中的forEache實現(xiàn)
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
// Collection中的stream實現(xiàn)
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
從上可見,默認方法的寫法也是比較簡單的,只需在方法聲明中添加 defalut 關鍵字,然后提供方法的默認實現(xiàn)即可。
和類不同,接口中沒有成員變量,因此默認方法只能通過調用子類的方法來修改子類本身,避免了對子類的實現(xiàn)做出各種假設。
3.1.1 默認方法與子類
添加默認方法特性后,方法的重寫規(guī)則也發(fā)生了變化,具體的場景如下:
a. 沒有重寫
沒有重寫是最簡單的情況,子類調用該方法的時候,自然繼承了默認方法。
interface Parent{
default void welcome(){
System.out.println("Parent");
}
}
// 調用Parent中的welcome, 輸入"Parent"
class ParentNotImpl implements Parent{
}
b. 子接口重寫
子接口對父接口中的默認方法進行了重新,其子類方法被調用時,執(zhí)行子接口中的默認方法
interface Parent{
default void welcome(){
System.out.println("Parent");
}
}
interface ChildInterface extends Parent{
@Override
default void welcome(){
System.out.println("ChildInterface");
}
}
// 執(zhí)行ChildInterface中的welcome, 輸入 "ChildInterface"
class ChildImpl implements ChildInterface{
}
c. 類重寫
一旦類中重寫了默認方法,優(yōu)先選擇類中定義的方法,如果存在多級類繼承,遵循類繼承邏輯。
interface Parent{
default void welcome(){
System.out.println("Parent");
}
}
interface ChildInterface extends Parent{
@Override
default void welcome(){
System.out.println("ChildInterface");
}
}
//執(zhí)行子類中的welcome方法,輸出"ChildImpl"
class ChildImpl1 implements ChildInterface{
@Override
public void welcome(){
System.out.println("ChildImpl");
}
}
3.1.2 多重繼承
接口允許多重繼承,因此有可能會碰到兩個接口包含簽名相同的默認方法的情況,此時 javac 并不明確應該繼承哪個接口中的方法,因此會導致編譯出錯,這時需要在類中實現(xiàn)該方法,如果想調用特定父接口中的默認方法,可以使用 ParentInterface.super.method() 的方式來指明具體的接口。
interface Parent1 {
default void print(){
System.out.println("parent1");
}
}
interface Parent2{
default void print(){
System.out.println("parent2");
}
}
class Child implements Parent1, Parent2{
@Override
public void print() {
System.out.println("self");
Parent1.super.print();
Parent2.super.print();
}
}
現(xiàn)在的接口提供了某種形式上的多繼承功能,然而多重繼承存在很多詬病。很多人認為多重繼承的問題在于對象狀態(tài)的繼承,而不是代碼塊的繼承,默認方法避免了狀態(tài)的繼承,也因此避免了 C++ 中多重繼承最大的缺點。
接口和抽象類之間還是有明顯的區(qū)別。接口允許多重繼承,卻沒有成員變量;抽象類可以繼承成員變量,卻不能多重繼承。
從某種角度出發(fā),Java 通過接口默認方法實現(xiàn)了代碼多重繼承,通過類實現(xiàn)了狀態(tài)單一繼承。
3.1.3 三定律
如果對默認方法的工作原理,特別是在多重繼承下的行為沒有把握,可以通過下面三條簡單定律幫助大家。
- 類勝于方法。
如果在繼承鏈中有方法體或抽象的方法聲明,那么就可以忽略接口中定義的方法。 - 子類勝于父類。
如果一個接口繼承另一個接口,且兩個接口都定義了一個默認方法,那么子接口中定義的方法勝出。 - 沒有規(guī)則三。
如果上面兩條規(guī)則不適用,子類要么實現(xiàn)該方法,要么將該方法聲明為抽象方法。
3.2 接口靜態(tài)方法
人們在編程過程中積累了這樣一條經驗,創(chuàng)建一個包含很多靜態(tài)方法的一個類。很多時候類是一個放置工具方法的好地方,比如 Java7 引入的 Objects 類,就包含很多工具方法,這些方法不是屬于具體的某個類。
如果一個方法有充分的語義原因和某個概念相關,那么就應該講該方法和相關的類或接口放在一起,而不是放到另一個工具類中,這非常有助于更好的組織代碼。
在接口中定義靜態(tài)方法,只需使用 static 關鍵字進行描述即可,例如 Stream 接口中的 of 方法。
/**
* Returns a sequential {@code Stream} containing a single element.
*
* @param t the single element
* @param <T> the type of stream elements
* @return a singleton sequential stream
*/
public static<T> Stream<T> of(T t) {
return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}
3.3 Stream
Stream 是 Java8 中最耀眼的亮點,它使得程序員得以站在更高的抽象層次對集合進行操作。
Stream 是用函數式編程方式在集合類上進行復雜操作的工具。
3.3.1. 從外部迭代到內部迭代
Java 程序員使用集合時,一個通用模式就是在集合上進行迭代,然后處理返回的每一個元素,盡管這種操作可行但存在幾個問題:
- 大量的樣板代碼
- 模糊了程序本意
- 串行化執(zhí)行
常見集合遍歷如下:
// 常見寫法1,不推薦使用
public void printAll1(List<String> msg){
for (int i=0; i< msg.size(); i++){
String m = msg.get(i);
System.out.println(m);
}
}
// Java5之前,正確寫法,過于繁瑣
public void printAll2(List<String> msg){
Iterator<String> iterator = msg.iterator();
while (iterator.hasNext()){
String m = iterator.next();
System.out.println(m);
}
}
// Java5之后,加強for循環(huán),采用語法糖,簡化for循環(huán),內部轉化為Iterator方式
public void printAll3(List<String> msg){
for (String m : msg){
System.out.println(m);
}
}
整個迭代過程,通過顯示的調用 Iterator 對象的 hasNext 和 next 方法完成整個迭代,這成為外部迭代。
另一種方式成為內部迭代,及將操作行為作為參數傳遞給 Stream,在 Stream 內部完成迭代操作。
// Java8中,使用Stream進行內部迭代操作
public void printAll4(List<String> msg){
msg.stream().forEach(System.out::println);
}
內部迭代:
3.3.2. 惰性求值 VS 及早求值
Stream 中存在兩類方法,不產生值的方法稱為惰性方法;從 Stream 中產生值的方法叫做及早求值方法。
判斷一個方法的類別很簡單:如果返回值是 Stream,那么就是惰性方法;如果返回值是另一個值或為空,那么就是及早求值方法。
惰性方法返回的 Stream 對象不是一個新的集合,而是創(chuàng)建新集合的配方,Stream 本身不會做任何迭代操作,只有調用及早求值方法時,才會開始真正的迭代。
整個過程與 Builder 模式有共通之處,惰性方法負責對 Stream 進行裝配(設置 builder 的屬性),調用及早求值方法時(調用 builder 的 build 方法),按照之前的裝配信息進行迭代操作。
常見 Stream 操作:
3.3.2.1 collect(toList())
及早求值方法:
collect(toList()) 方法由 Stream 里面的值生成一個列表,是一個及早求值操作。
collect 的功能不僅限于此,它是一個非常強大的結構。
@Data
class User{
private String name;
}
public List<String> getNames(List<User> users){
List<String> names = new ArrayList<>();
for (User user : users){
names.add(user.getName());
}
return names;
}
public List<String> getNamesUseStream(List<User> users){
// 方法引用
//return users.stream().map(User::getName).collect(toList());
// lambda表達式
return users.stream().map(user -> user.getName()).collect(toList());
}
3.3.2.2. count、max、min
及早求值方法:
Stream 上最常用的操作之一就是求總數、最大值和最小值,count、max 和 min 足以解決問題。
public Long getCount(List<User> users){
return users.stream().filter(user -> user != null).count();
}
// 求最小年齡
public Integer getMinAge(List<User> users){
return users.stream().map(user -> user.getAge()).min(Integer::compareTo).get();
}
// 求最大年齡
public Integer getMaxAge(List<User> users){
return users.stream().map(user -> user.getAge()).max(Integer::compareTo).get();
}
min 和 max 入參是一個 Comparator 對象,用于元素之間的比較,返回值是一個 Optional<T>,它代表一個可能不存在的值,如果 Stream 為空,那么該值不存在,如果不為空,該值存在。通過 get 方法可以獲取 Optional 中的值。
3.3.2.3 findAny、findFirst
及早求值方法:
兩個函數都以Optional為返回值,用于表示是否找到。
public Optional<User> getAnyActiveUser(List<User> users){
return users.stream()
.filter(user -> user.isActive())
.findAny();
}
public Optional<User> getFirstActiveUser(List<User> users){
return users.stream()
.filter(user -> user.isActive())
.findFirst();
}
3.3.2.4 allMatch、anyMatch、noneMatch
及早求值方法:
均以 Predicate 作為輸入參數,對集合中的元素進行判斷,并返回最終的結果。
// 所有用戶是否都已激活
boolean allMatch = users.stream().allMatch(user -> user.isActive());
// 是否有激活用戶
boolean anyMatch = users.stream().anyMatch(user -> user.isActive());
// 是否所有用戶都沒有激活
boolean noneMatch = users.stream().noneMatch(user -> user.isActive());
3.3.2.6. forEach
及早求值:
以 Consumer 為參數,對 Stream 中復合條件的對象進行操作。
public void printActiveName(List<User> users){
users.stream()
.filter(user -> user.isActive())
.map(user -> user.getName())
.forEach(name -> System.out.println(name));
}
3.3.2.7 reduce
及早求值方法:
reduce 操作可以實現(xiàn)從一組值中生成一個值,之前提到的 count、min、max 方法因為比較通用,單獨提取成方法,事實上,這些方法都是通過 reduce 完成的。
下圖展示的是對 stream 進行求和的過程,以 0 為起點,每一步都將 stream 中的元素累加到 accumulator 中,遍歷至最后一個元素,accumulator 就是所有元素值的和。
3.3.2.8. filter
惰性求值方法:
以 Predicate 作為參數(相當于 if 語句),對 Stream 中的元素進行過濾,只有復合條件的元素才能進入下面的處理流程。
處理流程如下:
public List<User> getActiveUser(List<User> users){
return users.stream()
.filter(user -> user.isActive())
.collect(toList());
}
3.3.2.9 map
及早求值方法:
以 Function 作為參數,將 Stream 中的元素從一種類型轉換成另外一種類型。
處理過程如下:
public List<String> getNames(List<User> users){
return users.stream()
.map(user -> user.getName())
.collect(toList());
}
3.3.2.10 peek
Stream 提供的是內迭代,有時候為了功能調試,需要查看每個值,同時能夠繼續(xù)操作流,這時就會用到 peek 方法。
public void printActiveName(List<User> users){
users.stream()
.filter(user -> user.isActive())
.peek(user -> System.out.println(user.isActive()))
.map(user -> user.getName())
.forEach(name -> System.out.println(name));
}
3.3.2.11 其他
針對集合 Stream 還提供了許多功能強大的操作,暫不一一列舉,簡單匯總一下。
- distinct:進行去重操作
- sorted:進行排序操作
- limit:限定結果輸出數量
- skip:跳過 n 個結果,從 n+1 開始輸出
3.4 Optional
Java 程序中出現(xiàn)最多的異常就是 NullPointerException,沒有之一。Optional 的出現(xiàn)力求改變這一狀態(tài)。
Optional 對象相當于值的容器,而該值可以通過 get 方法獲取,同時 Optional 提供了很多函數用于對值進行操作,從而最大限度的避免 NullPointerException 的出現(xiàn)。
Optional 與 Stream 的用法基本類型,所提供的方法同樣分為惰性和及早求值兩類,惰性方法主要用于流程組裝,及早求值用于最終計算。
3.4.1 of
使用工廠方法 of,可以從一個值中創(chuàng)建一個 Optional 對象,如果值為 null,會報 NullPointerException。
Optional<String> dataOptional = Optional.of("a");
String data = dataOptional.get(); // data is "a"
Optional<String> dataOptional = Optional.of(null);
String data = dataOptional.get(); // throw NullPointerException
3.4.2 empty
工廠方法 empty,可以創(chuàng)建一個不包含任何值的 Optional 對象。
Optional<String> dataOptional = Optional.empty();
String data = dataOptional.get(); //throw NoSuchElementException
3.4.3 ofNullable
工廠方法 ofNullable,可將一個空值轉化成 Optional。
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
3.4.4 get、orElse、orElseGet、orElseThrow
直接求值方法,用于獲取 Optional 中值,避免空指針異常的出現(xiàn)。
Optional<String> dataOptional = Optional.of("a");
dataOptional.get(); // 獲取Optional中的值, 不存在會拋出NoSuchElementException
dataOptional.orElse("b"); //獲取Optional中的值,不存在,直接返回"B"
dataOptional.orElseGet(()-> String.valueOf(System.currentTimeMillis())); //獲取Optional中的值,不存在,對Supplier進行計算,并返回計算結果
dataOptional.orElseThrow(()-> new XXXException()); //獲取Optional中的值,不存在,拋出自定義異常
3.4.5 isPresent、ifPresent
直接求值方法,isPresent 用于判斷 Optional 中是否有值,ifPresent 接收 Consumer 對象,當 Optional 有值的情況下執(zhí)行。
Optional<String> dataOptional = Optional.of("a");
String value = null;
if (dataOptional.isPresent()){
value = dataOptional.get();
}else {
value = "";
}
//等價于
String value2 = dataOptional.orElse("");
// 當Optional中有值的時候執(zhí)行
dataOptional.ifPresent(v->System.out.println(v));
3.4.6 map
惰性求值方法。map 與 Stream 中的用法基本相同,用于對 Optional 中的值進行映射處理,從而避免了大量 if 語句嵌套,多個 map 組合成鏈,只需對最終的結果進行操作,中間過程中如果存在 null 值,之后的 map 不會執(zhí)行。
@Data
static class Order{
private Name owner;
}
@Data
static class User{
private Name name;
}
@Data
static class Name{
String firstName;
String midName;
String lastName;
}
private String getFirstName(Order order){
if (order == null){
return "";
}
if (order.getOwner() == null){
return "";
}
if (order.getOwner().getFirstName() == null){
return "";
}
return order.getOwner().getFirstName();
}
private String getFirstName(Optional<Order> orderOptional){
return orderOptional.map(order -> order.getOwner())
.map(user->user.getFirstName())
.orElse("");
}
3.4.7 filter
惰性求值,對 Optional 中的值進行過濾,如果 Optional 為 empty,直接返回 empty;如果 Optional 中存在值,則對值進行驗證,驗證通過返回原 Optional,驗證不通過返回 empty。
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
4. Lambda 下模式的進化
設計模式是人們熟悉的一種設計思路,他是軟件架構中解決通用問題的模板,將解決特定問題的最佳實踐固定下來,但設計模式本身會比較復雜,包含多個接口、若干個實現(xiàn)類,應用過程相對繁瑣,這也是影響其應用的原因之一。
Lambda 表達式大大簡化了 Java 中行為傳遞的問題,對于很多行為式設計模式而言,減少了不少構建成本。
4.1 命令模式
命令者是一個對象,其封裝了調用另一個方法的實現(xiàn)細節(jié),命令者模式使用該對象可以編寫根據運行時條件,順序調用方法的一般性代碼。
大多數命令模式中的命令對象,其實是一種行為的封裝,甚至是對其他對象內部行為的一種適配,這種情況下,Lambda 表達式并有了用武之地。
interface Command{
void act();
}
interface Editor{
void open();
void write(String data);
void save();
}
class CommandRunner{
private List<Command> commands = new ArrayList<>();
public void run(Command command){
command.act();
this.commands.add(command);
}
public void redo(){
this.commands.forEach(Command::act);
}
}
class OpenCommand implements Command{
private final Editor editor;
OpenCommand(Editor editor) {
this.editor = editor;
}
@Override
public void act() {
this.editor.open();
}
}
class WriteCommand implements Command{
private final Editor editor;
private final String data;
WriteCommand(Editor editor, String data) {
this.editor = editor;
this.data = data;
}
@Override
public void act() {
editor.write(this.data);
}
}
class SaveCommand implements Command{
private final Editor editor;
SaveCommand(Editor editor) {
this.editor = editor;
}
@Override
public void act() {
this.editor.save();
}
}
public void useCommand(){
CommandRunner commandRunner = new CommandRunner();
Editor editor = new EditorImpl();
String data1 = "data1";
String data2 = "data2";
commandRunner.run(new OpenCommand(editor));
commandRunner.run(new WriteCommand(editor, data1));
commandRunner.run(new WriteCommand(editor, data2));
commandRunner.run(new SaveCommand(editor));
}
public void useLambda(){
CommandRunner commandRunner = new CommandRunner();
Editor editor = new EditorImpl();
String data1 = "data1";
String data2 = "data2";
commandRunner.run(()->editor.open());
commandRunner.run(()->editor.write(data1));
commandRunner.run(()->editor.write(data2));
commandRunner.run(()->editor.save());
}
class EditorImpl implements Editor{
@Override
public void open() {
}
@Override
public void write(String data) {
}
@Override
public void save() {
}
}
從代碼中可見,Lambda 表達式的應用,減少了創(chuàng)建子類的負擔,增加了代碼的靈活性。
4.2 策略模式
策略模式能夠在運行時改變軟件的算法行為,其核心的實現(xiàn)思路是,使用不同的算法來解決同一個問題,然后將這些算法封裝在一個統(tǒng)一的接口背后。
可見策略模式也是一種行為行為傳遞的模式。
interface CompressionStrategy{
OutputStream compress(OutputStream outputStream) throws IOException;
}
class GzipBasedCompressionStrategy implements CompressionStrategy{
@Override
public OutputStream compress(OutputStream outputStream) throws IOException {
return new GZIPOutputStream(outputStream);
}
}
class ZipBasedCompressionStrategy implements CompressionStrategy{
@Override
public OutputStream compress(OutputStream outputStream) throws IOException {
return new ZipOutputStream(outputStream);
}
}
class Compressor{
private final CompressionStrategy compressionStrategy;
Compressor(CompressionStrategy compressionStrategy) {
this.compressionStrategy = compressionStrategy;
}
public void compress(Path inFile, File outFile) throws IOException {
try (OutputStream outputStream = new FileOutputStream(outFile)){
Files.copy(inFile, this.compressionStrategy.compress(outputStream));
}
}
}
{
Compressor gzipCompressor = new Compressor(new GzipBasedCompressionStrategy());
gzipCompressor.compress(in,out);
Compressor ziCompressor = new Compressor(new ZipBasedCompressionStrategy());
ziCompressor.compress(in,out);
}
{
Compressor gzipCompressor = new Compressor(GZIPOutputStream::new);
gzipCompressor.compress(in,out);
Compressor ziCompressor = new Compressor(ZipOutputStream::new);
ziCompressor.compress(in,out);
}
4.3 觀察者模式
觀察者模式中,被觀察者持有觀察者的一個列表,當被觀察者的狀態(tài)發(fā)送變化時,會通知觀察者。
對于一個觀察者來說,往往是對一個行為的封裝。
interface NameObserver{
void onNameChange(String oName, String nName);
}
@Data
class User {
private final List<NameObserver> nameObservers = new ArrayList<>();
@Setter(AccessLevel.PRIVATE)
private String name;
public void updateName(String nName){
String oName = getName();
setName(nName);
nameObservers.forEach(nameObserver -> nameObserver.onNameChange(oName, nName));
}
public void addObserver(NameObserver nameObserver){
this.nameObservers.add(nameObserver);
}
}
class LoggerNameObserver implements NameObserver{
@Override
public void onNameChange(String oName, String nName) {
System.out.println(String.format("old Name is %s, new Name is %s", oName, nName));
}
}
class NameChangeNoticeObserver implements NameObserver{
@Override
public void onNameChange(String oName, String nName) {
notic.send(String.format("old Name is %s, new Name is %s", oName, nName));
}
}
{
User user = new User();
user.addObserver(new LoggerNameObserver());
user.addObserver(new NameChangeNoticeObserver());
user.updateName("張三");
}
{
User user = new User();
user.addObserver((oName, nName) ->
System.out.println(String.format("old Name is %s, new Name is %s", oName, nName)));
user.addObserver((oName, nName) ->
notic.send(String.format("old Name is %s, new Name is %s", oName, nName)));
user.updateName("張三");
}
4.4 模板方法模式
模板方法將整體算法設計成一個抽象類,他有一系列的抽象方法,代表方法中可被定制的步驟,同時這個類中包含一些通用代碼,算法的每一個變種都由具體的類實現(xiàn),他們重新抽象方法,提供相應的實現(xiàn)。
模板方法,實際是行為的一種整合,內部大量用到行為的傳遞。
先看一個標準的模板方法:
interface UserChecker{
void check(User user);
}
abstract class AbstractUserChecker implements UserChecker{
@Override
public final void check(User user){
checkName(user);
checkAge(user);
}
abstract void checkName(User user);
abstract void checkAge(User user);
}
class SimpleUserChecker extends AbstractUserChecker {
@Override
void checkName(User user) {
Preconditions.checkArgument(StringUtils.isNotEmpty(user.getName()));
}
@Override
void checkAge(User user) {
Preconditions.checkArgument(user.getAge() != null);
Preconditions.checkArgument(user.getAge().intValue() > 0);
Preconditions.checkArgument(user.getAge().intValue() < 150);
}
}
{
UserChecker userChecker = new SimpleUserChecker();
userChecker.check(new User());
}
class LambdaBaseUserChecker implements UserChecker{
private final List<Consumer<User>> userCheckers = Lists.newArrayList();
public LambdaBaseUserChecker(List<Consumer<User>>userCheckers){
this.userCheckers.addAll(userCheckers);
}
@Override
public void check(User user){
this.userCheckers.forEach(userConsumer -> userConsumer.accept(user));
}
}
{
UserChecker userChecker = new LambdaBaseUserChecker(Arrays.asList(
user -> Preconditions.checkArgument(StringUtils.isNotEmpty(user.getName())),
user -> Preconditions.checkArgument(user.getAge() != null),
user -> Preconditions.checkArgument(user.getAge().intValue() > 0),
user -> Preconditions.checkArgument(user.getAge().intValue() < 150)
));
userChecker.check(new User());
}
@Data
class User{
private String name;
private Integer age;
}
在看一個 Spring JdbcTemplate,如果使用 Lambda 進行簡化:
public JdbcTemplate jdbcTemplate;
public User getUserById(Integer id){
return jdbcTemplate.query("select id, name, age from tb_user where id = ?", new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement preparedStatement) throws SQLException {
preparedStatement.setInt(1, id);
}
}, new ResultSetExtractor<User>() {
@Override
public User extractData(ResultSet resultSet) throws SQLException, DataAccessException {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
user.setAge(resultSet.getInt("age"));
return user;
}
});
}
public User getUserByIdLambda(Integer id){
return jdbcTemplate.query("select id, name, age from tb_user where id = ?",
preparedStatement -> preparedStatement.setInt(1, id),
resultSet -> {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
user.setAge(resultSet.getInt("age"));
return user;
});
}
@Data
class User {
private Integer id;
private String name;
private Integer age;
}
5. Lambda 下并發(fā)程序
并發(fā)與并行:
- 并發(fā)是兩個任務共享時間段,并行是兩個任務同一時間發(fā)生。
- 并行化是指為了縮短任務執(zhí)行的時間,將任務分解為幾個部分,然后并行執(zhí)行,這和順序執(zhí)行的工作量是一樣的,區(qū)別是多個 CPU 一起來干活,花費的時間自然減少了。
- 數據并行化。數據并行化是指將數據分為塊,為每塊數據分配獨立的處理單元。
5.1 并行化流操作
并行化流操作是 Stream 提供的一個特性,只需改變一個方法調用,就可以讓其擁有并行操作的能力。
如果已經存在一個 Stream 對象,調用他的 parallel 方法就能讓其并行執(zhí)行。
如果已經存在一個集合,調用 parallelStream 方法就能獲取一個擁有并行執(zhí)行能力的 Stream。
并行流主要解決如何高效使用多核 CPU 的事情。
@Data
class Account{
private String name;
private boolean active;
private Integer amount;
}
public int getActiveAmount(List<Account> accounts){
return accounts.parallelStream()
.filter(account -> account.isActive())
.mapToInt(account -> account.getAmount())
.sum();
}
public int getActiveAmount2(List<Account> accounts){
return accounts.stream()
.parallel()
.filter(account -> account.isActive())
.mapToInt(Account::getAmount)
.sum();
}
并行流底層使用 fork/join 框架,fork 遞歸式的分解問題,然后每個段并行執(zhí)行,最終有 join 合并結果,返回最后的值。
5.2 阻塞 IO VS 非阻塞 IO
BIO VS NIO
BIO 阻塞式 IO,是一種通用且容易理解的方式,與程序交互時通常都符合這種順序執(zhí)行的方式,但其主要的缺陷在于每個 socket 會綁定一個 Thread 進行操作,當長鏈過多時會消耗大量的 Server 資源,從而導致其擴展性性下降。
NIO 非阻塞 IO,一般指的是 IO 多路復用,可以使用一個線程同時對多個 socket 的讀寫進行監(jiān)控,從而使用少量線程服務于大量 Socket。
由于客戶端開發(fā)的簡便性,大多數的驅動都是基于 BIO 實現(xiàn),包括 MySQL、Redis、Mongo 等;在服務器端,由于其高性能的要求,基本上是 NIO 的天下,以最大限度的提升系統(tǒng)的可擴展性。
由于客戶端存在大量的 BIO 操作,我們的客戶端線程會不停的被 BIO 阻塞,以等待操作返回值,因此線程的效率會大打折扣。
如上圖,線程在 IO 與 CPU 之間不停切換,走走停停,同時線程也沒有辦法釋放,一直等到任務完成。
5.3 Future
構建并發(fā)操作的另一種方案便是 Future,F(xiàn)uture 是一種憑證,調用方法不是直接返回值,而是返回一個 Future 對象,剛創(chuàng)建的 Future 為一個空對象,由后臺線程執(zhí)行耗時操作,并在結束時將結果寫回到 Future 中。
當調用 Future 對象的 get 方法獲取值時,會有兩個可能,如果后臺線程已經運行完成,則直接返回;如果后臺線程沒有運行完成,則阻塞調用線程,知道后臺線程運行完成或超時。
使用 Future 方式,可以以并行的方式運行多個子任務。
當主線程需要調用比較耗時的操作時,可以將其放在輔助線程中執(zhí)行,并在需要數據的時候從 future 中獲取,如果輔助線程已經運行完成,則立即拿到返回的結果,如果輔助線程還沒有運行完成,則主線程等待,并在完成時獲取結果。
一種常見的場景是在Controller中從多個Service中獲取結果,并將其封裝成一個View對象返回給前端用于顯示,假設需要從三個接口中獲取結果,每個接口的平均響應時間是20ms,那按照串行模式,總耗時為sum(i1, i2, i3) = 60ms;如果按照Future并發(fā)模式將加載任務交由輔助線程處理,總耗時為max(i1, i2, i3 ) = 20ms, 大大減少了系統(tǒng)的響應時間。
private ExecutorService executorService = Executors.newFixedThreadPool(20);
private User loadUserByUid(Long uid){
sleep(20);
return new User();
}
private Address loadAddressByUid(Long uid){
sleep(20);
return new Address();
}
private Account loadAccountByUid(Long uid){
sleep(20);
return new Account();
}
/**
* 總耗時 sum(LoadUser, LoadAddress, LoadAccount) = 60ms
* @param uid
* @return
*/
public View getViewByUid1(Long uid){
User user = loadUserByUid(uid);
Address address = loadAddressByUid(uid);
Account account = loadAccountByUid(uid);
View view = new View();
view.setUser(user);
view.setAddress(address);
view.setAccount(account);
return view;
}
/**
* 總耗時 max(LoadUser, LoadAddress, LoadAccount) = 20ms
* @param uid
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
public View getViewByUid(Long uid) throws ExecutionException, InterruptedException {
Future<User> userFuture = executorService.submit(()->loadUserByUid(uid));
Future<Address> addressFuture = executorService.submit(()->loadAddressByUid(uid));
Future<Account> accountFuture = executorService.submit(()->loadAccountByUid(uid));
View view = new View();
view.setUser(userFuture.get());
view.setAddress(addressFuture.get());
view.setAccount(accountFuture.get());
return view;
}
private void sleep(long time){
try {
TimeUnit.MILLISECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Data
class View{
private User user;
private Address address;
private Account account;
}
class User{
}
class Address{
}
class Account{
}
Future 方式存在一個問題,及在調用 get 方法時會阻塞主線程,這是資源的極大浪費,我們真正需要的是一種不必調用 get 方法阻塞當前線程,就可以操作 future 對象返回的結果。
上例中只是子任務能夠拆分并能并行執(zhí)行的一種典型案例,在實際開發(fā)過程中,我們會遇到更多、更復雜的場景,比如:
- 將兩個 Future 結果合并成一個,同時第二個又依賴于第一個的結果
- 等待 Future 集合中所有記錄的完成
- 等待 Future 集合中的最快的任務完成
- 定義任務完成后的操作
對此,我們引入了 CompletableFuture 對象。
5.4 CompletableFuture
- CompletableFuture 結合了 Future 和回調兩種策略,以更好的處理事件驅動任務。
- CompletableFuture 與Stream 的設計思路一致,通過注冊 Lambda 表達式,把高階函數鏈接起來,從而定制更復雜的處理流程。
CompletableFuture 提供了一組函數用于定義流程,其中包括:
5.4.1 創(chuàng)建函數
CompletableFuture 提供了一組靜態(tài)方法用于創(chuàng)建 CompletableFuture 實例:
public static <U> CompletableFuture<U> completedFuture(U value)// :使用已經創(chuàng)建好的值,創(chuàng)建 CompletableFuture 對象。
public static CompletableFuture<Void> runAsync(Runnable runnable)// 基于 Runnable 創(chuàng)建 CompletableFuture 對象,返回值為 Void,及沒有返回值
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)// 基于 Runnable 和自定義線程池創(chuàng)建 CompletableFuture 對象,返回值為 Void,及沒有返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)// 基于 Supplier 創(chuàng)建 CompletableFuture 對象,返回值為 U
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) // 基于 Supplier 和自定義線程池創(chuàng)建 CompletableFuture 對象,返回值為 U
以 Async 結尾并且沒有指定 Executor 的方法會使用 ForkJoinPool.commonPool() 作為它的線程池執(zhí)行異步代碼。
方法的參數類型都是函數式接口,所以可以使用 Lambda 表達式實現(xiàn)異步任務。
5.4.2 計算結果完成后
當 CompletableFuture 計算完成或者計算過程中拋出異常時進行回調。
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
Action 的類型是 BiConsumer<? super T,? super Throwable> 它可以處理正常的計算結果,或者異常情況。
方法不以 Async 結尾,意味著 Action 使用相同的線程執(zhí)行,而 Async 可能會使用其他線程執(zhí)行(如果是使用相同的線程池,也可能會被同一個線程選中執(zhí)行)。
exceptionally 針對異常情況進行處理,當原始的 CompletableFuture 拋出異常的時候,就會觸發(fā)這個 CompletableFuture 的計算。
下面一組方法雖然也返回 CompletableFuture 對象,但是對象的值和原來的 CompletableFuture 計算的值不同。當原先的 CompletableFuture 的值計算完成或者拋出異常的時候,會觸發(fā)這個 CompletableFuture 對象的計算,結果由 BiFunction 參數計算而得。因此這組方法兼有 whenComplete 和轉換的兩個功能。
public <?U> CompletableFuture<?U> handle(BiFunction<? super T,Throwable,? extends U> fn)
public <?U> CompletableFuture<?U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn)
public <?U> CompletableFuture<?U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn, Executor executor)
5.4.3 轉化函數
轉化函數類似于 Stream 中的惰性求助函數,主要對 CompletableFuture 的中間結果進行流程定制。
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
通過函數完成對 CompletableFuture 中的值得轉化,Async 在線的線程池中處理,Executor 可以自定義線程池。
5.4.4 純消費函數
上面的方法當計算完成的時候,會生成新的計算結果 (thenApply, handle),或者返回同樣的計算結果 whenComplete,CompletableFuture 還提供了一種處理結果的方法,只對結果執(zhí)行 Action,而不返回新的計算值,因此計算值為 Void。
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)
其他的參數類型與之前的含義一致,不同的是函數接口 Consumer,這個接口只有輸入,沒有返回值。
thenAcceptBoth 以及相關方法提供了類似的功能,當兩個 CompletionStage 都正常完成計算的時候,就會執(zhí)行提供的 action,它用來組合另外一個異步的結果。
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<?
extends U> other, BiConsumer<? super T,? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends
U> other, BiConsumer<? super T,? super U> action)
public <U>
CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends
U> other, BiConsumer<? super T,? super U> action, Executor executor)
5.4.5. 組合函數
組合函數主要應用于后續(xù)計算需要 CompletableFuture 計算結果的場景。
public <U> CompletableFuture<U> thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn, Executor executor)
這一組方法接受一個 Function 作為參數,這個 Function 的輸入是當前的 CompletableFuture 的計算值,返回結果將是一個新的 CompletableFuture,這個新的 CompletableFuture 會組合原來的 CompletableFuture 和函數返回的 CompletableFuture。因此它的功能類似:
A +–> B +—> C
下面的一組方法 thenCombine 用來復合另外一個 CompletionStage 的結果。兩個 CompletionStage 是并行執(zhí)行的,它們之間并沒有先后依賴順序,other 并不會等待先前的 CompletableFuture 執(zhí)行完畢后再執(zhí)行,當兩個 CompletionStage 全部執(zhí)行完成后,統(tǒng)一調用 BiFunction 函數,計算最終的結果。
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<?
extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<?
extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<?
extends U> other, BiFunction<? super T,? super U,? extends V> fn,
Executor executor)
5.4.6. Either
Either 系列方法不會等兩個 CompletableFuture 都計算完成后執(zhí)行計算,而是當任意一個 CompletableFuture 計算完成的時候就會執(zhí)行。
public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)
public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T,U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn, Executor executor)
5.4.7 輔助方法
輔助方法主要指 allOf 和 anyOf,這兩個靜態(tài)方法用于組合多個 CompletableFuture。
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)// allOf方法是當所有的CompletableFuture都執(zhí)行完后執(zhí)行計算。
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)// anyOf方法是當任意一個CompletableFuture執(zhí)行完后就會執(zhí)行計算。