日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

《Java8实战》笔记(08):重构、测试和调试

發布時間:2023/12/13 java 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《Java8实战》笔记(08):重构、测试和调试 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

重構、測試和調試

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

利用Lambda表達式,你可以寫出更簡潔、更靈活的代碼。用“更簡潔”來描述Lambda表達式是因為相較于匿名類,Lambda表達式可以幫助我們用更緊湊的方式描述程序的行為。

改善代碼的可讀性

改善代碼的可讀性到底意味著什么?

通常的理解是,“別人理解這段代碼的難易程度”。

改善可讀性意味著你要確保你的代碼能非常容易地被包括自己在內的所有人理解和維護。

為了確保你的代碼能被其他人理解,有幾個步驟可以嘗試,比如確保你的代碼附有良好的文檔,并嚴格遵守編程規范。

跟之前的版本相比較,Java 8的新特性也可以幫助提升代碼的可讀性:

  • 使用Java 8,你可以減少冗長的代碼,讓代碼更易于理解
  • 通過方法引用和Stream API,你的代碼會變得更直觀

這里會介紹三種簡單的重構,利用Lambda表達式、方法引用以及Stream改善程序代碼的可讀性:

  • 重構代碼,用Lambda表達式取代匿名類
  • 用方法引用重構Lambda表達式
  • 用Stream API重構命令式的數據處理

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

//傳統的方式,使用匿名類 Runnable r1 = new Runnable(){public void run(){System.out.println("Hello");} };//新的方式,使用Lambda表達式 Runnable r2 = () -> System.out.println("Hello");

但是某些情況下,將匿名類轉換為Lambda表達式可能是一個比較復雜的過程。 首先,匿名類Lambda表達式中的this和super的含義是不同的。

在匿名類中,this代表的是類自身,但是在Lambda中,它代表的是包含類。其次,匿名類可以屏蔽包含類的變量,而Lambda表達式不能(它們會導致編譯錯誤),譬如

int a = 10; Runnable r1 = () -> {int a = 2;//編譯錯誤System.out.println(a); };Runnable r2 = new Runnable(){public void run(){int a = 2;//一切正常System.out.println(a);} };

在涉及重載的上下文里,將匿名類轉換為Lambda表達式可能導致最終的代碼更加晦澀。實際上,匿名類的類型是在初始化時確定的,而Lambda的類型取決于它的上下文。

假設用與Runnable同樣的簽名聲明了一個函數接口,稱之為Task

interface Task{public void execute(); }public static void doSomething(Runnable r){ r.run(); } public static void doSomething(Task a){ a.execute(); }

再傳遞一個匿名類實現的Task,不會碰到任何問題:

doSomething(new Task() {public void execute() {System.out.println("Danger danger!!");} });

但是將這種匿名類轉換為Lambda表達式時,就導致了一種晦澀的方法調用,因為Runnable和Task都是合法的目標類型:

//麻煩來了: doSomething(Runnable) 和doSomething(Task)都匹配該類型 doSomething(() -> System.out.println("Danger danger!!"));

可以對Task嘗試使用顯式的類型轉換來解決這種模棱兩可的情況:

doSomething((Task)() -> System.out.println("Danger danger!!"));

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

Lambda表達式非常適用于需要傳遞代碼片段的場景。不過,為了改善代碼的可讀性,也請盡量使用方法引用。因為方法名往往能更直觀地表達代碼的意圖。

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(groupingBy(dish -> {if (dish.getCalories() <= 400) returnCaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;}));

Lambda表達式的內容抽取到一個單獨的方法中,將其作為參數傳遞給groupingBy方法。變換之后,代碼變得更加簡潔,程序的意圖也更加清晰了

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

為了實現這個方案,你還需要在Dish類中添加getCaloricLevel方法:

public class Dish{public CaloricLevel getCaloricLevel(){if (this.getCalories() <= 400) return CaloricLevel.DIET;else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT;} }

除此之外,我們還應該盡量考慮使用靜態輔助方法,比如comparing、maxBy。這些方法設計之初就考慮了會結合方法引用一起使用

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));inventory.sort(comparing(Apple::getWeight));

很多通用的歸約操作,比如sum、maximum,都有內建的輔助方法可以和方法引用結合使用。

比如,使用Collectors接口可以輕松得到和或者最大值,與采用Lambada表達式和底層的歸約操作比起來,這種方式要直觀得多。

int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

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

Stream API能更清晰地表達數據處理管道的意圖。除此之外,通過短路和延遲載入以及利用介紹的現代計算機的多核架構。

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

替代方案使用Stream API,采用這種方式編寫的代碼讀起來更像是問題陳述,并行化也非常容易

menu.parallelStream().filter(d -> d.getCalories() > 300).map(Dish::getName).collect(toList());

不幸的是,將命令式的代碼結構轉換為Stream API的形式是個困難的任務,因為你需要考慮控制流語句,比如break、continue、return,并選擇使用恰當的流操作。

增加代碼的靈活性

Lambda表達式有利于行為參數化。

你可以使用不同的Lambda表示不同的行為,并將它們作為參數傳遞給函數去處理執行。

比如,我們可以用多種方式為Predicate創建篩選條件,或者使用Comparator對多種對象進行比較。

采用函數接口

首先,你必須意識到,沒有函數接口,你就無法使用Lambda表達式。因此,你需要在代碼中引入函數接口。

聽起來很合理,但是在什么情況下使用它們呢?這里我們介紹兩種通用的模式,你可以依照這兩種模式重構代碼,利用Lambda表達式帶來的靈活性,它們分別是:

  • 有條件的延遲執行
  • 環繞執行。

有條件的延遲執行

控制語句被混雜在業務邏輯代碼之中。典型的情況包括進行安全
性檢查以及日志輸出。比如,

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

這段代碼的問題:

  • 日志器的狀態(它支持哪些日志等級)通過isLoggable方法暴露給了客戶端代碼。
  • 為什么要在每次輸出一條日志之前都去查詢日志器對象的狀態?這只能搞砸你的代碼。

更好的方案是使用log方法,該方法在輸出日志消息之前,會在內部檢查日志對象是否已經設置為恰當的日志等級:

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

這種方式更好的原因是你不再需要在代碼中插入那些條件判斷,與此同時日志器的狀態也不再被暴露出去。不過,這段代碼依舊存在一個問題。日志消息的輸出與否每次都需要判斷,即使你已經傳遞了參數,不開啟日志。


這就是Lambda表達式可以施展拳腳的地方。你需要做的僅僅是延遲消息構造,如此一來,日志就只會在某些特定的情況下才開啟(以此為例,當日志器的級別設置為FINER時)。

你可以通過下面的方式對它進行調用:

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

如果日志器的級別設置恰當,log方法會在內部執行作為參數傳遞進來的Lambda表達式。這里介紹的Log方法的內部實現如下:

public void log(Level level, Supplier<String> msgSupplier){if(logger.isLoggable(level)){log(level, msgSupplier.get());} }

從這個故事里我們學到了什么呢?如果你發現你需要頻繁地從客戶端代碼去查詢一個對象的狀態(比如前文例子中的日志器的狀態),只是為了傳遞參數、調用該對象的一個方法(比如輸出一條日志),那么可以考慮實現一個新的方法,以Lambda或者方法表達式作為參數,新方法在檢查完該對象的狀態之后才調用原來的方法。你的代碼會因此而變得更易讀(結構更清晰),封裝性更好(對象的狀態也不會暴露給客戶端代碼了)。

PS.與JS回調函數類似

環繞執行

如果你發現雖然你的業務代碼千差萬別,但是它們擁有同樣的準備和清理階段,這時,你完全可以將這部分代碼用Lambda實現。這種方式的好處是可以重用準備和清理階段的邏輯,減少重復冗余的代碼。

//傳入一個Lambda表達式 String oneLine = processFile((BufferedReader b) -> b.readLine()); String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());public static String processFile(BufferedReaderProcessor p) throws IOException {try(BufferedReader br = new BufferedReader(new FileReader("java8inaction/chap8/data.txt"))){return p.process(br);} }@FunctionalInterface public interface BufferedReaderProcessor{String process(BufferedReader b) throws IOException; }

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

對設計經驗的歸納總結被稱為設計模式。

設計軟件時,如果你愿意,可以復用這些方式方法來解決一些常見問題。這看起來像傳統建筑工程師的工作方式,對典型的場景(比如懸掛橋、拱橋等)都定義有可重用的解決方案。例如,

  • 訪問者模式常用于分離程序的算法和它的操作對象。
  • 單例模式一般用于限制類的實例化,僅生成一份對象。

策略模式

StrategyMain

策略模式代表了解決一類算法的通用解決方案,你可以在運行時選擇使用哪種方案

  • 一個代表某個算法的接口(它是策略模式的接口)。
  • 一個或多個該接口的具體實現,它們代表了算法的多種實現(比如,實體類Concrete-
    StrategyA或者ConcreteStrategyB)。
  • 一個或多個使用策略對象的客戶。

假設你希望驗證輸入的內容是否根據標準進行了恰當的格式化(比如只包含小寫字母或數字)。你可以從定義一個驗證文本(以String的形式表示)的接口入手:

public interface ValidationStrategy {boolean execute(String s); }

其次,你定義了該接口的一個或多個具體實現:

public class IsAllLowerCase implements ValidationStrategy {public boolean execute(String s){return s.matches("[a-z]+");} }public class IsNumeric implements ValidationStrategy {public boolean execute(String s){return s.matches("\\d+");} }public class Validator{private final ValidationStrategy strategy;public Validator(ValidationStrategy v){this.strategy = v;}public boolean validate(String s){return strategy.execute(s);} }

使用Lambda表達式

到現在為止,你應該已經意識到ValidationStrategy是一個函數接口了(除此之外,它還與Predicate具有同樣的函數描述)。這意味著我們不需要聲明新的類來實現不同的策略,通過直接傳遞Lambda表達式就能達到同樣的目的,并且還更簡潔:

Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+")); boolean b1 = numericValidator.validate("aaaa");Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+")); boolean b2 = lowerCaseValidator.validate("bbbb");

Lambda表達式避免了采用策略設計模式時僵化的模板代碼。

模板方法

OnlineBanking

OnlineBankingLambda

如果你需要采用某個算法的框架,同時又希望有一定的靈活度,能對它的某些部分進行改進,那么采用模板方法設計模式是比較通用的方案。換句話說,模板方法模式在你“希望使用這個算法,但是需要對其中的某些行進行改進,才能達到希望的效果”時是非常有用的。

假設你需要編寫一個簡單的在線銀行應用。通常,用戶需要輸入一個用戶賬戶,之后應用才能從銀行的數據庫中得到用戶的詳細信息,最終完成一些讓用戶滿意的操作。

不同分行的在線銀行應用讓客戶滿意的方式可能還略有不同,比如給客戶的賬戶發放紅利,或者僅僅是少發送一些推廣文件。你可能通過下面的抽象類方式來實現在線銀行應用:

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

使用Lambda表達式2

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){Customer c = Database.getCustomerWithId(id);makeCustomerHappy.accept(c); }new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello " + c.getName());

Lamba表達式能幫助你解決設計模式與生俱來的設計僵化問題

觀察者模式

觀察者模式是一種比較常見的方案,某些事件發生時(比如狀態轉變),如果一個對象(通常我們稱之為主題)需要自動地通知其他多個對象(稱為觀察者),就會采用該方案。

創建圖形用戶界面(GUI)程序時,你經常會使用該設計模式。這種情況下,你會在圖形用戶界面組件(比如按鈕)上注冊一系列的觀察者。如果點擊按鈕,觀察者就會收到通知,并隨即執行某個特定的行為。

但是觀察者模式并不局限于圖形用戶界面。比如,觀察者設計模式也適用于股票交易的情形,多個券商可能都希望對某一支股票價格(主題)的變動做出響應。


你需要為Twitter這樣的應用設計并實現一個定制化的通知系統。想法很簡單:好幾家報紙機構,比如《紐約時報》《衛報》以及《世界報》都訂閱了新聞,他們希望當接收的新聞中包含他們感興趣的關鍵字時,能得到特別通知。

ObserverMain

觀察者

interface Observer {void notify(String tweet); }class NYTimes implements Observer{public void notify(String tweet) {if(tweet != null && tweet.contains("money")){System.out.println("Breaking news in NY! " + tweet);}} } class Guardian implements Observer{public void notify(String tweet) {if(tweet != null && tweet.contains("queen")){System.out.println("Yet another news in London... " + tweet);}} } class LeMonde implements Observer{public void notify(String tweet) {if(tweet != null && tweet.contains("wine")){System.out.println("Today cheese, wine and news! " + tweet);}} }

主題

interface Subject{void registerObserver(Observer o);void notifyObservers(String tweet); }class Feed implements Subject{private final List<Observer> observers = new ArrayList<>();public void registerObserver(Observer o) {this.observers.add(o);}public void notifyObservers(String tweet) {observers.forEach(o -> o.notify(tweet));} }Feed f = new Feed(); f.registerObserver(new NYTimes()); f.registerObserver(new Guardian()); f.registerObserver(new LeMonde()); f.notifyObservers("The queen said her favourite book is Java 8 in Action!");

使用Lambda表達式3

f.registerObserver((String tweet) -> {if(tweet != null && tweet.contains("money")){System.out.println("Breaking news in NY! " + tweet);} });f.registerObserver((String tweet) -> {if(tweet != null && tweet.contains("queen")){System.out.println("Yet another news in London... " + tweet);} });

是否我們隨時隨地都可以使用Lambda表達式呢?答案是否定的!

Lambda適配得很好,那是因為需要執行的動作都很簡單,因此才能很方便地消除僵化代碼。但是,觀察者的邏輯有可能十分復雜,它們可能還持有狀態,抑或定義了多個方法,諸如此類。在這些情形下,你還是應該繼續使用類的方式。

責任鏈模式

ChainOfResponsibilityMain

責任鏈模式是一種創建處理對象序列(比如操作序列)的通用方案。一個處理對象可能需要在完成一些工作之后,將結果傳遞給另一個對象,這個對象接著做一些工作,再轉交給下一個處理對象,以此類推。

通常,這種模式是通過定義一個代表處理對象的抽象類來實現的,在抽象類中會定義一個字段來記錄后續對象。一旦對象完成它的工作,處理對象就會將它的工作轉交給它的后繼。

public abstract class ProcessingObject<T> {protected ProcessingObject<T> successor;public void setSuccessor(ProcessingObject<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);} }

可以創建兩個處理對象,它們的功能是進行一些文
本處理工作。

public class HeaderTextProcessing extends ProcessingObject<String> {public String handleWork(String text){return "From Raoul, Mario and Alan: " + text;} } public class SpellCheckerProcessing extends ProcessingObject<String> {public String handleWork(String text){return text.replaceAll("labda", "lambda");} }

現在你就可以將這兩個處理對象結合起來,構造一個操作序列!

ProcessingObject<String> p1 = new HeaderTextProcessing(); ProcessingObject<String> p2 = new SpellCheckerProcessing(); p1.setSuccessor(p2); String result = p1.handle("Aren't labdas really sexy?!!"); System.out.println(result);

使用Lambda表達式4

這個模式看起來像是在鏈接(也即是構造)函數,你需要使用andThen方法對其進行構造。

UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text; UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing); String result2 = pipeline.apply("Aren't labdas really sexy?!!");

工廠模式

FactoryMain

使用工廠模式,你無需向客戶暴露實例化的邏輯就能完成對象的創建。比如,我們假定你為一家銀行工作,他們需要一種方式創建不同的金融產品:貸款、期權、股票,等等。

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: throw new RuntimeException("No such product " + name);}}}

這里貸款(Loan)、股票(Stock)和債券(Bond)都是產品(Product)的子類。createProduct方法可以通過附加的邏輯來設置每個創建的產品。但是帶來的好處也顯而易見,你在創建對象時不用再擔心會將構造函數或者配置暴露給客戶,這使得客戶創建產品時更加簡單:

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

使用Lambda表達式5

Supplier<Product> loanSupplier = Loan::new; Loan loan = loanSupplier.get();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 static Product createProduct(String name){Supplier<Product> p = map.get(name);if(p != null) return p.get();throw new IllegalArgumentException("No such product " + name); }

這是個全新的嘗試,它使用Java 8中的新特性達到了傳統工廠模式同樣的效果。但是,如果工廠方法createProduct需要接收多個傳遞給產品構造方法的參數,這種方式的擴展性不是很好。你不得不提供不同的函數接口,無法采用之前統一使用一個簡單接口的方式。

比如,我們假設你希望保存具有三個參數(兩個參數為Integer類型,一個參數為String類型)的構造函數;為了完成這個任務,你需要創建一個特殊的函數接口TriFunction。最終的結果是Map變得更加復雜。

public interface TriFunction<T, U, V, R>{R apply(T t, U u, V v); }Map<String, TriFunction<Integer, Integer, String, Product>> map = new HashMap<>();

測試Lambda表達式

測試可見Lambda 函數的行為

由于moveRightBy方法聲明為public,測試工作變得相對容易。你可以在用例內部完成測試。但是Lambda并無函數名(畢竟它們都是匿名函數),因此要對你代碼中的Lambda函數進行測試實際上比較困難,因為你無法通過函數名的方式調用它們。

有些時候,你可以借助某個字段訪問Lambda函數,這種情況,你可以利用這些字段,通過它們對封裝在Lambda函數內的邏輯進行測試。比如,我們假設你在Point類中添加了靜態字段compareByXAndThenY,通過該字段,使用方法引用你可以訪問Comparator對象:

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

Lambda表達式會生成函數接口的一個實例。由此,你可以測試該實例的行為。這個例子中,我們可以使用不同的參數,對Comparator對象類型實例compareByXAndThenY的compare方法進行調用,驗證它們的行為是否符合預期:

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

測試使用Lambda 的方法的行為

我們需要對使用Lambda表達式的方法進行測試。比如下面這個方法moveAllPointsRightBy:

public static List<Point> moveAllPointsRightBy(List<Point> points, int x){return points.stream().map(p -> new Point(p.getX() + x, p.getY())).collect(toList()); }

沒必要對Lambda表達式p -> new Point(p.getX() + x,p.getY())進行測試,它只是moveAllPointsRightBy內部的實現細節。我們更應該關注的是方法moveAllPointsRightBy的行為

@Test public void testMoveAllPointsRightBy() throws Exception{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); }

將復雜的Lambda 表達式分到不同的方法

可能你會碰到非常復雜的Lambda表達式,包含大量的業務邏輯,比如需要處理復雜情況的定價算法。你無法在測試程序中引用Lambda表達式如果一個方法接受Lambda表達式作為參數,你可以采用的一個方案是使用不同的Lambda表達式對它進行測試。

調試

調試有問題的代碼時,程序員的兵器庫里有兩大老式武器,分別是:

  • 查看棧跟蹤
  • 輸出日志

查看棧跟蹤

Debugging

你的程序突然停止運行(比如突然拋出一個異常),這時你首先要調查程序在什么地方發生了異常以及為什么會發生該異常。這時棧幀就非常有用。程序的每次方法調用都會產生相應的調用信息,包括程序中方法調用的位置、該方法調用使用的參數、被調用方法的本地變量。這些信息被保存在棧幀上。

程序失敗時,你會得到它的棧跟蹤,通過一個又一個棧幀,你可以了解程序失敗時的概略信息。換句話說,通過這些你能得到程序失敗時的方法調用列表。這些方法調用列表最終會幫助你發現問題出現的原因。


不幸的是,由于Lambda表達式沒有名字,它的棧跟蹤可能很難分析。在下面這段簡單的代碼中,我們刻意地引入了一些錯誤:

import java.util.*; 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);} }

運行這段代碼會產生下面的棧跟蹤:

Exception in thread "main" java.lang.NullPointerExceptionat Debugging.lambda$main$0(Debugging.java:6)at Debugging$$Lambda$5/284720968.apply(Unknown Source)at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) …

這段程序當然會失敗,因為Points列表的第二個元素是空(null)。這時你的程序實際是在試圖處理一個空引用。由于Stream流水線發生了錯誤,構成Stream流水線的整個方法調用序列都暴露在你面前了。

不過,你留意到了嗎?棧跟蹤中還包含下面這樣類似加密的內容:

at Debugging.lambda$main$0(Debugging.java:6) at Debugging$$Lambda$5/284720968.apply(Unknown Source)

這些表示錯誤發生在Lambda表達式內部。由于Lambda表達式沒有名字,所以編譯器只能為它們指定一個名字。這個例子中,它的名字是lambda$main$0,看起來非常不直觀。如果你使用了大量的類,其中又包含多個Lambda表達式,這就成了一個非常頭痛的問題。


即使你使用了方法引用,還是有可能出現棧無法顯示你使用的方法名的情況。將之前的Lambda表達式p-> p.getX()替換為方法引用reference Point::getX也會產生難于分析的棧跟蹤:

points.stream().map(Point::getX).forEach(System.out::println);Exception in thread "main" java.lang.NullPointerExceptionat Debugging$$Lambda$5/284720968.apply(Unknown Source)at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)…

注意,如果方法引用指向的是同一個類中聲明的方法,那么它的名稱是可以在棧跟蹤中顯示的。比如,下面這個例子:

import java.util.*; public class Debugging{public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3);numbers.stream().map(Debugging::divideByZero).forEach(System.out::println);}public static int divideByZero(int n){return n / 0;} }

方法divideByZero在棧跟蹤中就正確地顯示了:

Exception in thread "main" java.lang.ArithmeticException: / by zeroat Debugging.divideByZero(Debugging.java:10)at Debugging$$Lambda$1/999966131.apply(Unknown Source)at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) …

總的來說,我們需要特別注意,涉及Lambda表達式的棧跟蹤可能非常難理解。這是Java編譯器未來版本可以改進的一個方面。

使用日志調試

Peek

使用forEach將流操作的結果日志輸出到屏幕上或者記錄到日志文件中:

List<Integer> numbers = Arrays.asList(2, 3, 4, 5); numbers.stream() .map(x -> x + 17) .filter(x -> x % 2 == 0) .limit(3) .forEach(System.out::println);

這段代碼的輸出如下:

20 22

不幸的是,一旦調用forEach,整個流就會恢復運行。到底哪種方式能更有效地幫助我們理解Stream流水線中的每個操作(比如map、filter、limit)產生的輸出?

peek的設計初衷就是在流的每個元素恢復運行之前,插入執行一個動作。但是它不像forEach那樣恢復整個流的運行,而是在一個元素上完成操作之后,它只會將操作順承到流水線中的下一個操作。

List<Integer> result = numbers.stream().peek(x -> System.out.println("from stream: " + x)).map(x -> x + 17).peek(x -> System.out.println("after map: " + x)).filter(x -> x % 2 == 0).peek(x -> System.out.println("after filter: " + x)).limit(3).peek(x -> System.out.println("after limit: " + x)).collect(toList());

通過peek操作我們能清楚地了解流水線操作中每一步的輸出結果:

taking from stream: 2 after map: 19 taking from stream: 3 after map: 20 after filter: 20 after limit: 20 taking from stream: 4 after map: 21 taking from stream: 5 after map: 22 after filter: 22 after limit: 22

小結

  • Lambda表達式能提升代碼的可讀性和靈活性。
  • 如果你的代碼中使用了匿名類,盡量用Lambda表達式替換它們,但是要注意二者間語義的微妙差別,比如關鍵字this,以及變量隱藏。
  • 跟Lambda表達式比起來,方法引用的可讀性更好 。
  • 盡量使用Stream API替換迭代式的集合處理。
  • Lambda表達式有助于避免使用面向對象設計模式時容易出現的僵化的模板代碼,典型的比如策略模式、模板方法、觀察者模式、責任鏈模式,以及工廠模式。
  • 即使采用了Lambda表達式,也同樣可以進行單元測試,但是通常你應該關注使用了Lambda表達式的方法的行為。
  • 盡量將復雜的Lambda表達式抽象到普通方法中。
  • Lambda表達式會讓棧跟蹤的分析變得更為復雜。
  • 流提供的peek方法在分析Stream流水線時,能將中間變量的值輸出到日志中,是非常有用的工具。

總結

以上是生活随笔為你收集整理的《Java8实战》笔记(08):重构、测试和调试的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。