《java 8 實戰》讀書筆記 -第八章 重構、測試和調試

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

1.改善代碼的可讀性

Java 8的新特性也能夠幫助提高代碼的可讀性:java

  • 使用Java 8,你能夠減小冗長的代碼,讓代碼更易於理解
  • 經過方法引用和Stream API,你的代碼會變得更直觀

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

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

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

  • 在匿名類中,this表明的是類自身,可是在Lambda中,它表明的是包含類。其次,匿名類能夠屏蔽包含類的變量,而Lambda表達式不
    能(它們會致使編譯錯誤),譬以下面這段代碼:設計模式

    int a = 10; 
    Runnable r1 = () -> { 
    int a = 2; //類中已包含變量a
    System.out.println(a); 
    };
  • 對於參數相同的函數式接口,調用時會形成都符合Lambda表達式的結果,不過NetBeans和IntelliJ都支持這種重構,它們能自動地幫你檢查,避免發生這些問題。

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

按照食物的熱量級別對菜餚進行分類:app

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
 menu.stream() 
 .collect( 
 groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.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));
  • 使用Collectors接口能夠輕鬆獲得和或者最大值,與採用Lambada表達式和底層的歸約操做比起來,這種方式要直觀得多.
int totalCalories = 
  menu.stream().map(Dish::getCalories) 
  .reduce(0, (c1, c2) -> c1 + c2);

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

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

原來:測試

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

替換成流式:this

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

5.增長代碼的靈活性

(1)採用函數接口

用Lambda表達式帶來的靈活性,它們分別是:有條件的延遲執行和環繞執行。設計

(2)有條件的延遲執行

若是你發現你須要頻繁地從客戶端代碼去查詢一個對象的狀態,只是爲了傳遞參數、調用該對象的一個方法(好比輸出一條日誌),那麼能夠考慮實現一個新的方法,以Lambda或者方法表達式做爲參數,新方法在檢查完該對象的狀態以後才調用原來的方法。調試

(3)環繞執行

若是你發現雖然你的業務代碼千差萬別,可是它們擁有一樣的準備和清理階段,這時,你徹底能夠將這部分代碼用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); 
 } 
} 
public interface BufferedReaderProcessor{ 
 String process(BufferedReader b) throws IOException; 
}

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

1.策略模式

策略模式表明瞭解決一類算法的通用解決方案,你能夠在運行時選擇使用哪一種方案。
數字)。你能夠從定義一個驗證文本(以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); 
  } 
} 
Validator numericValidator = new Validator(new IsNumeric()); 
boolean b1 = numericValidator.validate("aaaa"); 
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ()); 
boolean b2 = lowerCaseValidator.validate("bbbb");

若是使用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");

2.模板方法

若是你須要採用某個算法的框架,同時又但願有必定的靈活度,能對它的某些部分進行改進,那麼採用模板方法設計模式是比較通用的方案。

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

processCustomer方法搭建了在線銀行算法的框架:獲取客戶提供的ID,而後提供服務讓用戶滿意。不一樣的支行能夠經過繼承OnlineBanking類,對該方法提供差別化的實現。
若是使用Lambda表達式:

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());

3.觀察者模式

例子:好幾家報紙機構,好比《紐約時報》《衛報》以及《世界報》都訂閱了新聞,他們但願當接收的新聞中包含他們感興趣的關鍵字時,能獲得特別通知。

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表達式後,你無需顯式地實例化三個觀察者對象,直接傳遞Lambda表達式表示須要執行的行爲便可:

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

4.責任鏈模式

責任鏈模式是一種建立處理對象序列(好比操做序列)的通用方案。一個處理對象可能須要在完成一些工做以後,將結果傳遞給另外一個對象,這個對象接着作一些工做,再轉交給下一個處理對象,以此類推。一般,這種模式是經過定義一個表明處理對象的抽象類來實現的,在抽象類中會定義一個字段來記錄後續對象。一旦對象完成它的工做,處理對象就會將它的工做轉交給它的後繼。

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表達式
你能夠將處理對象做爲函數的一個實例,或者更確切地說做爲UnaryOperator<String>的一個實例。爲了連接這些函數,你須要使用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 result = pipeline.apply("Aren't labdas really sexy?!!");

5.工廠模式

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

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

使用Lambda表達式
第3章中,咱們已經知道能夠像引用方法同樣引用構造函數。好比,下面就是一個引用貸款
(Loan)構造函數的示例:

構造器參數列表要與接口中抽象方法的參數列表一致!所以,若是構造方法中有多個參數,須要自定義函數式接口。
Supplier<Product> loanSupplier = Loan::new; 
Loan loan = loanSupplier.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 static Product createProduct(String name){ 
 Supplier<Product> p = map.get(name); 
 if(p != null) return p.get(); 
 throw new IllegalArgumentException("No such product " + name); 
}

3、測試 Lambda 表達式

  • 你能夠藉助某個字段訪問Lambda函數
  • 要對使用Lambda表達式的方法進行測試
  • 一種策略是將Lambda表達式轉換爲方法引用,而後按照常規方式
  • 接受函數做爲參數的方法或者返回一個函數的方法(所謂的「高階函數」,higher-order function,咱們在第14章會深刻展開介紹)更難測試。若是一個方法接受Lambda表達式做爲參數,你能夠採用的一個方案是使用不一樣的Lambda表達式對它進行測試。

文中提到了List的equals方法
ArrayList、Vector二者都實現了List接口、繼承AbstractList抽象類,其equals方法是在AbstractList類中定義的,源代碼以下:

public boolean equals(Object o) {    
  if (o == this)        
     return true;    
  // 判斷是不是List列表,只要實現了List接口就是List列表
  if (!(o instanceof List))        
     return false;    
  // 遍歷list全部元素
  ListIterator<E> e1 = listIterator();
  ListIterator e2 = ((List) o).listIterator();    
  while (e1.hasNext() && e2.hasNext()) {
      E o1 = e1.next();
      Object o2 = e2.next();        
      // 有不相等的就退出
      if (!(o1==null ? o2==null : o1.equals(o2)))            
          return false;
   }    
   // 長度是否相等
   return !(e1.hasNext() || e2.hasNext());

從源碼能夠看出,equals方法並不關心List的具體實現類,只要是實現了List接口,而且全部元素相等、長度也相等的話就代表兩個List是相等的,因此例子中才會返回true。

4、調試

1.查看棧跟蹤

因爲Lambda表達式沒有名字,它的棧跟蹤可能很難分析,編譯器只能爲它們指定一個名字,若是你使用了大量的類,其中又包含多個Lambda表達式,這就成了一個很是頭痛的問題,這是Java編譯器將來版本能夠改進的一個方面。

2.使用日誌調試

這就是流操做方法peek大顯身手的時候。peek的設計初衷就是在流的每一個元素恢復運行以前,插入執行一個動做。可是它不像forEach那樣恢復整個流的運行,而是在一個元素上完成操做以後,它只會將操做順承到流水線中的下一個操做。

List<Integer> numbers = Arrays.asList(2, 3, 4, 5);

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)) 
//輸 出 map操做的結果
 .filter(x -> x % 2 == 0) 
 .peek(x -> System.out.println("after filter: " + x))
//輸出通過filter操做以後,剩下的元素個數
 .limit(3) 
 .peek(x -> System.out.println("after limit: " + x))
//輸出通過limit操做以後,剩下的元素個數
 .collect(toList());

輸出結果:

from stream: 2 
after map: 19 
from stream: 3 
after map: 20 
after filter: 20 
after limit: 20 
from stream: 4 
after map: 21 
from stream: 5 
after map: 22 
after filter: 22 
after limit: 22
相關文章
相關標籤/搜索