Java8学习笔记之重构
一、为改善可读性和灵活性重构代码
利用Lambda表达式,可以写出更简洁灵活的代码。Lambda表达式可以让我们用更紧凑的方式描述程序的行为,如将一个方法作为参数传递给另一个方法。
1、改善代码的可读性
改善可读性意味着要确保你的代码能很容易被别人理解和维护。为了确保你的代码能被其他人理解,有几个步骤可以尝试,比如确保你的代码附有良好的文档,并严格遵守编程规范。
Java8的新特性也可以帮助提升代码的可读性:
Java 8可以减少冗长的代码,让代码更易于理解。
通过方法引用和Stream API,代码会变得更直观。
重构的几种方式:
用Lambda表达式取代匿名类
用方法引用重构Lambda表达式
用Stream API重构命令式的数据处理
2、从匿名类到Lambda表达式的转换
将实现单一抽象方法的匿名类转换为Lambda表达式,因为匿名类繁琐且容易出错,采用Lambda表达式之后,代码会更简洁,可读性更好。如下:
Runnable r1 = new Runnable(){ //使用匿名类方式
public void run(){System.out.println("Hello");}
};
Runnable r2 = () -> System.out.println("Hello"); //采用Lambda表达式
在某些情况下,将匿名类转换为Lambda表达式可能是一个比较复杂的过程。 首先,匿名类和Lambda表达式中的this和super的含义是不同的。在匿名类中,this代表的是类自身,但在Lambda中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而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 doSome(Runnable r){ r.run(); }
public static void doSome(Task a){ a.execute(); }
此时再传递一个匿名类实现的Task,不会有任何问题:
doSome(new Task() {
public void execute() { System.out.println("danger!"); }
});
如果将这种匿名类转换为Lambda表达式时,就导致了一种晦涩的方法调用,因为Runnable和Task都是合法的目标类型:
doSome(() -> System.out.println("danger!")); //doSome(Runnable)和doSome(Task)都匹配该类型
可以对Task尝试使用显式的类型转换来解决这种模棱两可的情况:
doSome((Task)() -> System.out.println("danger!"));
目前大多数的集成开发环境都支持这种重构,如IntelliJ,它们能自动帮你检查,避免发生这些问题。
3、从Lambda表达式到方法引用的转换
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;
})
);
将上述Lambda表达式的内容抽取到一个单独的方法中,将其作为参数传递给groupingBy方法。如下:
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
menu.stream().collect(groupingBy(Dish::getCaloricLevel)); //将Lambda表达式抽到一个方法内
添加getCaloricLevel方法:
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都有内建的辅助方法可以和方法引用结合使用。比如,使用Collectors接口可以轻松得到和或者大值,与采用Lambada表达式和底层的归约操作比起来,这种方式要直观得多。如下:
int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2); //归约方式
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories)); //使用内置的集合类方式
4、从命令式的数据处理切换到Stream
建议将所有使用迭代器处理模式处理集合的代码都转换成Stream API的方式。一是Stream API能更清晰地表达数据处理管道的意图。二是通过短路和延迟载入以及利用现代计算机的多核架构,我们可以对Stream进行优化。
以下代码使用了两种模式:筛选和抽取,这两种模式混在一起,这样的代码结构要求程序员必须彻底搞清楚程序的每个细节才能理解代码的功能。此外,实现需要并行运行的程序所面对的困难也更多。如下:
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){
if(dish.getCalories() > 300) dishNames.add(dish.getName());
}
替代方案使用Stream API:
menu.parallelStream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.collect(toList());
5、增加代码的灵活性
Lambda表达式有利于行为参数化,可以使用不同的Lambda表示不同的行为,并将它们作为参数传递给函数去处理执行。这种方式可以帮助我们从容地面对需求的变化。比如,我们可以用多种方式为Predicate创建筛选条件,或者使用Comparator对多种对象进行比较。
采用函数接口
要想使用Lambda表达式,就必须要有函数接口。使用它有两种模式:有条件的延迟执行和环绕执行。
有条件的延迟执行
if (logger.isLoggable(Log.FINER)){ logger.finer("Problem: " + generateDiagnostic()); }
替代为以下方式:
logger.log(Level.FINER, "Problem: " + generateDiagnostic());
这种方式的优点是你不需要在代码中插入条件判断,同时日志器的状态也不再被暴露出去。但它依旧存在一个问题:日志消息的输出与否每次都需要判断,即使你已经传递了参数,不开启日志。
解决方式是延迟消息构造,日志就只会在某些特定的情况下才开启。
Java 8引入了一个对log方法的重载版本,此版本的log方法接受一个Supplier作为参数。它的方法签名如下:
public void log(Level level, Supplier<String> msgSupplier)
调用方式:logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
如果日志器的级别设置恰当,log方法会在内部执行作为参数传递进来的Lambda表达式。
Log方法的内部实现如下:
public void log(Level level, Supplier<String> msgSupplier){
if(logger.isLoggable(level)) log(level, msgSupplier.get()); //执行Lambda表达式
}
总结:如果需要频繁地从客户端代码去查询一个对象的状态,而只是为了传递参数或调用该对象的一个方法,则可以考虑实现一个新的方法,以Lambda或方法表达式作为参数,新方法在检查完该对象的状态之后才调用原来的方法。这样代码就会更易读、封装性更好。
环绕执行
如果发现业务代码虽有不同,但它们拥有同样的准备和清理阶段,这时你可以将这部分代码用Lambda实现。这种方式的好处是可以重用准备和清理阶段的逻辑,减少重复冗余的代码。如下对打开和关闭文件的处理:
String line1 = processFile((BufferedReader b) -> b.readLine()); //传入一个Lambda表达式
String line2 = processFile((BufferedReader b) -> b.readLine() + b.readLine()); //传入另一个Lambda表达式
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("conf/data.txt"))){
return p.process(br); //将BufferedReaderProcessor作为执行参数传入
}
}
public interface BufferedReaderProcessor{ //使用Lambda表达式的函数接口
String process(BufferedReader b) throws IOException;
}
通过BufferedReaderProcessor函数式接口,可以传递各种种Lamba表达式对BufferedReader对象进行处理。
二、使用Lambda重构面向对象的设计模式
对设计经验的归纳总结被称为设计模式。在设计软件时,可以复用这些方式方法来解决一些常见问题。如访问者模式常用于分离程序算法和它的操作对象,单例模式一般用于限制类的实例化,仅生成一份对象。
Lambda表达式为解决传统设计模式所面对的问题提供了新的解决方案,采用这些方案往往更高效、更简单。
1、策略模式
策略模式代表了解决一类算法的通用解决方案,可以在运行时选择使用哪种方案。
策略模式包含三部分内容:
一个代表某个算法的接口(它是策略模式的接口)。
一个或多个该接口的具体实现,它们代表了算法的多种实现。
一个或多个使用策略对象的客户。
示例:验证输入的内容是否根据标准进行了恰当的格式化
//定义接口
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 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("a"); //返回false
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
boolean b2 = lowerCaseValidator.validate("b"); //返回true
使用用Lambda表达式调用:
Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("a");
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("b");
总结:Lambda表达式避免了策略设计模式的模板代码,它已对部分代码(或策略)进行了封装。对于类似的问题,建议尽量使用Lambda表达式来解决。
2、模板方法
模板方法模式在你希望使用这个算法,但又需要对其中的某些行进行改进,才能达到希望的效果时非常有用。
示例:简单的在线银行应用,处理用户账户相关操作。
通过抽象类方式来实现:
abstract class OnlineBanking {
public void processCustomer(int id){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer c);
}
processCustomer方法搭建了在线银行算法的框架:获取客户提供的ID,然后提供服务让用户满意。不同的支行可以通过继承OnlineBanking类,对该方法提供差异化的实现。
使用Lambda表达式来实现:
public class OnlineBankingLambda {
//向processCustomer方法引入第二个参数Consumer<Customer>
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
}
此时可以通过传递Lambda表达式,直接插入不同的行为,无需再继承OnlineBanking类:
new OnlineBankingLambda().processCustomer(1, (Customer c) -> System.out.println("Hello " + c.getName());
3、观察者模式
观察者模式是一种常见的方案,某些事件发生时(如状态转变),如果一个对象(称为主题)需要自动通知其他多个对象(称为观察者),就会采用该方案。
创建图形用户界面(GUI)程序时,经常会使用该设计模式。这种情况下,在图形用户界面组件(如按钮)上注册一系列的观察者。如果点击按钮,观察者就会收到通知,并随即执行某个特定的行为。观察者设计模式也适用于股票交易,多个券商可能都希望对某一支股票价格(主题)的变动做出响应。
示例:订阅新闻通知
//定义观察者接口,将不同的观察者聚合在一起
interface Observer { void notify(String tweet); }
//声明不同的观察者
class News implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("car")) System.out.println("收到新华网的推文! " + tweet);
}
}
class PeopleCN implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("city")) System.out.println("收到人民网的推文!" + tweet);
}
}
class Sina implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("phone")) System.out.println("收到新浪网的推文!" + tweet);
}
}
//定义Subject接口
interface Subject{
void registerObserver(Observer o); //注册一个新的观察者
void notifyObservers(String tweet); //通知它的观察者一个新闻的到来
}
//定义Subject接口的实现类Feed
class Feed implements Subject{
private List<Observer> observers = new ArrayList<>();
public void registerObserver(Observer o) {this.observers.add(o);}
public void notifyObservers(String tweet) {observers.forEach(o -> o.notify(tweet));}
}
Feed类在内部维护了一个观察者列表,一条新闻到达时,它就进行通知。
执行调用:
Feed f = new Feed();
f.registerObserver(new News());
f.registerObserver(new PeopleCN());
f.registerObserver(new Sina());
f.notifyObservers("jack has a red car!");
使用Lambda表达式实现:
Observer接口的所有实现类都提供了一个notify方法,当新闻到达时,它们都是对同一 段代码封装执行。使用Lambda表达式后, 无需显式地实例化三个观察者对象,直接传递Lambda表达式表示需要执行的行为即可:
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("car")) System.out.println("收到新华网的推文! " + tweet);
});
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("city")) System.out.println("收到人民网的推文! " + tweet);
});
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("phone")) System.out.println("收到新浪网的推文! " + tweet);
});
注意:我们并不能随时随地使用Lambda表达式。Lambda适配得很好,是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。但观察者的逻辑有可能十分复杂,它们可能还持有状态,或者定义了多个方法,此时应该继续使用类的方式。
4、责任链模式
责任链模式是一种创建处理对象序列的通用方案。一个处理对象可能在完成一些工作后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。
通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。示例如下:
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);
}
handle方法提供了如何进行工作处理的框架,不同的处理对象可以通过继承ProcessingObject类,提供handleWork方法来进行创建。
//创建两个处理对象
public class HeaderTextProcessing extends ProcessingObject<String> {
public String handleWork(String text){return "hello" + text;}
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
public String handleWork(String text){return text.replaceAll("word", "world");}
}
//将这两个处理对象结合起来,构造一个操作序列
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2); //将两个处理对象链接起来
System.out.println(p1.handle(" word!")); //输出hello world!
使用Lambda表达式实现:
构造Lambda表达式:将处理对象作为函数的一个实例,或者说作为UnaryOperator<String>的一个实例,然后使用andThen方法对其进行链接。如下:
UnaryOperator<String> headerProcessing = (String text) -> "hello" + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("word", "world");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing); //将两个方法结合起来,结果就是一个操作链
System.out.println(pipeline.apply(" word!")); //输出hello world!
5、工厂模式
使用用工厂模式可以无需向客户暴露实例化的逻辑就能完成对象的创建。示例如下:
//创建一个工厂类,它包含一个负责实现不同对象的方法
public 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);
}
}
}
createProduct方法可以通过附加的逻辑来设置每个创建的产品。好处是在创建对象时不用担心会将构造函数或者配置暴露给客户,使得客户创建产品时更简单:
Product p = ProductFactory.createProduct("loan");
使用Lambda表达式实现:
/引用贷款 (Loan)的构造函数示例:
Supplier<Product> loanSupplier = Loan::new;
Loan loan = loanSupplier.get();
//创建一个Map,将产品名映射到对应的构造函数
final static Map<String, Supplier<Product>> productMap = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new);
map.put("bond", Bond::new);
}
//利用productMap来实例化不同的产品
public static Product createProduct(String name){
Supplier<Product> p = map.get(name);
if(p != null) return p.get();
throw new IllegalArgumentException("No such product " + name);
}
如果工厂方法createProduct需要接收多个参数,并传递给产品构造方法,就必须提供不同的函数接口,而无法统一使用一个简单接口的方式,造成其扩展性不好。
假设保存具有三个参数(两个参数为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<>();
--参考文献《Java8实战》