本章内容
1、应对不断变化的需求
2、行为参数化
3、匿名类
4、Lambda表达式预览
5、真实示例:Comparator、Runnable
应对不断变化的需求
编写能够应对变化的需求的代码并不容易,让我们来看一个例子,我们会逐渐改进这个例子。以展示一些让代码更灵活的最佳做法。
Alice是一个农场的农夫,你需要帮他实现一个从列表中筛选绿苹果的功能,听起来很简单吧。
小试牛刀,第一个解决方案可能是下面这样的:
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple : inventory) {
if ("green".equals(apple.getColor()) {
result.add(apple);
}
} return result;
}
现在Alice改变注意了,他还想要筛选红苹果,你该怎么做呢?简单的解决当大就是复制这个方法,把名字改成filterRedApples,然后更改if条件匹配红苹果。然而Alice想要筛选多种苹果:浅绿色、暗红色、黄色…这种做法就应付不了了。一个良好的原则是在编写类似的代码后,尝试将其抽象化。
再展身手,把颜色作为参数:
public static List<Apple> filterApplesByColor(List<Apple> inventory,
String color) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple : inventory) {
if (apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
Alice又跑来跟你说,要是能区分轻苹果和重苹果就太好了,重苹果一般是150g。作为软件工程师,你早就想到Alice可能还会更改重量,于是你是这样解决的:
public static List<Apple> filterApplesByWeight(List<Apple> inventory,
int weight) {
List<Apple> result = new ArrayList<Apple>();
For(Apple apple:inventory){
if (apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每一个苹果应用筛选条件。这有点令人失望,应为它打破了DRY(Don’t Repeat Yourself,不要重复你自己)的软件原则。
第三次尝试:对你想到的每一个属性做筛选:
public static List<Apple> filterApples(List<Apple> inventory, String color,
int weight, boolean flag) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple : inventory) {
if ((flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight)) {
result.add(apple);
}
}
return result;
}
你可以这么用
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
这个解决方法有点糟糕,首先代码看上去true和false是什么意思?并且这个解决方案还是不能很好的应对变化的需求。如果Alice要求你的对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?如果Alice要求你组合属性,比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的方法。下一节中,我会介绍如何使用行为参数化实现这种灵活性。
行为参数化
让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如绿色、重量)来返回一个boolean值。我们称它为谓词(一个返回boolean值的函数)。让我们定义一个接口来选择标准建模“
public interface ApplePredicate {
boolean test(Apple apple);
}
现在你可以用ApplePredicate的多个实现代表不同的选择标准了。
- 仅仅选出重的苹果
public class AppleHeavyWeightPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
- 仅仅选出绿苹果
public class AppleGreenColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
你可以把这些看作是filter方法的不同行为。这些行为就像是不同的策略,ApplePredicate就像是一个算法蔟。
但是,该怎么利用ApplePredicate的不同实现呢?你需要让filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为做参数,并在内部使用,来完成不同的行为。
第四次尝试:根据抽象条件筛选:
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
现在你把filterApples方法迭代集合的逻辑与你要应用到集合中每个元素的行为区分开了。
现在Alice让你找出所有重量超过150g的红苹果,你只需要创建一个类来实现ApplePredicate就行了。
public class AppleRedAndHeavyPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
你已经做了一件很酷的事::filterApples方法的行为取决你通过ApplePredicate对象传递的代码,换句话说,你把filterApples方法的行为参数化了!
请注意,在上一个例子中,唯一重要的代码是test方法的实现,由于filterApples方法只能接受对象,所以你必须把代码包裹ApplePredicate对象里。你的做法就类似与在内联“传递代码”。你会在下一节中看到,通过使用Lambda,可以直接将表达式"red".equals(apple.getColor())
&&apple.getWeight() > 150传递给filterApples方法,省去定义多个ApplePredicate的实现类,从而去掉不必要的代码。
匿名类
匿名类允许你随用随建。
第五次尝试:使用匿名类:
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
});
但是匿名类还不够好。第一,它看起来很笨重,因为它占用了很多空间。第二,让我们觉得它用起来很让人费解。好的代码应该是一目了然的。虽然匿名类在一定程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满意。下面简单介绍一下Lambda表达式是怎么让代码更干净的。
第六次尝试:使用Lambda表达式:
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
不得不承认这代码看上去比先前干净很多。
第七次尝试:将List类型抽象化:
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T e : list) {
if (p.test(e)) {
result.add(e);
}
}
return result;
}
现在你可以把filter方法用在香蕉、橘子Integer或String的列表上了。
eg.
List<Apple> redApples =
filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers =
filter(numbers, (Integer i) -> i % 2 == 0);
酷不酷?你现在在灵活性和简洁性之间找到了最佳的平衡点,这在Java8之前是不能做到的!
真实的例子
用Comparator来排序
Alice想要根据苹果的重量对库存进行排序(ps,Java8中,List自带了一个sort方法)我们用Collections.sort
// java.util.Comparator
public interface Comparator<T> {
public int compare(T o1, T o2);
}
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
用Lambda表达式的话,看起来是这样的:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
用 Runnable 来执行代码块
// java.lang.Runnable
public interface Runnable{
public void run();
}
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});
用Lambda表达式的话,看起来是这样的:
Thread t = new Thread(() -> System.out.println("Hello world"));
小结
以下是你从本章中学到的关键概念
1、行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它,完成不同行为的能力。
2、行为参数话可以让代码更好的适应不断变化的要求,减轻未来的工作量。
3、传递代码,就是将新行为作为参数传递给方法。为接口声明许多只使用一次的实体类二造成的啰嗦代码,在Java8之前可以使用匿名类来减少。
4、JavaAPI包含很多可以用不同行为进行参数化的方法,包括排序、线程等。