《Java8 函數語言程式設計》第二章、第三章 讀書筆記
一、簡介
什麼是函數語言程式設計,其核心是:使用不可變值和函式,函式對一個值進行處理,對映成另外一個值。
二、Lambda表示式
2.1 Lambda的引入
舉例:使用內部類,點選按鈕之後做出反應。採用匿名內部類實現
1 button.addActionListener(new ActionListener() { 2 public void acionPerformed(ActionEvent event) { 3 System.out.println("button clicked"); 4 } 5 }
程式碼即資料,將程式碼作為資料傳遞,給按鈕傳遞了一個代表某種行為的物件。
如果使用Lambda表示式來改寫相同的功能,可以如下:
1 button.addActionListener(event -> System.out.println("button clicked"));
javac可以根據上下文(addActionListener的簽名)推匯出event的型別。
2.2 Lambda表示式
常見的Lambda表示式
1 Runnable noArguments = () -> System.out.println("Hello World"); //實現了Runnable介面,引數為空,返回型別是void 2 ActionListener oneArguments = event -> System.out.println("button clicked"); //一個引數 3 Runnable multiStatement = () -> { 4 System.out.println("Hello"); 5 System.out.println("world"); 6 }; //表示式多行,用大括號包裹。 7 BinaryOperator<Long> add = (x, y) -> x + y; //返回的是一個函式,該函式的有兩個引數,返回Long型別,而不是引數相加的結果。 8 BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y; //同上,引數型別可以由編譯器推斷出來,也可以顯式宣告。型別推斷依賴上下文環境。
2.3 引用值,不是變數
匿名內部類內部使用的變數,需要宣告為final
1 final String name = getUserName(); //name必須不能為其重複賦值。 2 button.addActionListener(new ActionListener() { 3 public void actionPerformed(ActionEvent event) { 4 System.out.println("hi" + name); 5 } 6 });
其中,Lambda也有同樣的要求
1 String name = getUserName(); //雖然Java8不必用final修飾,但是該變數在既成事實上必須是final,不能重複賦值
//name = formatUserNam(name); 如果加上這一句,編譯器會報錯。 2 button.addActionListener(event -> System.out.println("hi" + name);
2.4 函式介面
Lambda表示式本身的型別:函式介面。函式介面是隻有一個抽象方法的介面。
1 // ActionListener介面:接受ActionEvent型別的引數,返回空 2 public interface ActionListener extends EventListener { 3 public void actionPerformed(ActionEvent event); 4 }
介面中單一方法的命名並不重要,只要方法簽名和Lambda表示式的型別匹配即可。
Java中重要的函式介面
介面 | 引數 | 返回型別 | 示例 |
Predicate<T> | T | boolean | Predicate<String> p1 = str -> str.length() < 5; |
Consumer<T> | T | void |
1 // 使用Consumer實現介面 2 Consumer<String> consumer = new Consumer<String>() { 3 @Override 4 public void accept(String s) { 5 System.out.println(s); 6 } 7 }; // 1 8 Consumer<String> consumer2 = (s) -> System.out.println(s); // 2 9 Consumer consumer3 = System.out::println; // 3 10 Stream<String> stream = Stream.of("aaa", "bbb", "ccc"); 11 stream.forEach(consumer); // 1 12 stream.forEach(consumer2); // 2 13 stream.forEach(consumer3); // 3 |
Function<T, R> | T | R |
1 @FunctionalInterface 2 public interface Function<T, R> { 3 R apply(T t); // 輸入引數為T,輸出型別為R 4 default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { 5 Objects.requireNonNull(before); 6 return (V v) -> apply(before.apply(v)); 7 } // V -> T, T -> R 8 9 default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { 10 Objects.requireNonNull(after); 11 return (T t) -> after.apply(apply(t)); 12 } // T -> R, R -> V 13 14 static <T> Function<T, T> identity() { 15 return t -> t; 16 } |
Supplier<T> | None | T |
1 // There is no requirement that a new or distinct result be returned each time the supplier is invoked. 2 @FunctionalInterface 3 public interface Supplier<T> { 4 T get(); 5 } 6 Supplier<String> supplier = () -> "Hello World!"; |
UnaryOperator<T> | T | T | 單引數函式 |
BinaryOperator<T> | (T, T) | T |
2.5 型別推斷
1 Map<String, Integer> oldWordCounts = new HashMap<String, Integer>(); 2 Map<String, Integer> diamondWordCounts = new HashMap<>(); //可以自動推斷。 3 private void useHashmap(Map<String, String> values); 4 useHashMap(new HashMap<>()); // 根據方法簽名做推斷 5 6 BinaryOperator add = (x, y) -> x + y; // 沒有給出變數的任何泛型資訊,無法推斷出型別。
三、流
3.1 從內部迭代到外部迭代
從簡單的例子入手,如果要統計從倫敦來的藝術家人數,通常程式碼會 如下所示:
1 int count = 0; 2 for (Artist artist : allArtists) { 3 if (artist.isFrom("London")) { 4 count++; 5 } 6 }
這段程式碼的背後是封裝了迭代器,可以寫成這樣
1 int count = 0; 2 Iterator<Artist> iterator = allArtistes.iterator(); 3 while(iterator.hasNext()) { 4 Artist artist = iterator.next(); 5 if (artist.isFrom("London")) { 6 count++; 7 } 8 }
以上程式碼在指令式程式設計中很常見,對於問題,這段程式碼演示瞭如何做而不是做什麼。在本書中作者稱為外部迭代
在Java8中可採用Stream
1 long count = allArtists.stream().filter(artist -> artist.isFrom("London")).count();
這段程式碼很明顯的表達出意圖:過濾來自倫敦的藝術家並計數,這種方法被成為內部迭代
Stream是用函數語言程式設計方式在集合類上進行復雜操作的工具。
3.2 實現機制
1 allArtists.stream().filter(artist -> artist.isFrom("London")); //惰性求值,只是刻畫出stream,並沒有產生新的集合
而count()屬於及早求值,會執行響應的動作。
判斷惰性求值還是及早求值:只需要看它的返回值。
如果返回值是Stream,那麼就是惰性求值;如果返回值是另一個值或為空,那麼就是及早求值。區分惰性和及早求值的目的是更有效率的計算。
3.3 常用的流操作
3.4 重構遺留程式碼
1 //找出長度大於1分鐘的曲目 2 public Set<String> findLongTracks(List<Album> albums) { 3 Set<String> trackNames = new HashSet<>(); 4 for (Album album : albums) { 5 for (Track track : album.getTrackList()) { 6 if (track.getLength() > 60) { 7 String name = track.getName(); 8 trackNames.add(name); 9 } 10 } 11 } 12 return trackNames; 13 }
這段程式碼是多重迴圈,從專輯列表裡取出每個專輯進行處理,從每個專輯裡取出每個曲目進行判斷。
其中有多個專輯到單個曲目的轉換,從上一節學習的流操作來看,可以用flatMap處理,把多個流轉換成單個流,用filter進行過濾,用map進行track到name的轉換。
1 albums.stream().flatMap(album -> album.getTracks()).filter(track -> track.getLength() > 60).map(track -> track.getName()).collect(toSet());