Java 8 (7) 重構、測試和調試

爲改善可讀性和靈活性重構代碼css

  看到這裏咱們已經可使用lambda和stream API來使代碼更簡潔,用在新項目上。但大多數並非全新的項目,而是對現有代碼的重構,讓它變的更簡潔可讀,更靈活。java

改善代碼的可讀性程序員

  別人理解這段代碼的難易程度,改善可讀性意味着你要確保你的代碼能很是容易的被別人理解和維護。爲了確保這點,有幾個步驟能夠嘗試:算法

    1.使用Java 8,你能夠減小冗長的代碼,讓代碼更易於理解設計模式

    2.經過方法引用和Stream API,你的代碼會變得更直觀app

    3.重構代碼,用Lambda表達式取代匿名類框架

    4.用方法引用重構Lambda表達式編輯器

    5.用Stream API重構命令式的數據處理ide

 

從匿名類到Lambda表達式的轉換函數

好比前面的Runnable例子:

        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("hh");
            }
        };

        //使用Lambda
        Runnable r1 = ()->System.out.println("hh");

可是,在某些狀況下,匿名類轉換爲Lambda表達式多是一個比較複雜的過程,由於在匿名類中和Lambda表達式中的this和super的含義是不一樣的,在匿名類中this是類的自身,Lambda表達式中,this表明的是包含類。還有在匿名類中能夠屏蔽包含類的變量,而Lambda表達式不能:

        int a = 10;
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                int a = 2; //正常
                System.out.println(a);
            }
        };

        Runnable r3 = ()->{
            int a = 2; //編譯報錯
            System.out.println(a);
        };

如今大多數編輯器,好比InteliJ能夠幫助咱們自動檢查是否能夠轉換爲Lambda表達式。

 

從Lambda表達式到方法引用的轉換

Lambda表達式很是適用於須要傳遞代碼片斷的場景。爲了改善代碼可讀性,也請儘可能使用方法引用,如:

        Map<String, List<Dish>> group1 = menu.stream().collect(groupingBy(c -> {
            if (c.getCalories() <= 400) {
                return "low";
            } else if (c.getCalories() > 400) {
                return "higt";
            } else {
                return "other";
            }
        }));

將Lambda表達式抽取到一個單獨的方法中,將其做爲參數傳遞給groupingBy方法,在Dish類中添加方法:

    public String getCaloricLevel(){
        if (Calories <= 400) {
            return "low";
        } else if (Calories > 400) {
            return "higt";
        } else {
            return "other";
        }
    }

而後經過方法引用調用這個方法:

Map<String, List<Dish>> group2 = menu.stream().collect(groupingBy(Dish::getCaloricLevel));

除此以外,咱們還應該儘可能考慮使用靜態輔助方法,好比comparing、maxBy。這些方法設計時就考慮了會結合方法引用一塊兒使用。

apples.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
  //使用方法引用
  apples.sort(Comparator.comparing(Apple::getWeight));

還有不少通用的歸約操做,好比sum、max,都有內建的輔助方法能夠和方法引用結合使用。

int totalCalories = menu.stream().map(Dish::getCalories).reduce(0,(c1,c2)->c1+c2);
  //歸約
  int totalCalories2 = menu.stream().mapToInt(Dish::getCalories).sum();
  int totalCalories3 = menu.stream().collect(summingInt(Dish::getCalories));

 

從命令式的數據處理切換到Stream

  建議將全部迭代器這種數據處理模式處理集合的代碼都轉換爲Stream API的方式,由於Stream API有短路和延遲載入的方式,而且流能夠很清楚的表達意圖。

好比下面的命令式代碼使用了兩種模式:篩選和抽取,這兩種模式混合在了一塊兒,這樣的代碼結構迫使程序員必須完全搞清楚程序的每一個細節才能理解代碼的功能。此外,實現並行運行的程序所面對的困難也多的多:

List<String> dishNames = new ArrayList<>();
        for(Dish dish : menu){
            if(dish.getCalories() > 300){
                dishNames.add(dish.getName());
            }
        }

        //Stream API
        List<String> dishNames2 = menu.stream().filter(c->c.getCalories()>300).map(Dish::getName).collect(toList());

 

增長代碼的靈活性

  咱們曾經介紹過,Lambda表達式有利於行爲參數化。你可使用不一樣的Lambda表示不一樣的行爲,並將它們做爲參數傳遞給函數去處理執行。

1.採用函數接口

  沒有函數接口,就沒法使用Lambda表達式。所以,須要在代碼中引入函數接口。這裏介紹兩種通用的模式,能夠按照這兩種模式重構代碼,它們分別是:有條件的延遲執行和環繞執行。

2.有條件的延遲執行

        if (logger.isLoggable(Level.FINER)){
            logger.finer("Problem: " + generateDiagnostic());
        }

好比Logger類中的finer方法,每次使用時都要去判斷一下日誌級別。比較好的作法是使用log方法,該方法在輸出日誌消息以前,會在內部檢查日誌對象是否已經爲恰當的等級:

logger.log(Level.FINER , "Problem:" + generateDiagnostic());

這種代碼的好處是,你不須要在代碼中插入那些條件判斷,但這段代碼仍是要每次都要判斷。

Lambda能夠施展拳腳了,你須要作的僅僅是延遲消息構造,如此一來,日誌就只會在某些特定的狀況下才開啓,log有一個重載版本,接受一個Supplier做爲參數。

logger.log(Level.FINER , () -> "Problem:" + generateDiagnostic());

此時,若是日誌的級別設置恰當,log則會在內部執行做爲參數傳遞進來的Lambda表達式。

3.環繞執行

環繞執行就是擁有不少一樣的準備和清理代碼,這時,徹底能夠講這部分代碼用Lambda實現。這種方式的好處是能夠重用準備和清理階段的邏輯,減小重複冗餘的代碼。

System.out.println(processFile((BufferedReader br) -> br.readLine() + br.readLine()));
    
public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("/Users/baidawei/Desktop/test.txt"))) {
            //處理BufferedReader對象
            return p.process(br);
        }
    }

@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;

}

這是第二章的例子,憑藉函數式接口BufferedReaderProcessor達成的,經過這個接口能夠傳遞各類Lambda表達式對BufferedReader對象進行處理。

 

使用Lambda重構面向對象的設計模式

  新的語言特性經常讓現存的變成模式或設計黯然失色。好比,java 5中引入了 for-each循環,因爲它的穩健性和簡介性,已經替代了不少迭代器的使用。Java 7中推出的菱形<>操做符,讓你們無需顯示使用泛型。

  對設計經驗的概括總結稱爲設計模式。Lambda表達式爲解決傳統設計模式的問題提供了新的解決方案。主要討論五個設計模式:

策略模式

  策略模式表明瞭解決一類算法的通用解決方案,你能夠在運行時選擇使用哪一種方案。好比以前的篩選蘋果例子:

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

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


    //根據抽象條件篩選
    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;
    }

將這種囉嗦的模板代碼使用Lambda表達式來替換:

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

使用Lambda表達式避免了採用策略設計模式時的模板代碼。

 

模板方法

  若是你須要採用某個算法的框架,同時又但願有必定的靈活度,能對它的某些部分進行改進,那麼採用模板方法設計模式是比較通用的方案。換句話說,你但願使用這個算法,可是須要對其中的某些行爲進行改進,才能達到但願的效果。

  例如,你須要編寫一個簡單的在線銀行應用。實現功能以下:輸入帳號,不一樣的銀行能夠給你不一樣的獎勵,能夠定義以下抽象類:

public abstract class OnlineBanking {
    public void processCustomer(int id){
        Customer c = Database.getCustomerById(id);
        makeCustomerHappy(c);
    }
    abstract  void makeCustomerHappy(Customer c);
}

  processCustomer方法經過 客戶id 爲客戶提供服務,不一樣的分行能夠經過繼承OnlineBanking類,對方法進行實現。

  使用Lambda表達式一樣能夠解決這些問題,而且無需繼承OnlineBanking類。可是要改造一下processCustomer方法,增長一個參數。Consumer<Customer>類型的參數,消費它。

public class OnlineBanking {
    public void processCustomer(int id,Consumer<Customer> makeCustomerHappy){
        Customer c = Database.getCustomerById(id);
        makeCustomerHappy.accept(c);
    }
}

如今就能夠傳遞lambda表達式了。

new OnlineBanking().processCustomer(1333,(Customer c) -> System.out.println(c.getName()));

 

觀察者模式

  若是一個對象(稱爲主題)須要自動的通知其餘多個對象(稱爲觀察者),就是觀察者模式。好比通知系統,cctv 1 2 3 4 5 都訂閱了新聞,若是新聞中存在感興趣的關鍵詞時就獲得特別通知。

定義一個觀察者接口,它將不一樣的觀察者聚合在一塊兒。它僅有一個notify的方法,一旦接收到一條新的新聞,該方法就會被調用:

public interface Observer {
    void notify(String news);
}

這裏舉例cctv3 音樂頻道和 cctv5 體育頻道 

public class CCTV5 implements Observer {
    @Override
    public void notify(String news) {
        if(news != null && news.contains("體育")){
            System.out.println("cctv5 播放體育:" + news);
        }
    }
}
public class CCTV3 implements Observer {
    @Override
    public void notify(String news) {
        if(news != null && news.contains("音樂")){
            System.out.println("cctv3 播放音樂:" + news);
        }
    }
}

最後 主題接口

public interface Subject {
    //註冊觀察者
    void registerObserver(Observer o);
    //通知它的觀察者一個新聞到來
    void notifyObservers(String news);
}

實現Feed類,註冊一個觀察者列表,一條新聞到達時,他就會進行通知。

        Feed feed = new Feed();
        feed.registerObserver(new CCTV3());
        feed.registerObserver(new CCTV5());
        feed.notifyObservers("一個音樂節目,周杰倫的龍捲風!");

CCTV3 會關注這條新聞,這裏也可使用Lambda表達式,能夠不用繼承Observer類,寫那些模板代碼。如:

        feed.registerObserver((String news)->{
            if(news != null && news.contains("音樂")){
                System.out.println("cctv3 播放着音樂:" + news);
            }
        });

觀察者中的代碼有可能邏輯十分複雜,還有可能定義了多個方法,這時最好依舊使用類的方式。

 

責任鏈模式

  一個處理對象可能須要在完成一些工做後,將結果傳遞給另外一個對象,這個對象接着作一些工做,再轉交給下一個對象,以此類推。例如:

public abstract class ProcessingObjet<T> {
    protected ProcessingObjet<T> successor;

    public void setSuccessor(ProcessingObjet<T> successor) {
        this.successor = successor;
    }

    public T handle(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handle(r);
        }
        return r;
    }

    abstract protected T handleWork(T input);
}

能夠經過不一樣的類繼承ProcessingObject類,提供handleWork方法來進行建立。

public class HandlerTextProcssing extends ProcessingObjet<String> {
    @Override
    protected String handleWork(String input) {
        return "哈嘍你們好啊" + input;
    }
}
public class SpellCheckerProcessing extends ProcessingObjet<String> {
    @Override
    protected String handleWork(String input) {
        return input.replace("哈嘍","hello,");
    }
}

 如今就能夠把這兩個對象結合起來,構造一個操做序列!

        ProcessingObjet<String> p1 = new HandlerTextProcssing();
        ProcessingObjet<String> p2 = new SpellCheckerProcessing();
        p1.setSuccessor(p2);
        String result = p1.handle("你說啥?");
        System.out.println(result);
//hello,你們好啊你說啥?

這個模式看起來像是在鏈接函數,咱們可使用Lambda來連接這些函數:

        UnaryOperator<String> headerProcessing = (String text) -> "哈嘍你們好啊" + text;
        UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replace("哈嘍","hello,");
        Function<String,String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
        String result1 = pipeline.apply("你說啥?");

 

工廠模式

  使用工廠模式,你無需像客戶暴露實例化的邏輯就能完成對象的建立。好比,須要一種方式建立不一樣的金融方式:貸款、期權、股票:

public class ProductFactory{
    public static Product createProduct(String name){
        switch (name){
            case "loan" : return new Loan();
            case "stock" : return new Stock();
            case "bond" : return new Bond();
            default: return null;
        }
    }
}

這裏的Loan、Stock、Bond都是Product的子類。 在前臺建立時能夠這樣:

Product p = ProductFactory.createProduct("loan");

使用Lambda表達式,咱們可使用::new的方式引用構造函數:

Supplier<Product> p = Loan::new;
Loan loan = p.get();

經過這種方式,你能夠重構以前的代碼,建立一個Map,將產品名映射到對應的構造函數:

        final static Map<String,Supplier<Product>> map = new HashMap<>();
        static{
            map.put("loan",Loan::new);
            map.put("stock",Stock::new);
            map.put("bond",Bond::new);
        }

這樣就可使用工廠設計模式那樣,利用這個Map來實例化不一樣的產品。

public class ProductFactory{
    public static Product createProduct(String name){
        Supplier<Product> p =map.get(name);
        if(p!=null) return p.get();
        return null;
    }
}

若是createProduct須要接受多個參數傳遞給構造方法,這種方法擴展性不是很好。還須要本身建立函數接口來支持更多的參數。

 

測試Lambda表達式

  好的軟件工程實踐必定少不了單元測試。編寫測試用例,經過這些測試用例確保代碼中的每一個組成部分都實現預期的結果。好比簡單的Point類:

public class Point {
    int x;
    int y;

    public Point() {

    }

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }


    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public Point moveRightBy(int x) {
        return new Point(this.x + x, this.y);
    }

    @Test
    public void testMoveRightBy() {
        Point p1 = new Point(5, 5);
        Point p2 = p1.moveRightBy(10);

        assertEquals(15, p2.getX());
        assertEquals(5, p2.getY());
    }
}

測試 testMoveRightBy 方法比較容易,可是Lambda並無函數名,所以要對Lambda函數進行測試能夠藉助某個字段訪問Lambda函數。好比在Point類中添加了靜態字段compareByXAndThenY,經過該字段使用方法引用能夠訪問Comparator對象:

public class Point {
    public final  static Comparator<Point> compareByXAndThenY = Comparator.comparing(Point::getX).thenComparing(Point::getY);
    ...

Lambda表達式會生成函數式接口的一個實例,所以能夠測試該實例的行爲。 對Comparator對象類型實例compareByXAndThenY的compare方法進行調用,驗證他們是否正確:

    @Test
    public void testComparingTwoPoints(){
        Point p1 = new Point(10,15);
        Point p2 = new Point(10,20);
        int result = Point.compareByXAndThenY.compare(p1,p2);
        assertEquals(-1,result);
    }

 

可是,Lambda的初衷是將一部分邏輯封裝起來給另外一個方法使用。不該該將Lambda表達式聲明爲public,他們僅是具體的實現細節。咱們須要對使用Lambda表達式的方法進行測試:

    public static List<Point> moveAllPointsRightBy(List<Point> points, int x){
        return points.stream()
                .map(p->new Point(p.getX() + x , p.getY()))
                .collect(toList());
    }
    @Test
    public void testMoveAllPointsRightBy(){
        List<Point> points = Arrays.asList(new Point(5,5),new Point(10,5));
        List<Point> expectedPoints = Arrays.asList(new Point(15,5),new Point(20,5));
        List<Point> newPoints = Point.moveAllPointsRightBy(points,10);
        assertEquals(expectedPoints,newPoints);
    }

運行後發生異常:

java.lang.AssertionError: expected: java.util.Arrays$ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]> but was: java.util.ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]>
Expected :java.util.Arrays$ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]> 
Actual   :java.util.ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]>

這是由於Arrays.asList返回的是一個不可變的集合。沒有add方法的。

 

複雜的Lambda表達式

  若是碰到大量的業務邏輯,沒法在測試程序中引用lambda表達式時,一種策略是將Lambda表達式轉換爲方法引用,而後用常規的方式對新的方法進行測試。

 

高階函數的測試

  接受函數做爲參數的方法或者返回一個函數的方法更難測試。若是一個方法接受Lambda表達式做爲參數,你能夠採用的方案是使用不一樣的Lambda表達式對它進行測試。

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

    @Test
    public void testFilter() {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
        List<Integer> even = filter(numbers, i -> i % 2 == 0);
        List<Integer> smallerThanThree = filter(numbers, i -> i < 3);

        assertEquals(Arrays.asList(2,4),even);
        assertEquals(Arrays.asList(1,2),smallerThanThree);
    }

 

調試

1.查看棧跟蹤

  當程序忽然中止時,好比拋出一個異常。你會獲得它的棧跟蹤,經過一個又一個棧幀,你能夠了解程序失敗時的概略信息。換句話說,經過這些你能夠獲得程序失敗時的方法調用列表。

public class Debugging {
    public static void main(String[] args) {
        List<Point> points = Arrays.asList(new Point(12, 2), null);
        points.stream().map(p -> p.getX()).forEach(System.out::println); }
}

這段代碼 故意傳入了一個null,查看下面的棧跟蹤:

Exception in thread "main" java.lang.NullPointerException
    at Java8_7.Debugging.lambda$main$0(Debugging.java:9)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at Java8_7.Debugging.main(Debugging.java:9)

首先異常說了是 空引用,而後第二行是告訴你 項目名 類名 是lambda表達式 $ main方法 中的 debugging.java 9行。 由於lambda沒有名字 ,因此編譯器爲他起了一個名字。 lambda$main$0

 

2.使用日誌調試

  加入對操做的流水線進行調試,可使用forEach將流操做的結果日誌輸出。

        List<Integer> nums = Arrays.asList(1,2,3,3,4,4,4,4,3,3,2,2);
        nums.stream()
                .map(c->c+17)
                .filter(c->c%2==0)
                .limit(3)
                .forEach(System.out::println); //18 20 20

可是,若是用這種方式就會終止流。peek方法能夠不終止流又輸出當前結果。

        nums.stream()
                .peek(c->System.out.println("調用stream後:" + c))
                .map(c->c+17)
                .peek(c->System.out.println("調用map後" + c))
                .filter(c->c%2==0)
                .peek(c->System.out.println("調用filter後" + c))
                .limit(3)
                .forEach(System.out::println); //18 20 20
調用stream後:1
調用map後18
調用filter後18
18
調用stream後:2
調用map後19
調用stream後:3
調用map後20
調用filter後20
20
調用stream後:3
調用map後20
調用filter後20
20

 

小結:

  1.Lambda表達式能提高代碼的可讀性和靈活性。

  2.儘可能使用Lambda替代匿名類,可是要注意this、局部變量問題。

  3.儘可能使用方法引用來替代Lambda表達式。

  4.儘可能使用Stream API替代迭代式集合處理。

  5.Lambda表達式有助於避免使用面向對象設計模式時容易出現的模板代碼。

  6.Lambda表達式也能夠單元測試

  7.儘可能將複雜的Lambda表達式抽象到普通方法中。

  8.Lambda調試 能夠經過日誌進行分析

  9.流提供的peek方法能夠分析Stream每一步。

相關文章
相關標籤/搜索