Java 8 行为参数化

Posted 海盗屋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 8 行为参数化相关的知识,希望对你有一定的参考价值。

  行为参数化就是可以帮助你处理频繁变更需求的一种软件开发模式。它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如:你可以将代码块作为参数传递给另一个方法,稍后再去执行它。

 

应对不断变化的需求

1.第一次尝试:实现一个功能,从一个列表中筛选出绿色苹果的功能。

首先准备Apple实体类

public class Apple {
    private Integer Id;
    private String Color;
    private Double Weight;

    //getter..setter..toString..
}

编写过滤出绿色苹果的功能

    public static List<Apple> filter(List<Apple> apples){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : apples){
            //筛选出绿色的苹果
            if("green".equals(apple.getColor())){
                result.add(apple);
            }
        }
        return result;
    }

测试数据

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(1,"green",18.0),
                new Apple(2,"yellow",36d),
                new Apple(3,"red",42d),
                new Apple(4,"green",15d),
                new Apple(5,"red",16d)
        );


        List<Apple> greenApple = filter(apples);
        System.out.println(greenApple);

    }

输出结果:

[Apple{Id=1, Color=\'green\', Weight=\'18.0\'}, Apple{Id=4, Color=\'green\', Weight=\'15.0\'}]

实现功能了,现在产品说我又想要筛选红色的苹果了,最简单的办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果,然而产品想要筛选更多颜色的苹果,黄色、橘黄色、大酱色等,这种方法就不行了,将颜色作为参数

 

2.第二次尝试:把颜色作为参数

    //把颜色作为参数
    public static List<Apple> filterApplesByColor(List<Apple> apples,String color){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : apples){
            if(color.equals(apple.getColor())){
                result.add(apple);
            }
        }
        return result;
    }

现在只需要这样调用,产品就会满意了。

List<Apple> yellowApple = filterApplesByColor(apples,"yellow");
List<Apple> redApple = filterApplesByColor(apples,"red");

然后产品又跑过来说,要是能区分苹果大小就太好了,把大于32的分为大苹果,小于32的分为小苹果。

于是你下了下面的方法,又增加了重量的参数

    //根据重量来筛选苹果
    public static List<Apple> filterApplesByWeight(List<Apple> apples,Double weight){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : apples){
            if(apple.getWeight() > weight){
                result.add(apple);
            }
        }
        return result;
    }

你终于实现了产品的需求,但是请注意,你复制了大部分的代码来实现遍历苹果列表,并对每个苹果筛选条件。这有点令人失望,因为它打破了Don\'t Repeat Yourself(不要重复你自己)的软件工程原则。

 

3.第三次尝试:把你能想到的每个属性做筛选

你可以将颜色和重量结合为一个方法,称为filterColorOrWeight,然后增加一个参数来区分需要筛选哪个属性。

    //按颜色筛选或按重量筛选
    public static List<Apple> filterColorOrWeight(List<Apple> apples, String color, Double weight, boolean flag) {
        List<Apple> result = new ArrayList<>();
        for (Apple apple : apples) {
            if ((flag && color.equals(apple.getColor())) || (!flag && apple.getWeight() > weight)) {
                result.add(apple);
            }
        }
        return result;
    }

然后你可以这样使用:

List<Apple> yellowApples = filterColorOrWeight(apples, "yellow",0d,true);
List<Apple> bigApples = filterColorOrWeight(apples, "",32d,false);

这个解决方案再差不过了,首先客户端代码看上去烂透了,true和false是什么意思?此外,这个解决方案还是不能很好的区应对变化的需求,如果产品又让你对苹果的其他不同的属性做筛选,比如大小、形状、产地等,又该怎么办?或者要求你组合属性,作更复杂的查询,比如绿色的大苹果,又该怎么办?

 

行为参数化

  你在上一节中看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。 一种解决方案是对你选择的标准建模:比如根据Apple的某些属性(是否是绿色,是否大于32)来返回一个boolean值,我们把它称之为谓词。首先我们来定义一个借口来对选择标准建模。

public interface ApplePredicate {
    boolean test(Apple apple);
}

现在就可以使用ApplePredicate的多个实现来代表不同的选择标准了,比如:

public class AppleGreenColorPredicate implements ApplePredicate {
    //绿苹果谓词
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
    //大苹果谓词
    public boolean test(Apple apple) {
        return apple.getWeight() > 32;
    }
}

现在,你可以把这些标准看做filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一组算法,把他们封装起来(成为“策略”),然后在运行时选择一个算法。在这里,ApplePredicate就是算法组,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。

你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。

 

4.第四次尝试:根据抽象条件筛选

    //根据抽象条件筛选
    public static List<Apple> filterApples(List<Apple> apples,ApplePredicate p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : apples){
            if(p.test(apple)){
                result.add(apple);
            }
        }
        return result;
    }

使用的时候这样:

List<Apple> greenApple = filterApples(apples,new AppleGreenColorPredicate());
List<Apple> bigApple = filterApples(apples,new AppleHeavyWeightPredicate());

1.传递代码/行为

到这里可以小小的庆祝一下了,这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将他们传递给filterApples方法。比如现在产品让你找出所有重量超过80克的红苹果,你只需创建一个类来实现ApplePredicate就行了,你的代码现在足够灵活,可以应对任何设计苹果属性的需求变更了:

public class AppleRedAndBigPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor()) && apple.getWeight() > 80;
    }
}

传递给filterApples方法,无需修改filterApples方法的内部实现:

List<Apple> redBigApple = filterApples(apples,new AppleRedAndBigPredicate());

现在你做了一件很酷的事:filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,你把filterApples方法行为参数化了!

 

在上一个例子中,唯一重要的代码是test方法的实现,正式它定义了filterApples方法的新行为。由于filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。这种做法类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。

 

2.多种行为,一个参数

  行为参数化的好处在于你可以把遍历集合的逻辑和 对集合中每个元素的判断逻辑 区分开来。这样就可以重复使用同一个方法,给它不同的行为来达到不同的目的。

 

行为参数化练习

  编写printApple方法,实现一个功能,以多种方式根据苹果生成一个String输出(例如,可以让printApple方法只打印每个苹果的颜色,或者让它打印什么颜色的大(小)苹果)

创建AppleFormater接口

public interface AppleFormater {
    String accept(Apple a);
}

创建AppleWeightFormater、AppleColorFormater 来实现接口

public class AppleWeightFormater implements AppleFormater {
    @Override
    public String accept(Apple a) {
        return "这是一个" + a.getColor() + "的" + (a.getWeight() > 32 ? "大" : "小") + "苹果";
    }
}
public class AppleColorFormater implements AppleFormater {
    @Override
    public String accept(Apple a) {
        return "一个" + a.getColor() + "的苹果";
    }
}

然后创建printApple方法,给它传递不同的formater对象

    public static void printApple(List<Apple> apples,AppleFormater af) {
        for (Apple apple : apples) {
            String output = af.accept(apple);
            System.out.println(output);
        }
    }

测试:

printApple(apples, new AppleWeightFormater());
printApple(apples, new AppleColorFormater());

 

现在,你可以把行为抽象出来了,让你的代码更加适应需求的变化,但是这个过程很啰嗦,因为你需要声明很多只要实例化一次的类。

 

匿名类

使用匿名类来对付这些只需要使用一次的类。

比如说使用匿名类来新增加一个打印样式为:哇塞,这是一个大苹果啊?

printApple(apples, new AppleFormater() {
  public String accept(Apple a) {
    return "哇塞,这是一个" + (a.getWeight() > 32 ? "大" : "小") + "苹果啊?";
  }
});

使用匿名类虽然解决了为一个接口声明好几个实体类的啰嗦问题,但它仍然不能令人满意。

 

使用Lambda表达式

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

printApple(apples, (Apple a) -> "哇塞,这是一个" + (a.getWeight() > 32 ? "大" : "小") + "苹果啊?");

这样看起来是不是更清爽多了?更像是在描述问题本身!关于lambda将会在下节介绍。

 

将List类型抽象化

目前filterApples方法还只适用于Apple,可以将List类型抽象化,让它支持香蕉、橘子、Integer或是String的列表上!

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;
    }

测试:

List<Apple> greenApple = filter(apples,(Apple apple) -> "green".equals(apple.getColor()));
System.out.println(greenApple);

List<Integer> evenNumbers = filter(Arrays.asList(1,2,3,4,5,6,10),(Integer i) -> i%2 ==0);
System.out.println(evenNumbers);

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

 

真实的例子

  你现在已经看到,行为参数化是一个很有用的模式,它能够轻松的适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用穿件的行为(例如对Apple的不同谓词)将方法的行为参数化。

1.使用Comparator来排序

  对集合排序是一个常见的任务,比如,产品过来说想按照苹果的重量进行排序。在java 8中,List自带了一个sort方法(也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:

package java.util;

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

因此,你可以随时创建Comparator的实现,用sort方法来排序,使用匿名类按照重量升序排序:

apples.sort(new Comparator<Apple>() {

        @Override
         public int compare(Apple o1, Apple o2) {
            return o1.getWeight().compareTo(o2.getWeight());
         }
});

使用lambda如下:

apples.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

 

2.用Runnable执行代码块

  线程就像是轻量级的进程:它们自己执行一个代码块。在Java离可以使用Runnable接口表示一个要执行的代码块。

package java.lang;

@FunctionalInterface
public interface Runnable {
    
    public abstract void run();
}

使用匿名类创建执行不同行为的线程:

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("t111");
    }
});

 

使用Lambda:

Thread t2 = new Thread(() -> System.out.println("t2222"));

 

小结:

  1.行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。

  2.行为参数化可让代码更好地适应不断变化的需求,减轻未来的工作量。

  3.传递代码,就是将新行为作为参数传递给方法,在java 8 之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在java 8 之前可以使用匿名类来减少。

  4.java API 包含很多可以用不同行为进行参数化的方法、包括排序、线程等。

 

以上是关于Java 8 行为参数化的主要内容,如果未能解决你的问题,请参考以下文章

行为参数化与lambda表达式 - 读《Java 8实战》

Java 8实战 (笔记)第二章

Java8新特性之:行为参数化传递代码

Java8-通过行为参数化传递代码

Java8新特性系列一:行为参数化

侠说java8-行为参数化(开山篇)