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