1. 程式人生 > 其它 >Java8 in action(1) 通過行為引數化傳遞程式碼--lambda代替策略模式

Java8 in action(1) 通過行為引數化傳遞程式碼--lambda代替策略模式

豬腳:以下內容參考《Java 8 in Action》

釋出:https://ryan-miao.github.io/2017/07/15/java8-in-action-2/

原始碼:github

需求

果農需要篩選蘋果,可能想要綠色的,也可能想要紅色的,可能想要大蘋果(>150g),也可能需要紅的大蘋果。基於此等條件,編寫篩選的程式碼。

1. 策略模式解決方案

1.1 最直觀的做法

首先,已知資訊是一筐蘋果(List<Apple> inventory),但篩選條件多種多樣。我們可以根據不同的條件寫不同的方法來達到目的。比如,找出綠色的蘋果:

public static List<Apple> filterGreenApples(List<Apple> inventory){
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory){
        if ("green".equals(apple.getColor())){
            result.add(apple);
        }
    }

    return result;
}

同樣的,可以編寫filterRed, filterWeight等等。但必然出現重複程式碼,違反軟體工程原則Don't repeast yourself。而且,篩選的類也會顯得臃腫。

現在,有一種更容易維護,更容易閱讀的策略模式來實現這個需求。

1.2 策略模式

由於多種篩選條件的結果都是返回一個boolean值,那麼可以把這個條件抽取出來,然後在篩選的時候傳入條件。這個篩選條件叫做謂詞

建立謂詞介面:

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

新增幾個判斷條件:

public class AppleGreenColorPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
public class AppleRedAndHeavyPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor()) && apple.getWeight() >150;
    }
}

篩選的方法:

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate){
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
        if (predicate.test(apple)){
            result.add(apple);
        }
    }

    return result;
}

這樣,我們就可以根據不同的條件進行篩選了。

List<Apple> inventory = new ArrayList<>();
inventory.add(new Apple("red", 100));
inventory.add(new Apple("red", 200));
inventory.add(new Apple("green", 200));
List<Apple> redHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
Assert.assertEquals(1, redHeavyApples.size());
Assert.assertEquals(200, redHeavyApples.get(0).getWeight());

以上的程式碼設計方案几乎是最好理解和擴充套件的了,當條件發生改變的時候只要增加一個類就可以。但java8提供了更好的選擇,一種你只要宣告一個介面,具體實現不用管,只有當使用的時候才去關心。

1.3 方法傳遞

java8提供了把方法當做引數傳遞的能力。這樣,上面的程式碼就可以這樣寫:

List<Apple> apples = filterApples(inventory, apple -> "red".equals(apple.getColor()) && apple.getWeight() > 150);
Assert.assertEquals(1, apples.size());
Assert.assertEquals(200, apples.get(0).getWeight());

除了介面宣告,不需要實現介面的類。我們只需要傳入一個類似匿名內部類的東西,是的,lambda表示式和匿名內部類是可以互相轉換的。

如此,我們設計介面的時候只要宣告一個介面作為引數,然後再呼叫的時候把邏輯當做引數傳進去。這個在我看來就是傳遞方法了。就像Javascript,可以把一個方法當做引數。

與之前的設計模式相比,lambda可以不用寫那麼類。

1.4 新需求

現在,果農需要包裝蘋果。包裝的方式有多種,我將包裝的結果打印出來,就是列印的樣式也有多種。比如:

A light green apple

或者

An apple of 150g

上面是兩種列印方式,按照之前的策略模式需要建立兩個類。下面採用lambda來實現。

public interface AppleFormatter {
    String format(Apple apple);
}

public class AppleOutput{
    public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){
        for (Apple apple : inventory) {
            String format = formatter.format(apple);
            System.out.println(format);
        }
    }
    
    public static void main(String[] args){
        List<Apple> inventory = new ArrayList<>();
        inventory.add(new Apple("red", 100));
        inventory.add(new Apple("red", 200));
        inventory.add(new Apple("green", 200));

        prettyPrintApple(inventory, new AppleFormatter() {
            @Override
            public String format(Apple apple) {
                String characteristic = apple.getWeight()>150?"heavy":"light";
                return "A " + characteristic + " " + apple.getColor() + " apple.";
            }
        });

        prettyPrintApple(inventory, apple -> "An apple of " + apple.getWeight() + "g");

    }
}

控制檯列印:

A light red apple.
A heavy red apple.
A heavy green apple.
An apple of 100g
An apple of 200g
An apple of 200g

如果使用IntelIJ IDEA作為編輯器,那麼肯定會忍受不了匿名內部類,因為IDEA會不停的提示你:匿名內部類可以轉變為方法引數。

1.5 更普遍的用法

上面的篩選只是針對Apple的,那麼是否可以推廣開來呢?下面針對List型別抽象化來構造篩選條件。

建立一個條件介面:

public interface Predicate<T> {
    boolean test(T t);
}

更新一個更普遍的filter:

public static <T> List<T> filter(List<T> list, Predicate<T> p){
    List<T> result = new ArrayList<T>();
    for (T e : list) {
        if (p.test(e)){
            result.add(e);
        }
    }

    return result;
}

那麼,可能這樣用:

public static void main(String[] args) {
    List<Apple> appleList = new ArrayList<>();
    appleList.add(new Apple("red", 100));
    appleList.add(new Apple("red", 160));
    appleList.add(new Apple("green", 60));

    List<Apple> redApples = filter(appleList, (Apple apple) -> "red".equals(apple.getColor()));
    Assert.assertEquals(2, redApples.size());

    List<Integer> numberList = Arrays.asList(1,2,3,4,5,6,7,8,9);
    List<Integer> lessThan4Numbers = filter(numberList, (Integer num) -> num < 4);
    Assert.assertEquals(3, lessThan4Numbers.size());

}

1.6 排序

行為引數化的過程掌握後,很多東西就會自然而然的使用了。比如排序。果農需要將蘋果按照大小排序呢?

java8中List是有預設方法的:

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

其實就是將以前手動排序封裝了。那麼,蘋果的排序就可以傳入一個比較器實現:

@Test
public void sort(){
    List<Apple> appleList = new ArrayList<>();
    appleList.add(new Apple("red", 100));
    appleList.add(new Apple("red", 160));
    appleList.add(new Apple("green", 60));
    
    appleList.sort((o1, o2) -> o1.getWeight()-o2.getWeight());
}

根據IDEA的提示,進一步:

appleList.sort(Comparator.comparingInt(Apple::getWeight));

這裡就涉及了多次行為傳參了。後面再說。

1.7 Runnable

多執行緒Runnable的時候經常會採用匿名內部類的做法:

@Test
public void testRunnable(){
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("running");
        }
    };

    new Thread(runnable).start();
}

採用lambda行為傳參就變為:

@Test
public void testRunnable(){
    Runnable runnable = () -> System.out.println("running");

    new Thread(runnable).start();
}

小結

本次測試主要理解如下內容:

  • 行為引數化,就是一個方法接受多個不同的行為作為引數,並在內部使用它們,完成不同行為的能力。
  • 傳遞程式碼,就是將行為作為引數傳遞給方法。

參考