1.为改善可读性和灵活性重构代码
改善代码的可读性
三种简单的重构,利用Lambda表达式、方法引用以及Stream改善程序代码的可读性:
重构代码,用Lambda表达式取代匿名类
用方法引用重构Lambda表达式
用Stream API重构命令式的数据处理
从匿名类到 Lambda 表达式的转换
虽然将匿名类转为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 doSomething(Runnable r){ r.run(); }
public static void doSomething(Task a){ a.execute(); }
/**
* 匿名类的方式
*/
doSomething(new Task() {
public void execute() {
System.out.println("Danger danger!!");
}
});
/**
*将这种匿名类转换为Lambda表达式时,就导致了一种晦涩的方法调用,因为Runnable和Task都是合法的目标类型
*/
//doSomething(Runnable) 和doSomething(Task)都匹配该类型
doSomething(() -> System.out.println("Danger danger!!"));
//可以对Task尝试使用显式的类型转换来解决这种模棱两可的情况:
doSomething((Task)() -> System.out.println("Danger danger!!"));
public class Fruit {
protected String color;
protected Integer weight;
public Fruit() { }
public Fruit(String color) {
this.color = color;
}
public Fruit(Integer weight) {
this.weight = weight;
}
public Fruit(String color, Integer weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
@Override
public String toString() {
return "Fruit{" +
"color='" + color + '\'' +
", weight=" + weight +
'}';
}
}
public class Apple extends Fruit {
public Apple() { }
public Apple(String color) {
super(color);
}
public Apple(Integer weight) {
super(weight);
}
public Apple(String color, Integer weight) {
super(color, weight);
}
public FruitWeightLevel getWeightLevel(){
if (this.weight <= 10) return FruitWeightLevel.LEVEL1;
else if (this.weight <= 20) return FruitWeightLevel.LEVEL2;
else return FruitWeightLevel.LEVEL3;
}
}
public class Orange extends Fruit{
public Orange() { }
public Orange(String color) {
super(color);
}
public Orange(Integer weight) {
super(weight);
}
public Orange(String color, Integer weight) {
super(color, weight);
}
}
public enum FruitWeightLevel {
LEVEL1(1,"[0,10]"),
LEVEL2(2,"[11,20]"),
LEVEL3(3,"[21,∞]");
private int level;
private String range;
FruitWeightLevel(int level, String range) {
this.level = level;
this.range = range;
}
}
/**
* 从Lambda 表达式到方法引用的转换
* Lambda表达式非常适用于需要传递代码片段的场景。不过,为了改善代码的可读性,也请
* 尽量使用方法引用。因为方法名往往能更直观地表达代码的意图。
*/
List<Apple> list = Arrays.asList(new Apple("g", 8), new Apple("r", 12), new Apple("g", 21), new Apple("g", 16));
//求出不同的重量级别的所有苹果 [0,10]~LEVEL1 [11,20]~LEVEL2 >20~LEVEL3
//1.将分组逻辑直接以Lambda表达式传递
Map<FruitWeightLevel, List<Apple>> map = list.stream().collect(Collectors.groupingBy(a -> {
Integer weight = a.getWeight();
if (weight <= 10) return FruitWeightLevel.LEVEL1;
else if (weight <= 20) return FruitWeightLevel.LEVEL2;
else return FruitWeightLevel.LEVEL3;
}));
map.forEach((k,v) -> System.out.println(k + "=" + v));
System.out.println("+++++++++++++++++++");
//2.将Lambda表达式转为方法引用,但需要在Apple类中添加分组方法
Map<FruitWeightLevel, List<Apple>> map1 = list.stream().collect(Collectors.groupingBy(Apple::getWeightLevel));
map.forEach((k,v) -> System.out.println(k + "=" + v));
System.out.println("+++++++++++++++++++");
/**
* 从命令式的数据处理切换到Stream
* 将所有使用迭代器这种数据处理模式处理集合的代码都转换成Stream API的方式。
* Stream API能更清晰地表达数据处理管道的意图。除此之外,通过短路和延迟载
* 入以及现代计算机的多核架构,我们可以对Stream进行优化。
* 但是将命令式的代码结构转换为Stream API的形式是个困难的任务,因为你需要考虑
* 控制流语句,比如break、continue、return,并选择使用恰当的流操作。
*/
//筛选出重量大于10的所有苹果的重量分别是多少
//以前的方式
List<Integer> weightList = new ArrayList<>();
for (Apple a:list){
if (a.getWeight() > 10){
weightList.add(a.getWeight());
}
}
System.out.println(weightList);
//现在的方式
List<Integer> weightList2 = list.stream().filter(a -> a.getWeight() > 10).map(Apple::getWeight).collect(Collectors.toList());
System.out.println(weightList2);
System.out.println("======================");
/**
* 增加代码的灵活性
* 1.采用函数接口
* 没有函数接口,你就无法使用Lambda表达式。因此,你需要在代码中引入函数接口
* 在什么情况下使用? 有条件的延迟执行和环绕执行
* 2.有条件的延迟执行
* 我们经常看到这样的代码,控制语句被混杂在业务逻辑代码之中。典型的情况包括进行安全性检查以及日志输出。
* 3. 环绕执行
*/
2.使用Lambda 重构面向对象的设计模式
public class Validator {
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v) {
this.strategy = v;
}
public boolean validate(String s) {
return strategy.execute(s);
}
}
public class IsNumeric implements ValidationStrategy {
@Override
public boolean execute(String s){
return s.matches("\\d+");
}
}
public class IsAllLowerCase implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
package com.h.java8;
import java.util.Objects;
/**
* <br>责任链模式示例</br>
*/
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor;
public void setSuccessor(ProcessingObject<T> successor) {
this.successor = successor;
}
/**
* 定义工作处理的框架
* 不同的处理对象可以通过继承ProcessingObject类,提供handleWork方法来进行创建。
* @param input
* @return
*/
public T handle(T input){
T r = handleWork(input);
if (Objects.nonNull(successor)){
return successor.handleWork(r);
}
return r;
}
protected abstract T handleWork(T input);
}
public class HeaderTextProcessing extends ProcessingObject<String> {
@Override
public String handleWork(String text){
return "From Raoul, Mario and Alan: " + text;
}
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
@Override
public String handleWork(String text){
return text.replaceAll("labda", "lambda");
}
}
public class FruitFactory {
public static Fruit createFruit(String name){
switch (name){
//case "apple":return new Apple("g",1);
case "apple":return new Apple();
//case "orange":return new Orange("y",1);
case "orange":return new Orange();
default:throw new RuntimeException("No such fruit " + name);
}
}
}
public class FruitFactory2 {
private static final Map<String,Supplier<Fruit>> map = new HashMap<>(2);
/**
* 创建一个Map,将水果名映射到对应的构造函数
*/
static {
map.put("apple",Apple::new);
map.put("orange",Orange::new);
}
public static Fruit createFruit(String name){
Supplier<Fruit> supplier = map.get(name);
if (Objects.isNull(supplier)) {
throw new RuntimeException("No such fruit " + name);
}
return supplier.get();
}
}
package com.h;
import com.h.java8.*;
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class MyTest {
public static void main(String[] args) throws Exception {
/**
* 示Lambda表达式是如何另辟蹊径解决设计模式原来试图解决的问题的?
* 1.策略模式
* 策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。
* 策略模式包含三部分内容:
* 一个代表某个算法的接口(它是策略模式的接口)。
* 一个或多个该接口的具体实现,它们代表了算法的多种实现(比如,实体类Concrete-trategyA或者ConcreteStrategyB)。
* 一个或多个使用策略对象的客户。
*/
/**
* 应用场景示例:验证输入的内容是否根据标准进行了恰当的格式化(比如只包含小写字母或数字)
*/
/**
* 传统的方式
*/
Validator validator = new Validator(new IsNumeric());
boolean b1 = validator.validate("aaaa");
System.out.println(b1);
Validator validator1 = new Validator(new IsAllLowerCase());
boolean b2 = validator1.validate("bbb");
System.out.println(b2);
/**
* 使用Lambda表达式
* 不需要声明新的类来实现不同的策略,通过直接传递Lambda表达式就能达到同样的目的,并且还更简洁
* ValidationStrategy是一个函数接口,与Predict<String>具有同样的函数描述
*/
Validator validator2 = new Validator(s -> s.matches("\\d+"));
boolean b3 = validator2.validate("aaaa");
System.out.println(b3);
Validator validator3 = new Validator((String s) -> s.matches("[a-z]+"));
boolean b4 = validator3.validate("bbbb");
System.out.println(b4);
System.out.println("++++++++++++++++");
/**
* 模板方法
* 模板方法模式在你“希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果”时是非常有用的
* 创建算法框架,让具体的实现插入某些部分,你想要插入的不同算法组件可以通过Lambda表达式或者方法引用的方式实现。
*/
/**
*观察者模式
*某些事件发生时(比如状态转变),如果一个对象(通常我们称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。
*/
/**
* 那么,是否我们随时随地都可以使用Lambda表达式呢?答案是否定的!
* 演示的例子中,,Lambda适配得很好,那是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。
* 但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,诸如此类。在这些情形下,你还是应该继续使用类的方式。
*/
/**
* 责任链模式
* 责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,
* 将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,依此类推。
* 通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字
* 段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。
*/
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);//将两个处理对象链接起来
String result = p1.handle("Aren't labda really sexy?!!");
System.out.println(result);
/**
* 使用Lambda表达式
* 将处理对象作为函数的一个实例,或者更确切地说作为UnaryOperator<String>的一个实例。
* 为了链接这些函数,你需要使用andThen方法对其进行构造
*/
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 result1 = pipeline.apply("Aren't labda really sexy?!!");
System.out.println(result1);
System.out.println("+++++++++++++++++++");
/**
* 工厂模式
* 使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。
*/
Fruit orange = FruitFactory.createFruit("orange");
System.out.println(orange);
/**
* 使用Lambda表达式
* 如果工厂方法createFruit需要接收多个传递给产品构造方法的参数,这种方式的扩展性不是很好。
* 你不得不提供不同的函数接口,无法采用之前统一使用一个简单接口的方式。
* 可以自定义函数式接口,但这会使原本的设计变得更复杂
*/
Fruit orange1 = FruitFactory2.createFruit("orange");
System.out.println(orange1);
}
}
3.测试Lambda 表达式
4.调试
调试有问题的代码时,程序员的兵器库里有两大老式武器,分别是:
查看栈跟踪
输出日志
/**
* 使用peek()方法进行Lambda表达式的调试
* peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5,6,7,8);
numbers.stream().peek(x -> System.out.println("from stream: " + x)) //输出来自数据源的当前元素值
.map(x -> x + 1)
.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(Collectors.toList());
//打印结果:
from stream: 1
after map: 2
after filter: 2
after limit: 2
from stream: 2
after map: 3
from stream: 3
after map: 4
after filter: 4
after limit: 4
from stream: 4
after map: 5
from stream: 5
after map: 6
after filter: 6
after limit: 6
小结:
Lambda表达式能提升代码的可读性和灵活性。
如果你的代码中使用了匿名类,尽量用Lambda表达式替换它们,但是要注意二者间语义的微妙差别,比如关键字this,以及变量隐藏。
跟Lambda表达式比起来,方法引用的可读性更好 。
尽量使用Stream API替换迭代式的集合处理。
Lambda表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典型的比如策略模式、模板方法、观察者模式、责任链模式,以及工厂模式。
即使采用了Lambda表达式,也同样可以进行单元测试,但是通常你应该关注使用了Lambda表达式的方法的行为。
尽量将复杂的Lambda表达式抽象到普通方法中。
Lambda表达式会让栈跟踪的分析变得更为复杂。
流提供的peek方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。