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();
}
小結
本次測試主要理解如下內容:
- 行為引數化,就是一個方法接受多個不同的行為作為引數,並在內部使用它們,完成不同行為的能力。
- 傳遞程式碼,就是將行為作為引數傳遞給方法。