java8的lambda表达式的引入对我们现有代码有很大的影响。你可能会在你的新代码中去使用这些新的特性。如果你创建的是全新的java工程,这是极好的时机,你可以轻装上阵,迅速地将新特性应用到你的项目中。如果很不幸,你面对的是一些很老旧的代码,这时候你就需要重构了。
1.改善可读性和灵活性
在java8中,我们知道只要善于利用Lambda表达式可以帮助我们写出更简洁,更灵活的代码。采用lambda表达式以后,我们的代码可以根据传入的参数动态选择和执行相应的行为。
1.1改善代码的可读性
我们很难定义什么是好的可读性,因为可能非常主观,每个人都会对可读性都有自己的定义。对于大多数人来说都有一个共同的认知就是“理解这段代码的难易程度决定了代码的可读性”。如果我们要改善我们的可读性意味着你要确保你的代码能非常容易地被包括自己在内的所有人理解和维护。我们平时只要遵守代码编码规范和文档即可。
java8能得新特性也能帮助你改善:
- 使用java8,减少一些冗长的代码,让代码更加容易理解。
- 通过方法引用和Stream API,能让代码变得更直观。
接下来我们会介绍三种简单的重构方法:
- lambda表达式取代匿名类
- 方法引用重构lambda表达式
- 用Stream API重构命令式数据处理
1.2匿名类到lambda表达式
匿名类转为lambda表达式是我们最简单的一种重构。匿名类比较繁琐并且容易出错,用了Lambda表达式之后,你的代码更简洁。
//匿名类
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
//Lambda表达式
Runnable r2 = () -> System.out.println("Hello");
上面的转换比较简单,但是在某些情况下,将匿名类转换为Lambda表达式可能是一个比较复杂的过程。首先,匿名类和Lambda表达式中的this和super的含义不同。但是在lambda中,代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而Lambda表达式不能,会导致我们编译错误。
int a = 10;
Runnable r3 = () -> {
int a = 2; //这里会编译错误
System.out.println(a);
};
//在匿名类中一切正常
Runnable r4 = new Runnable(){
public void run(){
int a = 2;
System.out.println(a);
}
};
如果在涉及重载的上下文里面,我们把匿名类转换为Lambda表达式可能最终导致我们的代码更加难以理解。实际上,匿名类的类型是在初始化时确定的,而Lambda的类型取决于它的上下文。通过下
面这个例子,我们可以了解问题是如何发生的。我们假设你用与Runnable同样的签名声明了一个函数接口,我们称之为Task:
public interface Task {
public void execute();
}
//写两个重载方法
public static void doSomething(Runnable r){ r.run(); }
public static void doSomething(Task a){ a.execute(); }
在我们匿名类中去使用:
doSomething(new Task() {
@Override
public void execute() {
System.out.println("Danger danger!!");
}
});
上面的方法不会出任何的问题,但是如果使用lambda表达式我们就会出现编译错误
doSomething( () -> System.out.println("Dangeer"));
因为lambda表达式他也可以匹配Runnable的重载,所以就会出现冲突,我们可以使用显示的转换来解决这个问题
doSomething((Task) () -> System.out.println("Dangeer"));
1.3从方法引用到Lambda表达式
Lambda表达式非常适用于需要传递代码片段的场景,我们把参数传递变为传递方法引用。下面我们展现匿名函数,lambda,以及我们的方法引用的展现。
stringList.sort(String::compareTo);
stringList.sort((o1 ,o2) -> {return 0;} );
stringList.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return 0;
}
});
1.4从命令式的数据处理切换到Stream
在java8将会建议你将所有迭代器这种数据处理模式处理集合的代码都转换成Stream API的方法。为什么呢?StreamAPI能更清晰地表达数据处理管道的意图。除此之外,通过短路和延迟载入以及利用多核架构,我们可以对Stream进行优化。如果我们要筛选字符串长度大于3,并且将其转换为全大写。
java8之前的写法:
List<String> strings = new ArrayList<>();
for (String s : stringList){
if (s.length() > 3){
strings.add(s.toUpperCase());
}
}
上面的写法比较繁琐,且可读性不是很高。java8的写法:
strings = stringList.parallelStream().filter(s -> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
2.使用Lambda重构设计模式
新的语言特性尝尝让现存的编程模式或设计黯然失色。比如,Java 5引入了for-each循环,由于它的稳健性和间接性,已经替代了很多显示使用迭代器的情形。
对设计经验的归纳总结被称为设计模式。设计软件时,如果你愿意,可以复用这些方式方法来解决一些常见问题。Lambda表达式为程序员的工具箱又添加了一件力气。它们为解决传统设计模式所面对的问题提供了新的解决方案,不但如此,采用这些方法更加高效。这一节中,我们会针对四个设计模式展开讨论,它们分别是:
- 策略模式
- 模板方法
- 观察者模式
- 责任链模式
2.1策略模式
策略模式代表了一类算法的通用解决方法,你可以在运行时选择使用哪种通用方案。策略模式包含三部分内容:
- 一个代表某个算法的接口(策略模式接口)
- 一个或者多个该接口的具体实现。
- 使用策略对象的客户。
我们假设你希望验证输入的内容是否根据标准进行了恰当的格式化。下面是一个验证接口也就是我们策略模式的接口:
public interface ValidationStrategy {
boolean execute(String s);
}
定义了该接口的多个具体实现:
public class IsAllLowerCase implements ValidationStrategy{
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
public class IsNumeric implements ValidationStrategy{
@Override
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);
}
public static void main(String[] args) {
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaa");//返回fase
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("bbbb");//返回true
}
}
在这里我们可以使用Lambda表达式,因为ValidationStrategy是一个函数式接口可以额很好的来作为一个Lambda表达式
Validator numericValidator =
new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator =
new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");
2.2模板方法
如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。模板方法模式在你“希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果”时非常有用的。
让我们从一个例子着手,让我们看看如何用Java8以前的方法去实现。比如你编写一个简单的在线银行。通常,用户需要输入一个用户账户,之后应用才能从银行的数据库中得到详细信息,最终完成一些让用户满意的操作。不同分行的在线银行应用让客户满意的方式可能还略有不同,不如给客户的账户发送红利,或者仅仅是少发送一些推广文件。你可能通过下面的抽象类方式来实现在线银行应用:abstract class OnlineBanking {
public void processCustomer(int id){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer c);
}
processCustomer方法搭建了在线银行算法框架:获取客户提供的ID,然后提供服务让用户满意。不同的支行可以通过集成OnlineBanking类,对该方法提供差异化的实现。
为了解决差异化的问题,使用Lambada表达式,可以很好的解决设计僵硬的问题,再也不需要继承,直接在Lambda表达式中插入行为。
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
2.3观察者模式
观察者模式是一种比较常见的方案,某些事件发生时,如果一个对象需要自动地通知其他多个对象,就会采用该方案。创建图形用户界面程序时,你经常会使用该设计模式。这种情况下,你会在图形用户界面组件上注册一系列的观察者。如果点击按钮,观察者就会收到通知,并随时执行某个特定的行为。但是观察者模式并不局限于图形用户界面。让我们下面来举个例子。你需要为Twitter这样的应用设计并实现一个定制化的通知系统。想法简单:好几家报纸机构,比如<<纽约时报>>等等报纸都订阅了新闻,他们希望当接收的新闻中包含他们感兴趣的关键字时,可以接受到通知。
首先,你需要一个观察者接口,他将不同的观察者聚合在一起。一旦接收到一条新的新闻,该方法就会被调用。
public interface Observer {
void notify(String tweet);
}
现在,你可以声明不同的观察者(比如,这里是三家不同的报纸机构),依据新闻中不同的关键字分别定义不同的行为:
class NYTimes implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
}
}
class Guardian implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet);
}
}
}
class LeMonde implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("wine")){
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}
当然还有我们的Subject类,首先定义一个接口:
interface Subject{
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
Subject使用registerObserver方法可以注册一个新的观察者,使用notifyObservers
方法通知它的观察者一个新闻的到来。让我们更进一步,实现Feed类:
class Feed implements Subject {
private final 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));
}
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 java 8 is coming");
}
}
这是一个非常直观的实现:Feed类在内部维护了一个观察者列表,一条新闻到达时,它就进行通知。
对于我们的Observer接口是一个函数式接口我们一样的可以用Lambda来消除我们的僵化的代码。
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
});
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet);
}
});
那么,是否我们随时随地都可以使用Lambda表达式呢?答案是否定的!我们前文介绍的例子中, Lambda适配得很好,那是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义多个方法,诸如此类。在这些情形下,你还是应该继续使用类的方式。
2.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);
}
我们需要把不同的处理器都给连接起来才能使用。我们使用UnaryOperator中的andThen方法,可以很好的解决责任链处理器的链接。