集合工厂
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, B13
for (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 Bond
System.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) {
// 创建一个 HashMap
HashMap<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);
// 输出更新后的HashMap
System.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) {
// 创建一个 HashMap
HashMap<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);
// 输出更新后的 HashMap
System.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) throws
IOException {
try(BufferedReader br = new BufferedReader(new
FileReader("ModernJavaInAction/chap9/data.txt"))) {
return p.process(br); ←---- 将BufferedReaderProcessor作为执行参数传入
}
}
public interface BufferedReaderProcessor { ←---- 使用Lambda表达式的函数接口,该接口能够抛出一个IOException
String 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 class
static class Customer {}
// dummy Database class
static private class Database {
static Customer getCustomerWithId(int id) {
return new Customer();
}
}
}
//继承
public class MyOnlineBanking extends OnlineBanking {
@Override
void 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 class
static private class Customer {
}
// dummy Database class
static 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接口的所有实现类都提供了一个方法:notify
feedLambda.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 {
@Override
public void inform(String tweet) {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY!" + tweet);
}
}
}
static private class Guardian implements Observer {
@Override
public 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 {
@Override
public 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<>();
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public 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> {
@Override
public String handleWork(String text) {
return "From Raoul, Mario and Alan: " + text;
}
}
private static class SpellCheckerProcessing extends ProcessingObject<String> {
@Override
public 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.NullPointerException
at 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 zero
at 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 {
@Bean
public MessageSource<?> integerMessageSource() {
MethodInvokingMessageSource source =
new MethodInvokingMessageSource(); ←---- 创建一个新消息源,每次调用是以原子操作的方式递增一个整型变量
source.setObject(new AtomicInteger());
source.setMethodName("getAndIncrement");
return source;
}
@Bean
public DirectChannel inputChannel() {
return new DirectChannel(); ←---- 管道传送由消息源发送过来的数据
}
@Bean
public 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扩展库。