集合工廠
List<String> friends = Arrays.asList("Raphael", "Olivia");
friends.set(0, "Richard");
friends.add("Thibaut"); ←---- 拋出一個UnsupportedModificationException異常
通過工廠方法創建的Collection的底層是大小固定的可變數組。
:::info
JAVA 11及之前無Java中還沒有Arrays.asSet()這種工廠方法
Python、Groovy在內的多種語言都支持集合常量,可以通過譬如[42, 1, 5]這樣的語法格式創建含有三個數字的集合
Java并沒有提供集合常量的語法支持,原因是這種語言上的變化往往伴隨著高昂的維護成本,并且會限制將來可能使用的語法。與此相反,Java 9 +通過增強Collection API,另辟蹊徑地增加了對集合常量的支持。
避免不可預知的缺陷,同時以更緊湊的方式存儲內部數據,不要在工廠方法創建的列表中存放null元素
:::
重載(overloading)和變參(vararg)
如果進一步審視List接口,會發現List.of包含了多個重載的版本,包括:
static List of(E e1, E e2, E e3, E e4)
static List of(E e1, E e2, E e3, E e4, E e5)
變參版本
static List of(E… elements) 可變參
變參版本的函數需要額外分配一個數組,這個數組被封裝于列表中。
使用變參版本的方法,你就要負擔分配數組、初始化以及最后對它進行垃圾回收的開銷。
使用定長(最多為10個)元素版本的函數,就沒有這部分開銷。
:::info
注意,如果使用List.of創建超過10個元素的列表,這種情況下實際調用的還是變參類型的函數。類似的情況也會出現在Set.of和Map.of中。
:::
SET工廠
//類似于List.of 、創建不可變的Set集合
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
System.out.println(friends); ←---- [Raphael,Olivia, Thibaut]
Map工廠
Java 9中提供了兩種初始化一個不可變Map的方式
Map<String, Integer> ageOfFriends= Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
System.out.println(ageOfFriends); ←---- {Olivia=25, Raphael=30, Thibaut=26}
:::info
只需要創建不到10個鍵值對的小型Map,那么使用這種方法比較方便。
如果鍵值對的規模比較大,則可以考慮使用另外一種叫作Map.ofEntries的工廠方法,這種工廠方法接受以變長參數列表形式組織的Map.Entry<K, V>對象作為參數。
使用第二種方法,你需要創建額外的對象,從而實現對鍵和值的封裝
:::
import static java.util.Map.entry;
Map<String, Integer> ageOfFriends= Map.ofEntries(entry("Raphael", 30),entry("Olivia", 25),entry("Thibaut", 26));
System.out.println(ageOfFriends); ←---- {Olivia=25, Raphael=30, Thibaut=26}
List<String> actors = List.of("Keanu", "Jessica")
actors.set(0, "Brad");
System.out.println(actors)/**
*答案:執行該代碼片段會拋出一個UnsupportedOperationException異常,
*因為由List.of方法構造的集合對象是不可修改的。
**/
使用List和Set
Java 8在List和Set的接口中新引入了以下方法。
- removeIf移除集合中匹配指定謂詞的元素。實現了List和Set的所有類都提供了該方法(事實上,這個方法繼承自Collection接口)。
- replaceAll用于 List接口中,它使用一個函數(UnaryOperator)替換元素。
- sort也用于List接口中,對列表自身的元素進行排序。
for (Transaction transaction : transactions) {if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {transactions.remove(transaction);}
}
removeIf方法
可代替上述繁瑣步驟[]
//發現其中的問題了嗎?非常不幸,
這段代碼可能導致ConcurrentModificationException。
為什么會這樣?
因為在底層實現上,
for-each循環使用了一個迭代器對象,所以代碼的執行會像下面這樣:for (Iterator<Transaction> iterator = transactions.iterator();iterator.hasNext(); ) {Transaction transaction = iterator.next();if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {transactions.remove(transaction); ←---- 問題在這兒,我們使用了兩個不同的對象來迭代和修改集合}
}//因此,迭代器對象的狀態沒有與集合對象的狀態同步,反之亦然。
//為了解決這個問題,只能顯式地使用Iterator對象,并通過它調用remove()方法
/**
Iterator對象,它使用next()和hasNext()方法查詢源;
Collection對象,它通過調用remove()方法刪除集合中的元素。
**/
for (Iterator<Transaction> iterator = transactions.iterator();iterator.hasNext(); ) {Transaction transaction = iterator.next();if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {iterator.remove();}
}
transactions.removeIf(transaction ->Character.isDigit(transaction.getReferenceCode().charAt(0)));
replaceAll方法
referenceCodes.stream() ←---- [a12, C14, b13].map(code -> Character.toUpperCase(code.charAt(0)) +code.substring(1)).collect(Collectors.toList()).forEach(System.out::println); ←---- 輸出A12, C14, B13for (ListIterator<String> iterator = referenceCodes.listIterator();iterator.hasNext(); ) {String code = iterator.next();iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
}//缺點:把Iterator對象和集合對象混在一起使用比較容易出錯,
//特別是還需要修改集合對象的場景
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) +code.substring(1));
使用Map
Foreach
for(Map.Entry<String, Integer> entry: ageOfFriends.entrySet()) {String friend = entry.getKey();Integer age = entry.getValue();System.out.println(friend + " is " + age + " years old");
}ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " +age + " years old"));
排序
Map<String, String> favouriteMovies= Map.ofEntries(entry("Raphael", "Star Wars"),entry("Cristina", "Matrix"),entry("Olivia","James Bond"));favouriteMovies.entrySet().stream().sorted(Entry.comparingByKey()).forEachOrdered(System.out::println);
// ←---- 按照人名的字母順序對流中的元素進行排序//Cristina=Matrix
//Olivia=James Bond
//Raphael=Star Wars
getOrDefault方法
查找的鍵在Map中不存在該怎么辦。新的getOrDefault方法可以解決這一問題。
Map<String, String> favouriteMovies= Map.ofEntries(entry("Raphael", "Star Wars"),entry("Olivia", "James Bond"));System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix")); ←---- 輸出James BondSystem.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix")); ←---- 輸出Matrix/**getOrDefault以接受的第一個參數作為鍵,
第二個參數作為默認值(在Map中找不到指定的鍵時,該默認值會作為返回值)
**/
//注意,如果鍵在Map中存在,但碰巧被賦予的值是null,那么getOrDefault還是會返回null。
//此外,無論該鍵存在與否,你作為參數傳入的表達式每次都會被執行。
//判斷有無KEY
計算模式
緩存某個昂貴操作的結果,將其保存在一個鍵對應的值中。如果該鍵存在,就不需要再次展開計算。解決這個問題有三種新的途徑
- computeIfAbsent——如果指定的鍵沒有對應的值(沒有該鍵或者該鍵對應的值是空),那么使用該鍵計算新的值,并將其添加到Map中;
- computeIfAbsent的一個應用場景是緩存信息。假設你要解析一系列文件中每一個行的內容并計算它們的SHA-256值。如果你之前已經處理過這些數據,就沒有必要重復計算。
- 沒有則新增,有就替換。
- computeIfPresent——如果指定的鍵在Map中存在,就計算該鍵的新值,并將其添加到Map中;
- compute——使用指定的鍵計算新的值,并將其存儲到Map中。
import java.util.HashMap;class Main {public static void main(String[] args) {// 創建一個 HashMapHashMap<String, Integer> prices = new HashMap<>();// 往HashMap中添加映射項prices.put("Shoes", 200);prices.put("Bag", 300);prices.put("Pant", 150);System.out.println("HashMap: " + prices);// 計算 Shirt 的值int shirtPrice = prices.computeIfAbsent("Shirt", key -> 280);System.out.println("Price of Shirt: " + shirtPrice);// 輸出更新后的HashMapSystem.out.println("Updated HashMap: " + prices);}/**
HashMap: {Pant=150, Bag=300, Shoes=200}
Price of Shirt: 280
Updated HashMap: {Pant=150, Shirt=280, Bag=300, Shoes=200}
**/public static void main(String[] args) {// 創建一個 HashMapHashMap<String, Integer> prices = new HashMap<>();// 往HashMap中添加映射關系prices.put("Shoes", 180);prices.put("Bag", 300);prices.put("Pant", 150);System.out.println("HashMap: " + prices);// Shoes中的映射關系已經存在// Shoes并沒有計算新值int shoePrice = prices.computeIfAbsent("Shoes", (key) -> 280);System.out.println("Price of Shoes: " + shoePrice);// 輸出更新后的 HashMapSystem.out.println("Updated HashMap: " + prices);}/**HashMap: {Pant=150, Bag=300, Shoes=180}
Price of Shoes: 180
Updated HashMap: {Pant=150, Bag=300, Shoes=180}
**/}
刪除模式
String key = "Raphael";
String value = "Jack Reacher 2";
if (favouriteMovies.containsKey(key) &&Objects.equals(favouriteMovies.get(key), value)) {favouriteMovies.remove(key);return true;
}
else {return false;
}//等效于
favouriteMovies.remove(key, value);//如果找不到建議用K、V
替換模式
Map<String, String> favouriteMovies = new HashMap<>(); ←---- 因為要使用replaceAll方法,所以只能創建可變的Map
favouriteMovies.put("Raphael", "Star Wars");
favouriteMovies.put("Olivia", "james bond");
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
System.out.println(favouriteMovies); ←---- {Olivia=JAMES BOND, Raphael=STAR WARS}
merge方法
//沒有重復的KEY
Map<String, String> family = Map.ofEntries(entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(entry("Raphael", "Star Wars"));
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends); ←---- 復制friends的所有條目到everyone中
System.out.println(everyone); ←---- {Cristina=James Bond, Raphael= Star Wars, Teo=Star Wars}//可能含有重復的KEY
Map<String, String> family = Map.ofEntries(entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));Map<String, String> everyone = new HashMap<>(family);
friends.forEach((k, v) ->everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2));
//←---- 如果存在重復的鍵,就連接兩個值
System.out.println(everyone);
//←---- 輸出{Raphael=Star Wars, Cristina=JamesBond & Matrix, Teo=Star Wars}/**
如果指定的鍵并沒有關聯值,或者關聯的是一個空值,那么[merge]會將它關聯到指定的非空值。否則,[merge]會用給定映射函數的[返回值]替換該值,如果映射函數的返回值為空就刪除[該鍵]
**/Map<String, Long> moviesToCount = new HashMap<>();
String movieName = "James Bond";
long count = moviesToCount.get(movieName);
if(count == null) {moviesToCount.put(movieName, 1);
}
else {moviesToCount.put(moviename, count + 1);
}
moviesToCount.merge(movieName, 1L, (key, count) -> count + 1L);
/**
傳遞給merge方法的第二個參數是1L。Javadoc文檔中說該參數是“與鍵關聯的非空值,該值將與現有的值合并,如果沒有當前值,或者該鍵關聯的當今值為空,就將該鍵關聯到非空值”。因為該鍵的返回值是空,所以第一輪里鍵的值被賦值為1。接下來的一輪,由于鍵已經初始化為1,因此后續的操作由BiFunction方法對count進行遞增。
**/Map<String, Long> moviesToCount = new HashMap<>();String movieName = "James Bond";Long count = moviesToCount.get(movieName);if (count == null) {moviesToCount.put(movieName, 1L);}else {moviesToCount.put(movieName, count + 1);}moviesToCount.merge(movieName, 1L, (key, value) -> value + 1L);System.out.println(moviesToCount);
// {James Bond=2}
總結
Map<String, Integer> movies = new HashMap<>();
movies.put("JamesBond", 20);
movies.put("Matrix", 15);
movies.put("Harry Potter", 5);
Iterator<Map.Entry<String, Integer>> iterator =movies.entrySet().iterator();
while(iterator.hasNext()) {Map.Entry<String, Integer> entry = iterator.next();if(entry.getValue() < 10) {iterator.remove();}
}
System.out.println(movies); ←---- {Matrix=15, JamesBond=20}
/**
答案:可以對Map的集合項使用removeIf方法,該方法接受一個謂詞,依據謂詞的結果刪除元素。
**/movies.entrySet().removeIf(entry -> entry.getValue() < 10);
改進的ConcurrentHashMap
引入ConcurrentHashMap類是為了提供一個更加現代的HashMap,以更好地應對高并發的場景。ConcurrentHashMap允許執行并發的添加和更新操作,其內部實現基于分段鎖。與另一種解決方案——同步式的Hashtable相比較,ConcurrentHashMap的讀寫性能都更好(注意,標準的HashMap是不帶同步的)。
歸約和搜索
已學
- forEach——對每個(鍵, 值)對執行指定的操作;
- reduce——依據歸約函數整合所有(鍵, 值)對的計算結果;
- search——對每個(鍵, 值)對執行一個函數,直到函數取得一個非空值。
每種操作支持四種形式的參數,接受函數使用鍵、值、Map.Entry以及(鍵, 值)對作為參數:
- 使用鍵(forEachKey,reduceKeys,searchKeys);
- 使用值(forEachValue,reduceValues,searchValues);
- 使用Map.Entry對象(forEachEntry,reduceEntries,searchEntries);
- 使用鍵和值(forEach,reduce,search)
:::info
所有這些操作都不會對ConcurrentHashMap的狀態上鎖,它們只是在運行中動態地對對象加鎖。執行操作的函數不應對執行順序或其他對象或可能在運行中變化的值有任何的依賴。
規則!
- 此外,還需要為所有操作設定一個并行閾值。如果當前Map的規模比指定的閾值小,方法就只能順序執行。
- 使用通用線程池時,如果把并行閾值設置為1將獲得最大的并行度。
- 將閾值設定為Long.MAX_VALUE時,方法將以單線程的方式運行。
除非軟件架構經過高度的資源優化,否則通常情況下,建議遵守這些原則。
:::
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>(); ←---- 一個可能有多個鍵和值更新的ConcurrentHashMap對象
long parallelismThreshold = 1;
Optional<Integer> maxValue =Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));//請留意,int、long、double等基礎類型的歸約操作(reduceValuesToInt、reduceKeysToLong等)
//會更加高效,因為它們沒有額外的封裝開銷
計數
ConcurrentHashMap類提供了一個新的mappingCount方法,能以長整形long返回Map中的映射數目。
應該盡量在新的代碼中使用它,而不是繼續使用返回int的size方法。
這樣做能讓你的代碼更具擴展性,更好地適應將來的需要,因為總有一天Map中映射的數目可能會超過int能表示的范疇。
Set視圖
ConcurrentHashMap類還提供了一個新的keySet方法,該方法以Set的形式返回ConcurrentHashMap的一個視圖(Map中的變化會反映在返回的Set中,反之亦然)。
也可以使用新的靜態方法newKeySet創建一個由ConcurrentHashMap構成的Set。
Collection API增強
Java 9支持集合工廠,使用List.of、Set.of、Map.of以及Map.ofEntries可以創建小型不可變的List、Set和Map。
集合工廠返回的對象都是不可變的,這意味著創建之后你不能修改它們的狀態。
List接口支持默認方法removeIf、replaceAll和sort。
Set接口支持默認方法removeIf。
Map接口為常見模式提供了幾種新的默認方法,并降低了出現缺陷的概率。
ConcurrentHashMap支持從Map中繼承的新默認方法,并提供了線程安全的實現。
重構、測試和調試
:::info
如何使用Lambda表達式重構代碼
Lambda表達式對面向對象的設計模式的影響
Lambda表達式的測試
如何調試使用Lambda表達式和Stream API的代碼
:::
改善可讀性
- 重構代碼,用Lambda表達式取代匿名類;
- 用方法引用重構Lambda表達式;
- 用Stream API重構命令式的數據處理。
從匿名類到Lambda表達式的轉換
Runnable r1 = new Runnable(){ ←---- 傳統的方式,使用匿名類public void run(){System.out.println("Hello");}
};
Runnable r2 = () -> System.out.println("Hello"); ←---- 新的方式,使用Lambda表達式
int a = 10;
Runnable r1 = () -> {int a = 2; ←---- 編譯錯誤System.out.println(a);
};
Runnable r2 = new Runnable(){public void run(){int a = 2; ←---- 一切正常!System.out.println(a);}
};
//在涉及重載的上下文里,將匿名類轉換為Lambda表達式可能導致最終的代碼更加晦澀。實際上,匿名類的類型是在初始化時確定的,而Lambda的類型取決于它的上下文
interface Task{public void execute();
}
public static void doSomething(Runnable r){ r.run(); }
public static void doSomething(Task a){ a.execute(); }//這種匿名類轉換為Lambda表達式時,就導致了一種晦澀的方法調用,因為Runnable和Task都是合法的目標類型:
doSomething(() -> System.out.println("Danger danger!!"));
//←---- 麻煩來了:doSomething(Runnable)和doSomething(Task)都匹配該類型
doSomething((Task)() -> System.out.println("Danger danger!!"));/**
大部分主流開發環境 支持自動檢查重構
**/
從Lambda表達式到方法引用的轉換
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =menu.stream().collect(groupingBy(dish -> {if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;}));//簡約寫法
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =menu.stream().collect(groupingBy(Dish::getCaloricLevel)); ←---- 將Lambda表達式抽取到一個方法內
//新增一個類 方法引用
public class Dish{...public CaloricLevel getCaloricLevel(){if (this.getCalories() <= 400) return CaloricLevel.DIET;else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;}
}
靜態輔助方法
comparing和maxBy。結合方法引用一起使用
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); ←---- 你需要考慮如何實現比較算法
inventory.sort(comparing(Apple::getWeight)); ←---- 讀起來就像問題描述,非常清晰
通用的歸約操作,比如sum和maximum
int totalCalories =menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);
//內置的集合類,它能更清晰地表達問題陳述是什么。使用了集合類summingInt(方法的名詞很直觀地解釋了它的功能):
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
從命令式的數據處理切換到Stream
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){if(dish.getCalories() > 300){dishNames.add(dish.getName());}
}
//替換
menu.parallelStream().filter(d -> d.getCalories() > 300).map(Dish::getName).collect(toList());
增加代碼的靈活性
- 重構代碼,用Lambda表達式取代匿名類;
- 用方法引用重構Lambda表達式;
- 用Stream API重構命令式的數據處理。
采用函數接口
String oneLine =processFile((BufferedReader b) -> b.readLine()); ←---- 傳入一個Lambda表達式
String twoLines =processFile((BufferedReader b) -> b.readLine() + b.readLine()); ←---- 傳入另一個Lambda表達式
public static String processFile(BufferedReaderProcessor p) throwsIOException {try(BufferedReader br = new BufferedReader(newFileReader("ModernJavaInAction/chap9/data.txt"))) {return p.process(br); ←---- 將BufferedReaderProcessor作為執行參數傳入}
}
public interface BufferedReaderProcessor { ←---- 使用Lambda表達式的函數接口,該接口能夠拋出一個IOExceptionString process(BufferedReader b) throws IOException;
}
使用Lambda重構面向對象的設計模式
- 訪問者模式常用于分離程序的算法和它的操作對象。
- 單例模式一般用于限制類的實例化,僅生成一份對象。
- 其他21種設計模式…
策略模式
策略模式代表了解決一類算法的通用解決方案,你可以在運行時選擇使用哪種方案。
可使用不同的標準來驗證輸入的有效性,使用不同的方式來分析或者格式化輸入。
策略模式包含三部分內容
- 一個代表某個算法的接口(Strategy接口)。
- 一個或多個該接口的具體實現,它們代表了算法的多種實現(比如,實體類ConcreteStrategyA或者ConcreteStrategyB)。
- 一個或多個使用策略對象的客戶
//假設希望驗證輸入的內容是否根據標準進行了恰當的格式化(比如只包含小寫字母或數字)。
//可以從定義一個驗證文本(以String的形式表示)的接口入手
public interface ValidationStrategy {boolean execute(String s);
}//其次,定義了該接口的一個或多個具體實現:
public class IsAllLowerCase implements ValidationStrategy {public boolean execute(String s){return s.matches("[a-z]+");}
}
public class IsNumeric implements ValidationStrategy {public boolean execute(String s){return s.matches("\\d+");}
}// 實際情況
public class Validator{private final ValidationStrategy strategy;public Validator(ValidationStrategy v){this.strategy = v;}public boolean validate(String s){return strategy.execute(s);}
}
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaaa"); ←---- 返回false
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
boolean b2 = lowerCaseValidator.validate("bbbb"); ←---- 返回true
:::info
ValidationStrategy是一個函數接口了。
除此之外,它還與Predicate具有同樣的函數描述。這意味著我們不需要聲明新的類來實現不同的策略,通過直接傳遞Lambda表達式就能達到同樣的目的,并且還更簡潔
:::
Validator numericValidator =new Validator((String s) -> s.matches("[a-z]+")); (以下4行)直接傳遞Lambda表達式
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");//Lambda表達式避免了采用策略設計模式時僵化的模板代碼。
//Lambda表達式實際已經對部分代碼(或策略)進行了封裝,
//而這就是創建策略設計模式的初衷
模板方法
采用某個算法的框架,同時又希望有一定的靈活度,能對它的某些部分進行改進,那么采用模板方法設計模式是比較通用的方案。
換句話說,模板方法模式在你“希望使用這個算法,但是需要對其中的某些行進行改進,才能達到希望的效果”時是非常有用的。
/**
需要編寫一個簡單的在線銀行應用。通常,用戶需要輸入一個用戶賬戶,
之后應用才能從銀行的數據庫中得到用戶的詳細信息,最終完成一些讓用戶滿意的操作。
不同分行的在線銀行應用讓客戶滿意的方式可能略有不同,比如給客戶的賬戶發放紅利,
或者僅僅是少發送一些推廣文件。
你可能通過下面的抽象類方式來實現在線銀行應用
**/
abstract class OnlineBanking {public void processCustomer(int id) {Customer c = Database.getCustomerWithId(id);makeCustomerHappy(c);}abstract void makeCustomerHappy(Customer c);// dummy Customer classstatic class Customer {}// dummy Database classstatic private class Database {static Customer getCustomerWithId(int id) {return new Customer();}}}
//繼承
public class MyOnlineBanking extends OnlineBanking {@Overridevoid makeCustomerHappy(Customer c) {// 實現具體的邏輯來使客戶滿意System.out.println("Customer with ID " + c + " is happy now!");}
}
//調用和使用
public class Main {public static void main(String[] args) {MyOnlineBanking banking = new MyOnlineBanking();banking.processCustomer(123); // 傳入客戶的ID}
}
等同于
public class OnlineBankingLambda {public static void main(String[] args) {new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Customer with ID " + c.toString() + " is happy now!"));}public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {Customer c = Database.getCustomerWithId(id);makeCustomerHappy.accept(c);}// dummy Customer classstatic private class Customer {}// dummy Database classstatic private class Database {static Customer getCustomerWithId(int id) {return new Customer();}}}
觀察者模式
某些事件發生時(比如狀態轉變),如果一個對象(通常稱之為主題)需要自動地通知其他多個對象(稱為觀察者),就會采用該方案。
創建圖形用戶界面(GUI)程序時,經常會使用該設計模式。這種情況下,你會在圖形用戶界面組件(比如按鈕)上注冊一系列的觀察者。
如果點擊按鈕,觀察者就會收到通知,并隨即執行某個特定的行為。但是觀察者模式并不局限于圖形用戶界面。比如,觀察者設計模式也適用于股票交易的情形,多個券商(觀察者)可能都希望對某一支股票價格(主題)的變動做出響應。
/**
Twitter這樣的應用設計并實現一個定制化的通知系統。
想法很簡單:好幾家報紙機構,比如美國《紐約時報》、英國《衛報》以及法國《世界報》都訂閱了新聞推文,
他們希望當接收的新聞中包含他們感興趣的關鍵字時,能得到特別通知。
**/public class ObserverMain {public static void main(String[] args) {Feed f = new Feed();//新聞中不同的關鍵字分別定義不同的行為f.registerObserver(new NYTimes());f.registerObserver(new Guardian());f.registerObserver(new LeMonde());f.notifyObservers("The queen said her favourite book is Java 8 & 9 in Action!");Feed feedLambda = new Feed();//Observer接口的所有實現類都提供了一個方法:notifyfeedLambda.registerObserver((String tweet) -> {if (tweet != null && tweet.contains("money")) {System.out.println("Breaking news in NY! " + tweet);}});feedLambda.registerObserver((String tweet) -> {if (tweet != null && tweet.contains("queen")) {System.out.println("Yet another news in London... " + tweet);}});feedLambda.notifyObservers("Money money money, give me money!");}interface Observer {void inform(String tweet);}interface Subject {void registerObserver(Observer o);void notifyObservers(String tweet);}static private class NYTimes implements Observer {@Overridepublic void inform(String tweet) {if (tweet != null && tweet.contains("money")) {System.out.println("Breaking news in NY!" + tweet);}}}static private class Guardian implements Observer {@Overridepublic void inform(String tweet) {if (tweet != null && tweet.contains("queen")) {System.out.println("Yet another news in London... " + tweet);}}}static private class LeMonde implements Observer {@Overridepublic void inform(String tweet) {if (tweet != null && tweet.contains("wine")) {System.out.println("Today cheese, wine and news! " + tweet);}}}static private class Feed implements Subject {private final List<Observer> observers = new ArrayList<>();@Overridepublic void registerObserver(Observer o) {observers.add(o);}@Overridepublic void notifyObservers(String tweet) {observers.forEach(o -> o.inform(tweet));}}}
:::info
是否隨時隨地都可以使用Lambda表達式呢?
答案是否定的!前文介紹的例子中,Lambda適配得很好,
那是因為需要執行的動作都很簡單,因此才能很方便地消除僵化代碼。
但是,觀察者的邏輯有可能十分復雜,它們可能還持有狀態,抑或定義了多個方法,
諸如此類。在這些情形下,還是應該繼續使用類的方式
:::
責任鏈模式
責任鏈模式是一種創建處理對象序列(比如操作序列)的通用方案。一個處理對象可能需要在完成一些工作之后,將結果傳遞給另一個對象,這個對象接著做一些工作,再轉交給下一個處理對象,以此類推。
通常,這種模式是通過定義一個代表處理對象的抽象類來實現的,在抽象類中會定義一個字段來記錄后續對象。一旦對象完成它的工作,處理對象就會將它的工作轉交給它的后繼
public abstract class ProcessingObject<T> {protected ProcessingObject<T> successor;public void setSuccessor(ProcessingObject<T> successor){this.successor = successor;}public T handle(T input){T r = handleWork(input);if(successor != null){return successor.handle(r);}return r;}abstract protected T handleWork(T input);
}
以UML的方式闡釋了責任鏈模式
模板方法設計模式。handle方法提供了如何進行工作處理的框架。不同的處理對象可以通過繼承ProcessingObject類,提供handleWork方法來進行創建。
public class ChainOfResponsibilityMain {public static void main(String[] args) {ProcessingObject<String> p1 = new HeaderTextProcessing();ProcessingObject<String> p2 = new SpellCheckerProcessing();p1.setSuccessor(p2);String result1 = p1.handle("Aren't labdas really sexy?!!");System.out.println(result1);UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);String result2 = pipeline.apply("Aren't labdas really sexy?!!");System.out.println(result2);}private static abstract class ProcessingObject<T> {protected ProcessingObject<T> successor;public void setSuccessor(ProcessingObject<T> successor) {this.successor = successor;}public T handle(T input) {T r = handleWork(input);if (successor != null) {return successor.handle(r);}return r;}abstract protected T handleWork(T input);}private static class HeaderTextProcessing extends ProcessingObject<String> {@Overridepublic String handleWork(String text) {return "From Raoul, Mario and Alan: " + text;}}private static class SpellCheckerProcessing extends ProcessingObject<String> {@Overridepublic String handleWork(String text) {return text.replaceAll("labda", "lambda");}}}
UnaryOperator<String> headerProcessing =(String text) -> "From Raoul, Mario and Alan: " + text; ←---- 第一個處理對象
UnaryOperator<String> spellCheckerProcessing =(String text) -> text.replaceAll("labda", "lambda"); ←---- 第二個處理對象
Function<String, String> pipeline =headerProcessing.andThen(spellCheckerProcessing); ←---- 將兩個方法結合起來,結果就是一個操作鏈
String result = pipeline.apply("Aren't labdas really sexy?!!");
:::info
處理對象作為Function<String, String>的一個實例,或者更確切地說作為UnaryOperator的一個實例。andThen方法對其進行構造。
:::
工廠模式
無須向客戶暴露實例化的邏輯就能完成對象的創建。假定你為一家銀行工作,他們需要一種方式創建不同的金融產品:貸款、期權、股票,等等。
通常,你會創建一個工廠類,它包含一個負責實現不同對象的方法
public class FactoryMain {public static void main(String[] args) {Product p1 = ProductFactory.createProduct("loan");System.out.printf("p1: %s%n", p1.getClass().getSimpleName());Supplier<Product> loanSupplier = Loan::new;Product p2 = loanSupplier.get();System.out.printf("p2: %s%n", p2.getClass().getSimpleName());Product p3 = ProductFactory.createProductLambda("loan");System.out.printf("p3: %s%n", p3.getClass().getSimpleName());}static private class ProductFactory {public static Product createProduct(String name) {switch (name) {case "loan":return new Loan();case "stock":return new Stock();case "bond":return new Bond();default:throw new RuntimeException("No such product " + name);}}public static Product createProductLambda(String name) {Supplier<Product> p = map.get(name);if (p != null) {return p.get();}throw new RuntimeException("No such product " + name);}}static private interface Product {}static private class Loan implements Product {}static private class Stock implements Product {}static private class Bond implements Product {}final static private Map<String, Supplier<Product>> map = new HashMap<>();static {map.put("loan", Loan::new);map.put("stock", Stock::new);map.put("bond", Bond::new);}}
:::info
Java 8中的新特性達到了傳統工廠模式同樣的效果。
但是,如果工廠方法createProduct需要接受多個傳遞給產品構造方法的參數,那這種方式的擴展性不是很好。所以除了簡單的Supplier接口外,你還必須提供一個函數接口。
假設希望保存具有三個參數(兩個參數為Integer類型,一個參數為String類型)的構造函數。為了完成這個任務,需要創建一個特殊的函數接口TriFunction。最終的結果是Map變得更加復雜。
:::
public interface TriFunction<T, U, V, R>{R apply(T t, U u, V v);
}
Map<String, TriFunction<Integer, Integer, String, Product>> map= new HashMap<>();
測試Lambda表達式
將復雜的Lambda表達式分為不同的方法
高階函數的測試
調試
程序員的兵器庫里有兩大經典武器,分別是:
- 查看棧跟蹤;
- 輸出日志。
查看棧跟蹤
程序突然停止運行(比如突然拋出一個異常),這時首先要調查程序在什么地方發生了異常以及為什么會發生該異常。這時棧幀就非常有用了。程序的每次方法調用都會產生相應的調用信息,包括程序中方法調用的位置、該方法調用使用的參數,以及被調用方法的本地變量。這些信息被保存在棧幀上。
程序失敗時,會得到它的棧跟蹤,通過一個又一個棧幀,可以了解程序失敗時的概略信息。
通過這些能得到程序失敗時的方法調用列表。這些方法調用列表最終會幫助你發現問題出現的原因。由于Lambda表達式沒有名字,因此棧跟蹤可能很難分析
import java.util.*;
public class Debugging{public static void main(String[] args) {List<Point> points = Arrays.asList(new Point(12, 2), null);points.stream().map(p -> p.getX()).forEach(System.out::println);}
}
//錯誤異常
Exception in thread "main" java.lang.NullPointerExceptionat Debugging.lambda$main$0(Debugging.java:6) ←---- 這行中的$0是什么意思?at Debugging$$Lambda$5/284720968.apply(Unknown Source)at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
...//如果方法引用指向的是同一個類中聲明的方法,那么它的名稱是可以在棧跟蹤中顯示的
import java.util.*;
public class Debugging{public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3);numbers.stream().map(Debugging::divideByZero).forEach(System.out::println);}public static int divideByZero(int n){return n / 0;}
}//方法divideByZero在棧跟蹤中就正確地顯示了:Exception in thread "main" java.lang.ArithmeticException: / by zeroat Debugging.divideByZero(Debugging.java:10) ←---- divideByZero正確地輸出到棧跟蹤中at Debugging$$Lambda$1/999966131.apply(Unknown Source)at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
...
使用日志調試
//可以像下面的例子那樣,使用forEach將流操作的結果日志輸出到屏幕上或者記錄到日志文件中:List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
numbers.stream().map(x -> x + 17).filter(x -> x % 2 == 0).limit(3).forEach(System.out::println);
//一旦調用forEach,整個流就會恢復運行
/**
到底哪種方式能更有效地幫助我們理解Stream流水線中的
每個操作(比如map、filter、limit)產生的輸出呢?
**/
:::info
流操作方法peek大顯身手
peek的設計初衷就是在流的每個元素恢復運行之前,插入執行一個動作。
但是它不像forEach那樣恢復整個流的運行,
而是在一個元素上完成操作之后,只會將操作順承到流水線中的下一個操作
:::
List<Integer> result =numbers.stream().peek(x -> System.out.println("from stream: " + x)) ←---- 輸出來自數據源的當前元素值.map(x -> x + 17).peek(x -> System.out.println("after map: " + x)) ←---- 輸出map操作的結果.filter(x -> x % 2 == 0).peek(x -> System.out.println("after filter: " + x)) ←---- 輸出經過filter操作之后,剩下的元素個數.limit(3).peek(x -> System.out.println("after limit: " + x)) ←---- 輸出經過limit操作之后,剩下的元素個數.collect(toList());
- Lambda表達式能提升代碼的可讀性和靈活性。
- 如果你的代碼中使用了匿名類,那么盡量用Lambda表達式替換它們,但是要注意二者間語義的微妙差別,比如關鍵字this,以及變量隱藏。
- 跟Lambda表達式比起來,方法引用的可讀性更好。
- 盡量使用Stream API替換迭代式的集合處理。
- Lambda表達式有助于避免使用面向對象設計模式時容易出現的僵化的模板代碼,典型的比如策略模式、模板方法、觀察者模式、責任鏈模式,以及工廠模式。
- 即使采用了Lambda表達式,也同樣可以進行單元測試,但是通常你應該關注使用了Lambda表達式的方法的行為。
- 盡量將復雜的Lambda表達式抽象到普通方法中。
- Lambda表達式會讓棧跟蹤的分析變得更為復雜。
- 流提供的peek方法在分析Stream流水線時,能將中間變量的值輸出到日志中,是非常有用的工具。
基于Lambda的領域特定語言
- 領域特定語言(domain-specifc language, DSL)及其形式
- 為你的API添加DSL都有哪些優缺點
- 除了簡單的基于Java的DSL之外,JVM還有哪些領域特定語言可供選擇
- 從現代Java接口和類中學習領域特定語言
- 高效實現基于Java的DSL都有哪些模式和技巧
- 常見Java庫以及工具是如何使用這些模式的
JVM提供了第三個備選項,這是一種介于內部DSL與外部DSL之間的解決方案:可以在JVM上運行另一種通用編程語言,而這種語言比Java自身更靈活、更有表現力,譬如Scala,或者Groovy。
把這樣的第三種選項稱為“多語言DSL”(polyglot DSL)
DSL具有以下優點。
- 簡潔——DSL提供的API非常貼心地封裝了業務邏輯,避免編寫重復的代碼,最終代碼將會非常簡潔。
- 可讀性——DSL使用領域中的術語描述功能和行為,讓代碼的邏輯很容易理解,即使是不懂代碼的非領域專家也能輕松上手。由于DSL的這個特性,代碼和領域知識能在你的組織內無縫地分享與溝通。
- 可維護性——構建于設計良好的DSL之上的代碼既易于維護又便于修改。可維護性對于業務相關的代碼尤其重要,應用這部分的代碼很可能需要經常變更。
- 高層的抽象性——DSL中提供的操作與領域中的抽象在同一層次,因此隱藏了那些與領域問題不直接相關的細節。
- 專注——使用專門為表述業務領域規則而設計的語言,可以幫助程序員更專注于代碼的某個部分。結果是生產效率得到了提升。
- 關注點隔離——使用專用的語言描述業務邏輯使得與業務相關的代碼可以同應用的基礎架構代碼相分離。以這種方式設計的代碼將更容易維護。
弊端
- DSL的設計比較困難——要想用精簡有限的語言描述領域知識本身就是件困難的事情。
- 開發代價——向代碼庫中加入DSL是一項長期投資,尤其是其啟動開銷很大,這在項目的早期可能導致進度延遲。此外,DSL的維護和演化還需要占用額外的工程開銷。額外的中間層——DSL會在額外的一層中封
- 裝領域模型,這一層的設計應該盡可能地薄,只有這樣才能避免帶來性能問題。
- 又一門要掌握的語言——當今時代,開發者已經習慣了使用多種語言進行開發。然而,在你的項目中加入新的DSL意味著你和你的團隊又需要掌握一門新的語言。如果你決定在你的項目中使用多個DSL以處理來自不同業務領域的作業,并將它們無縫地整合在一起,那這種代價就更大了,因為DSL的演化也是各自獨立的。
- 宿主語言的局限性——有些通用型的語言(比如Java)一向以其煩瑣和僵硬而聞名。這些語言使得設計一個用戶友好的DSL變得相當困難。實際上,構建于這種煩瑣語言之上的DSL已經受限于其臃腫的語法,使得其代碼幾乎不具備可讀性。好消息是,Java 8引入的Lambda表達式提供了一個強大的新工具可以緩解這個問題。
import static java.util.stream.Collectors.groupingBy;public class GroupingBuilder<T, D, K> {private final Collector<? super T, ?, Map<K, D>> collector;private GroupingBuilder(Collector<? super T, ?, Map<K, D>> collector) {this.collector = collector;}public Collector<? super T, ?, Map<K, D>> get() {return collector;}public <J> GroupingBuilder<T, Map<K, D>, J>after(Function<? super T, ? extends J> classifier) {return new GroupingBuilder<>(groupingBy(classifier, collector));}public static <T, D, K> GroupingBuilder<T, List<T>, K>groupOn(Function<? super T, ? extends K> classifier) {return new GroupingBuilder<>(groupingBy(classifier));}
}
public class MethodChainingOrderBuilder {public final Order order = new Order(); ←---- 由構建器封裝的訂單對象private MethodChainingOrderBuilder(String customer) {order.setCustomer(customer);}public static MethodChainingOrderBuilder forCustomer(String customer) {return new MethodChainingOrderBuilder(customer); ←---- 靜態工廠方法,用于創建指定客戶訂單的構建器}public TradeBuilder buy(int quantity) {return new TradeBuilder(this, Trade.Type.BUY, quantity); ←---- 創建一個TradeBuilder,構造一個購買股票的交易}public TradeBuilder sell(int quantity) {return new TradeBuilder(this, Trade.Type.SELL, quantity); ←---- 創建一個TradeBuilder,構造一個賣出股票的交易}public MethodChainingOrderBuilder addTrade(Trade trade) {order.addTrade(trade); ←---- 向訂單中添加交易return this; ←---- 返回訂單構建器自身,允許你流暢地創建和添加新的交易}public Order end() {return order; ←---- 終止創建訂單并返回它}
}
DSL 優點
模式名 | 優點 | 缺點 |
---|---|---|
方法鏈接 | □ 方法名可以作為關鍵字參數 | |
□ 與optional參數的兼容性很好 | ||
□ 可以強制DSL的用戶按照預定義的順序調用方法 | ||
□ 很少使用或者基本不使用靜態方法 | ||
□ 可能的語法噪聲很低 | □ 實現起來代碼很冗長 | |
□ 需要使用膠水語言整合多個構建器 | ||
□ 領域對象的層級只能通過代碼的縮進公約定義 | ||
嵌套函數 | □ 實現代碼比較簡潔 | |
□ 領域對象的層次與函數嵌套保持一致 | □ 大量使用了靜態方法 | |
□ 參數通過位置而非變量名識別 | ||
□ 支持可選參數需要實現重載方法 | ||
使用Lambda的函數序列 | □ 對可選參數的支持很好 | |
□ 很少或者基本不使用靜態方法 | ||
□ 領域對象的層次與Lambda的嵌套保持一致 | ||
□ 不需要為支持構建器而使用膠水語言 | □ 實現代碼很冗長 | |
□ DSL中的Lambda表達式會帶來更多的語法噪聲 |
JOOQ
SELECT * FROM BOOK
WHERE BOOK.PUBLISHED_IN = 2016
ORDER BY BOOK.TITLE;
create.selectFrom(BOOK).where(BOOK.PUBLISHED_IN.eq(2016)).orderBy(BOOK.TITLE);
:::info
jOOQ DSL選擇使用的主要DSL模式是方法鏈接
:::
public class BuyStocksSteps {private Map<String, Integer> stockUnitPrices = new HashMap<>();private Order order = new Order();@Given("^the price of a \"(.*?)\" stock is (\\d+)\\$$") ←---- 定義該場景的前置條件和股票的單位價格public void setUnitPrice(String stockName, int unitPrice) {stockUnitValues.put(stockName, unitPrice); ←---- 保存股票的單位價格}@When("^I buy (\\d+) \"(.*?)\"$") ←---- 定義測試領域模型時的動作public void buyStocks(int quantity, String stockName) {Trade trade = new Trade(); ←---- 生成相應的領域模型trade.setType(Trade.Type.BUY);Stock stock = new Stock();stock.setSymbol(stockName);trade.setStock(stock);trade.setPrice(stockUnitPrices.get(stockName));trade.setQuantity(quantity);order.addTrade(trade);}@Then("^the order value should be (\\d+)\\$$") ←---- 定義期望的場景輸出public void checkOrderValue(int expectedValue) {assertEquals(expectedValue, order.getValue()); ←---- 檢查測試的斷言}
}//Java 8引入的Lambda表達式賦予了Cucumber新的活力,借助于新語法,
//可以使用帶兩個參數的方法替換掉注釋,
//這兩個參數分別是:包含之前注釋中期望值的正則表達式以及實現測試方法的Lambda表達式。
//使用第二種標記法,你可以像下面這樣重寫測試場景public class BuyStocksSteps implements cucumber.api.java8.En {private Map<String, Integer> stockUnitPrices = new HashMap<>();private Order order = new Order();public BuyStocksSteps() {Given("^the price of a \"(.*?)\" stock is (\\d+)\\$$",(String stockName, int unitPrice) -> {stockUnitValues.put(stockName, unitPrice);});// ……為了簡潔起見,我們省略了更多的Lambda,譬如什么情況要做什么}
}
Spring Integration
在基于Spring的應用中開發輕量級的遠程服務(remoting)、消息(messaging),以及計劃任務(scheduling)都很方便。這些特性可以借由形式豐富的流暢DSL實現,而這并不只是基于Spring傳統XML配置文件構建的語法糖。
Spring Integration實現了創建基于消息的應用所需的所有常用模式,包括管道(channel)、消息處理節點(endpoint)、輪詢器(poller)、管道攔截器(channel interceptor)。為了改善可讀性,處理節點在該DSL中被表述為動詞,集成的過程就是將這些處理節點組合成一個或多個消息流。
下面這段代碼就是一個展示Spring Integration如何工作的例子,雖然簡單,但是“五臟俱全”。
@Configuration
@EnableIntegration
public class MyConfiguration {@Beanpublic MessageSource<?> integerMessageSource() {MethodInvokingMessageSource source =new MethodInvokingMessageSource(); ←---- 創建一個新消息源,每次調用是以原子操作的方式遞增一個整型變量source.setObject(new AtomicInteger());source.setMethodName("getAndIncrement");return source;}@Beanpublic DirectChannel inputChannel() {return new DirectChannel(); ←---- 管道傳送由消息源發送過來的數據}@Beanpublic IntegrationFlow myFlow() {return IntegrationFlows ←---- 以方法鏈接方式通過一個構建器創建IntegrationFlow.from(this.integerMessageSource(), ←---- 以之前定義的MessageSource作為IntegrationFlow的來源c -> c.poller(Pollers.fixedRate(10))) ←---- 輪詢MessageSource,對它傳遞的數據隊列執行出隊操作,取出數據.channel(this.inputChannel()).filter((Integer p) -> p % 2 == 0) ←---- 過濾出那些偶數.transform(Object::toString) ←---- 將由MessageSource 獲取的整數轉換為字符串類型.channel(MessageChannels.queue("queueChannel")) ←---- 將queueChannel作為該IntegrationFlow的輸出管道.get(); ←---- 終止IntegrationFlow的構建執行,并返回結果}
}
:::info
這段代碼中,方法myFlow()構建IntegrationFlow時使用了Spring Integration DSL。它使用的是IntegrationFlow類提供的流暢構建器,該構建器采用的就是方法鏈接模式。
這個例子中,最終的流會以固定的頻率輪詢MessageSource,生成一個整數序列,過濾出其中的偶數,再將它們轉化為字符串,最終將結果發送給輸出管道,這種行為與Java 8原生的Stream API非常像。
該API允許你將消息發送給流中的任何一個組件,只要你知道它的inputChannel名。如果流始于一個直接管道(direct channel),而非一個MessageSource,你完全可以使用Lambda表達式定義該IntegrationFlow
:::
@Bean
public IntegrationFlow myFlow() {return flow -> flow.filter((Integer p) -> p % 2 == 0).transform(Object::toString).handle(System.out::println);
}
// Spring Integration DSL中使用最廣泛的模式是方法鏈接。
// 這種模式非常適合IntegrationFlow構建器的主要用途: 創建一個執行消息傳遞和數據轉換的流。
// 然而,正如我們在上一個例子中看到的那樣,它也并非只用一種模式,
// 構建頂層對象時它也使用了Lambda表達式的函數序列
// (有些情況下,也是為了解決方法內部更加復雜的參數傳遞問題)。
小結
引入DSL的主要目的是為了彌補程序員與領域專家之間對程序認知理解上的差異。對于編寫實現應用程序業務邏輯的代碼的程序員來說,很可能對程序應用領域的業務邏輯理解不深,甚至完全不了解。以一種“非程序員”也能理解的方式書寫業務邏輯并不能把領域專家們變成專業的程序員,卻使得他們在項目早期就能閱讀程序的邏輯并對其進行驗證。
DSL的兩大主要分類分別是內部DSL(采用與開發應用相同的語言開發的DSL)和外部DSL(采用與開發應用不同的語言開發的DSL)。內部DSL所需的開發代價比較小,不過它的語法會受宿主語言限制。外部DSL提供了更高的靈活性,但是實現難度比較大。
可以利用JVM上已經存在的另一種語言開發多語言DSL,譬如Scala或者Groovy。這些新型語言通常都比Java更加簡潔,也更靈活。然而,要將Java與它們整合在一起使用需要修改構建流程,而這并不是一項小工程,并且Java與這些語言的互操作也遠沒達到完全無縫的程度。
由于自身冗長、煩瑣以及僵硬的語法,Java并非創建內部DSL的理想語言,然而隨著Lambda表達式及方法引用在Java 8中的引入,這種情況有所好轉。
現代Java語言已經以原生API的方式提供了很多小型DSL。這些DSL,譬如Stream和Collectors類中的那些方法,都非常有用,使用起來也極其方便,特別是你需要對集合中的數據進行排序、過濾、轉換或者分組的時候,非常值得一試。
在Java中實現DSL有三種主要模式,分別是方法鏈接、嵌套函數以及函數序列。每種模式都有其優點和弊端。不過,你可以在一個DSL中整合這三種DSL,盡量地揚長避短,充分發揮各種模式的長處。
很多Java框架和庫都可以通過DSL使用其特性。本章介紹了其中的三種,分別是:jOOQ,一種SQL映射工具;Cucumber,一種基于行為驅動的開發框架;Spring Integration,一種實現企業集成模式的Spring擴展庫。