2.3 对付啰唆

我们都知道,人们不愿意用那些很麻烦的功能或概念。目前,当要把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。下面的程序总结了你目前看到的一切。这真是很啰唆,很费时间!

代码清单 2-1 行为参数化:用谓词筛选苹果

  1. public class AppleHeavyWeightPredicate implements ApplePredicate{ ←---- 选择较重苹果的谓词
  2. public boolean test(Apple apple){
  3. return apple.getWeight() > 150;
  4. }
  5. }
  6. public class AppleGreenColorPredicate implements ApplePredicate{ ←---- 选择绿苹果的谓词
  7. public boolean test(Apple apple){
  8. return GREEN.equals(apple.getColor());
  9. }
  10. }
  11. public class FilteringApples{
  12. public static void main(String...args) {
  13. List<Apple> inventory = Arrays.asList(new Apple(80, GREEN),
  14. new Apple(155, GREEN),
  15. new Apple(120, RED));
  16. List<Apple> heavyApples =
  17. filterApples(inventory, new AppleHeavyWeightPredicate()); ←---- 结果是一个包含一个155AppleList
  18. List<Apple> greenApples =
  19. filterApples(inventory, new AppleGreenColorPredicate()); ←---- 结果是一个包含两个绿AppleList
  20. }
  21. public static List<Apple> filterApples(List<Apple> inventory,
  22. ApplePredicate p) {
  23. List<Apple> result = new ArrayList<>();
  24. for (Apple apple : inventory){
  25. if (p.test(apple)){
  26. result.add(apple);
  27. }
  28. }
  29. return result;
  30. }
  31. }

费这么大劲儿真没必要,能不能做得更好呢?Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类。它可以帮助你进一步改善代码,让它变得更简洁。但这也不完全令人满意。2.3.3节简短地介绍了Lambda表达式如何让你的代码更易读,下一章将会对此进行更加详细的讨论。

2.3.1 匿名类

匿名类和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。

2.3.2 第五次尝试:使用匿名类

下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:

  1. List<Apple> redApples = filterApples(inventory, new ApplePredicate() { ←---- 使用匿名类参数化filterApples方法的行为
  2. public boolean test(Apple apple){
  3. return RED.equals(apple.getColor());
  4. }
  5. });

GUI应用程序中经常使用匿名类来创建事件处理器对象(下面的例子使用的是Java FX API,一种现代的Java UI平台):

  1. button.setOnAction(new EventHandler<ActionEvent>() {
  2. public void handle(ActionEvent event) {
  3. System.out.println("Whoooo a click!!");
  4. }
  5. });

但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。还拿前面的例子来看,如下面的粗体代码所示:

  1. List<Apple> redApples = filterApples(inventory, new ApplePredicate() { (以下6行)很多模板代码
  2. public boolean test(Apple a){
  3. return RED.equals(a.getColor());
  4. }
  5. });
  6. button.setOnAction(new EventHandler<ActionEvent>() {
  7. public void handle(ActionEvent event) {
  8. System.out.println("Whoooo a click!!");
  9. }
  10. });

第二,很多程序员觉得它用起来很让人费解。比如,测验2.2展示了一个经典的Java谜题,它让大多数程序员都措手不及。你来试试看吧。

测验2.2:匿名类谜题

下面的代码执行时会有什么样的输出,456还是42

  1. public class MeaningOfThis {
  2. public final int value = 4;
  3. public void doIt() {
  4. int value = 6;
  5. Runnable r = new Runnable(){
  6. public final int value = 5;
  7. public void run(){
  8. int value = 10;
  9. System.out.println(this.value);
  10. }
  11. };
  12. r.run();
  13. }
  14. public static void main(Stringargs) {
  15. MeaningOfThis m = new MeaningOfThis();
  16. m.doIt(); ←—— 这一行的输出是什么?
  17. }
  18. }

答案:会输出5,因为this指的是包含它的Runnable,而不是外面的类MeaningOfThis

整体来说,啰唆就不好。它让人不愿意使用语言的某种功能,因为编写和维护啰唆的代码需要很长时间,而且代码也不易读。好的代码应该是一目了然的。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰唆问题,但它仍不能令人满意。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法或是EventHandler中的handle方法)。

在理想的情况下,我们想鼓励程序员使用行为参数化模式,因为正如你在前面看到的,它让代码更能适应需求的变化。在第3章中,你会看到Java 8的语言设计者通过引入Lambda表达式——一种更简洁的传递代码的方式——解决了这个问题。好了,悬念够多了,下面简单介绍一下Lambda表达式是怎么让代码更干净的。

2.3.3 第六次尝试:使用Lambda表达式

上面的代码在Java 8里可以用Lambda表达式重写为下面的样子:

  1. List<Apple> result =
  2. filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

不得不承认这段代码看上去比先前干净很多。这很好,因为它看起来更像问题陈述本身了。现在已经解决了啰唆的问题。图2-4对我们到目前为止的工作做了一个小结。

2.3 对付啰唆 - 图1

图 2-4 行为参数化与值参数化

2.3.4 第七次尝试:将List类型抽象化

在通往抽象的路上,还可以更进一步。目前,filterApples方法还只适用于Apple。你还可以将List类型抽象化,从而超越你眼前要处理的问题:

  1. public interface Predicate<T>{
  2. boolean test(T t);
  3. }
  4. public static <T> List<T> filter(List<T> list, Predicate<T> p){ ←---- 引入类型参数T
  5. List<T> result = new ArrayList<>();
  6. for(T e: list){
  7. if(p.test(e)){
  8. result.add(e);
  9. }
  10. }
  11. return result;
  12. }

现在你可以把filter方法用在香蕉、橘子、Integer或是String的列表上了。这里有一个使用Lambda表达式的例子:

  1. List<Apple> redApples =
  2. filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
  3. List<Integer> evenNumbers =
  4. filter(numbers, (Integer i) -> i % 2 == 0);

酷不酷?你现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!