1. 程式人生 > 其它 >《Java8 函數語言程式設計》第二章、第三章 讀書筆記

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