List 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元素
:::
如果进一步审视List接口,会发现List.of包含了多个重载的版本,包括:
static
static
static
List of(E... elements) 可变参
变参版本的函数需要额外分配一个数组,这个数组被封装于列表中。
使用变参版本的方法,你就要负担分配数组、初始化以及最后对它进行垃圾回收的开销。
使用定长(最多为10个)元素版本的函数,就没有这部分开销。
:::info
注意,如果使用List.of创建超过10个元素的列表,这种情况下实际调用的还是变参类型的函数。类似的情况也会出现在Set.of和Map.of中。
:::
//类似于List.of 、创建不可变的Set集合
Set friends = Set.of("Raphael", "Olivia", "Thibaut");
System.out.println(friends); ←---- [Raphael,Olivia, Thibaut]
Java 9中提供了两种初始化一个不可变Map的方式
Map 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
使用第二种方法,你需要创建额外的对象,从而实现对键和值的封装
:::
import static java.util.Map.entry;
Map ageOfFriends
= Map.ofEntries(entry("Raphael", 30),
entry("Olivia", 25),
entry("Thibaut", 26));
System.out.println(ageOfFriends); ←---- {Olivia=25, Raphael=30, Thibaut=26}
List actors = List.of("Keanu", "Jessica")
actors.set(0, "Brad");
System.out.println(actors)
/**
*答案:执行该代码片段会抛出一个UnsupportedOperationException异常,
*因为由List.of方法构造的集合对象是不可修改的。
**/
Java 8在List和Set的接口中新引入了以下方法。
for (Transaction transaction : transactions) {
if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
transactions.remove(transaction);
}
}
可代替上述繁琐步骤[]
//发现其中的问题了吗?非常不幸,
这段代码可能导致ConcurrentModificationException。
为什么会这样?
因为在底层实现上,
for-each循环使用了一个迭代器对象,所以代码的执行会像下面这样:
for (Iterator 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 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)));
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 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));
for(Map.Entry 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 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
查找的键在Map中不存在该怎么办。新的getOrDefault方法可以解决这一问题。
Map 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
缓存某个昂贵操作的结果,将其保存在一个键对应的值中。如果该键存在,就不需要再次展开计算。解决这个问题有三种新的途径
import java.util.HashMap;
class Main {
public static void main(String[] args) {
// 创建一个 HashMap
HashMap prices = new HashMap();
// 往HashMap中添加映射项
prices.put("Shoes", 200);
prices.put("Bag", 300);
prices.put("Pant", 150);
System.out.println("HashMap: " + prices);
// 计算 Shirt 的值
int shirtPrice = pricecnblogs.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 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 = pricecnblogs.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 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}
//没有重复的KEY
Map family = Map.ofEntries(
entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map friends = Map.ofEntries(
entry("Raphael", "Star Wars"));
Map everyone = new HashMap(family);
everyone.putAll(friends); ←---- 复制friends的所有条目到everyone中
System.out.println(everyone); ←---- {Cristina=James Bond, Raphael= Star Wars, Teo=Star Wars}
//可能含有重复的KEY
Map family = Map.ofEntries(
entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map friends = Map.ofEntries(
entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));
Map 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 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 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 movies = new HashMap();
movies.put("JamesBond", 20);
movies.put("Matrix", 15);
movies.put("Harry Potter", 5);
Iterator> iterator =
movies.entrySet().iterator();
while(iterator.hasNext()) {
Map.Entry entry = iterator.next();
if(entry.getValue() entry.getValue()
引入ConcurrentHashMap类是为了提供一个更加现代的HashMap,以更好地应对高并发的场景。ConcurrentHashMap允许执行并发的添加和更新操作,其内部实现基于分段锁。与另一种解决方案——同步式的Hashtable相比较,ConcurrentHashMap的读写性能都更好(注意,标准的HashMap是不带同步的)。
已学
每种操作支持四种形式的参数,接受函数使用键、值、Map.Entry以及(键, 值)对作为参数:
规则!
除非软件架构经过高度的资源优化,否则通常情况下,建议遵守这些原则。
:::
ConcurrentHashMap map = new ConcurrentHashMap(); ←---- 一个可能有多个键和值更新的ConcurrentHashMap对象
long parallelismThreshold = 1;
Optional maxValue =
Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));
//请留意,int、long、double等基础类型的归约操作(reduceValuesToInt、reduceKeysToLong等)
//会更加高效,因为它们没有额外的封装开销
ConcurrentHashMap类提供了一个新的mappingCount方法,能以长整形long返回Map中的映射数目。
应该尽量在新的代码中使用它,而不是继续使用返回int的size方法。
这样做能让你的代码更具扩展性,更好地适应将来的需要,因为总有一天Map中映射的数目可能会超过int能表示的范畴。
ConcurrentHashMap类还提供了一个新的keySet方法,该方法以Set的形式返回ConcurrentHashMap的一个视图(Map中的变化会反映在返回的Set中,反之亦然)。
也可以使用新的静态方法newKeySet创建一个由ConcurrentHashMap构成的Set。
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的代码
:::
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!!"));
/**
大部分主流开发环境 支持自动检查重构
**/
Map> dishesByCaloricLevel =
menu.stream()
.collect(
groupingBy(dish -> {
if (dish.getCalories() > dishesByCaloricLevel =
menu.stream().collect(groupingBy(Dish::getCaloricLevel)); ←---- 将Lambda表达式抽取到一个方法内
//新增一个类 方法引用
public class Dish{
...
public CaloricLevel getCaloricLevel(){
if (this.getCalories()
静态辅助方法
comparing和maxBy。结合方法引用一起使用
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); ←---- 你需要考虑如何实现比较算法
inventory.sort(comparing(Apple::getWeight)); ←---- 读起来就像问题描述,非常清晰
int totalCalories =
menu.stream().map(Dish::getCalories)
.reduce(0, (c1, c2) -> c1 + c2);
//内置的集合类,它能更清晰地表达问题陈述是什么。使用了集合类summingInt(方法的名词很直观地解释了它的功能):
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
List 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());
采用函数接口
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;
}
策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。
可使用不同的标准来验证输入的有效性,使用不同的方式来分析或者格式化输入。
策略模式包含三部分内容
//假设希望验证输入的内容是否根据标准进行了恰当的格式化(比如只包含小写字母或数字)。
//可以从定义一个验证文本(以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
:::
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 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 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 {
protected ProcessingObject successor;
public void setSuccessor(ProcessingObject 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 p1 = new HeaderTextProcessing();
ProcessingObject p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result1 = p1.handle("Aren't labdas really sexy?!!");
System.out.println(result1);
UnaryOperator headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result2 = pipeline.apply("Aren't labdas really sexy?!!");
System.out.println(result2);
}
private static abstract class ProcessingObject {
protected ProcessingObject successor;
public void setSuccessor(ProcessingObject 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 {
@Override
public String handleWork(String text) {
return "From Raoul, Mario and Alan: " + text;
}
}
private static class SpellCheckerProcessing extends ProcessingObject {
@Override
public String handleWork(String text) {
return text.replaceAll("labda", "lambda");
}
}
}
UnaryOperator headerProcessing =
(String text) -> "From Raoul, Mario and Alan: " + text; ←---- 第一个处理对象
UnaryOperator spellCheckerProcessing =
(String text) -> text.replaceAll("labda", "lambda"); ←---- 第二个处理对象
Function pipeline =
headerProcessing.andThen(spellCheckerProcessing); ←---- 将两个方法结合起来,结果就是一个操作链
String result = pipeline.apply("Aren't labdas really sexy?!!");
:::info
处理对象作为Function
:::
无须向客户暴露实例化的逻辑就能完成对象的创建。假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。
通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法
public class FactoryMain {
public static void main(String[] args) {
Product p1 = ProductFactory.createProduct("loan");
System.out.printf("p1: %s%n", p1.getClass().getSimpleName());
Supplier 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 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> 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{
R apply(T t, U u, V v);
}
Map> map
= new HashMap();
将复杂的Lambda表达式分为不同的方法
高阶函数的测试
程序员的兵器库里有两大经典武器,分别是:
程序突然停止运行(比如突然抛出一个异常),这时首先要调查程序在什么地方发生了异常以及为什么会发生该异常。这时栈帧就非常有用了。程序的每次方法调用都会产生相应的调用信息,包括程序中方法调用的位置、该方法调用使用的参数,以及被调用方法的本地变量。这些信息被保存在栈帧上。
程序失败时,会得到它的栈跟踪,通过一个又一个栈帧,可以了解程序失败时的概略信息。
通过这些能得到程序失败时的方法调用列表。这些方法调用列表最终会帮助你发现问题出现的原因。由于Lambda表达式没有名字,因此栈跟踪可能很难分析
import java.util.*;
public class Debugging{
public static void main(String[] args) {
List 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 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 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 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());
JVM提供了第三个备选项,这是一种介于内部DSL与外部DSL之间的解决方案:可以在JVM上运行另一种通用编程语言,而这种语言比Java自身更灵活、更有表现力,譬如Scala,或者Groovy。
把这样的第三种选项称为“多语言DSL”(polyglot DSL)
DSL具有以下优点。
弊端
import static java.util.stream.Collectors.groupingBy;
public class GroupingBuilder {
private final Collector super T, ?, Map> collector;
private GroupingBuilder(Collector super T, ?, Map> collector) {
this.collector = collector;
}
public Collector super T, ?, Map> get() {
return collector;
}
public GroupingBuilder, J>
after(Function super T, ? extends J> classifier) {
return new GroupingBuilder(groupingBy(classifier, collector));
}
public static GroupingBuilder, 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; ←---- 终止创建订单并返回它
}
}
模式名 | 优点 | 缺点 |
---|---|---|
方法链接 | □ 方法名可以作为关键字参数 | |
□ 与optional参数的兼容性很好 | ||
□ 可以强制DSL的用户按照预定义的顺序调用方法 | ||
□ 很少使用或者基本不使用静态方法 | ||
□ 可能的语法噪声很低 | □ 实现起来代码很冗长 | |
□ 需要使用胶水语言整合多个构建器 | ||
□ 领域对象的层级只能通过代码的缩进公约定义 | ||
嵌套函数 | □ 实现代码比较简洁 | |
□ 领域对象的层次与函数嵌套保持一致 | □ 大量使用了静态方法 | |
□ 参数通过位置而非变量名识别 | ||
□ 支持可选参数需要实现重载方法 | ||
使用Lambda的函数序列 | □ 对可选参数的支持很好 | |
□ 很少或者基本不使用静态方法 | ||
□ 领域对象的层次与Lambda的嵌套保持一致 | ||
□ 不需要为支持构建器而使用胶水语言 | □ 实现代码很冗长 | |
□ DSL中的Lambda表达式会带来更多的语法噪声 |
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 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 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的应用中开发轻量级的远程服务(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扩展库。
本文来自博客园,作者:李好秀,转载请注明原文链接:https://www.cnblogs.com/lehoso/p/18277305
参与评论
手机查看
返回顶部