java8 函數式編程讀書筆記

1.2 什麼是函數式編程

每個人對函數式編程的理解不盡相同。但其核心是:在思考問題時,使用不可變值和函 數,函數對一個值進行處理,映射成另一個值。

第二章 Lambda表達式

2.2 辨別Lambda表達式

Runnable noArguments = () -> System.out.println("Hello World”); 

ActionListener oneArgument = event -> System.out.println("button clicked”); 

Runnable multiStatement = () -> { 
          System.out.print("Hello");
          System.out.println(" World");
 };

BinaryOperator<Long> add = (x, y) -> x + y; 

BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y; 

上述例子還隱含了另外一層意思:Lambda 表達式的類型依賴於上下文環境,是由編譯器 推斷出來的。

2.3 引用值,而不是變量

final String name = getUserName(); button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) { 
    System.out.println("hi " + name);
	} 
});    

Java 8雖然放鬆了這一限制,可以引用非final變量,但是該變量在既成事實上必須是 final

//這裏編譯不會通過
	 String name = getUserName();
     name = formatUserName(name);
     button.addActionListener(event -> System.out.println("hi " + name));

這種行爲也解釋了爲什麼 Lambda 表達式也被稱爲閉包。未賦值的變量與周邊環境隔離起 來,進而被綁定到一個特定的值
總而言之, lambda 引用的是值 而非變量

2.4 函數接口

使用只有一個方法的接口來表示某特定方法並反覆使用,是很早就有的習慣。使用 Swing 編寫過用戶界面的人對這種方式都不陌生,這裏無需再標新立 異,Lambda 表達式也使用同樣的技巧,並將這種接口稱爲函數接口

2.5 類型推斷

javac 根據 Lambda 表達式上下文信息 就能推斷出參數的正確類型。程序依然要經過類型檢查來保證運行的安全性,但不用再顯 式聲明類型罷了。這就是所謂的類型推斷。

    BinaryOperator<Long> add = (x, y) -> x + y; 
    BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

2.6 要點回顧

  • Lambda 表達式是一個匿名方法,將行爲像數據一樣進行傳遞。
  • Lambda 表達式的常見結構:BinaryOperator add = (x, y) → x + y。
  • 函數接口指僅具有單個抽象方法的接口,用來表示Lambda表達式的類型。

3.1 從外部迭代到內部迭代
在這裏插入圖片描述

在這裏插入圖片描述

3.2 實現機制

1、惰性求值法
如下,不會出現打印結果

allArtists.stream()
               .filter(artist -> {
                   System.out.println(artist.getName());
					return artist.isFrom("London"); 
					}
				);

2、及早求值法
如下,會打印結果

long count = allArtists.stream()
                            .filter(artist -> {
                                System.out.println(artist.getName());
								return artist.isFrom("London"); 
							}).count();

使用這些操作的理 想方式就是形成一個惰性求值的鏈,最後用一個及早求值的操作返回想要的結果,這正是 它的合理之處。計數的示例也是這樣運行的

3.3 常用的流操作

3.3.1 collect(toList())

List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList()); 
assertEquals(Arrays.asList("a", "b", "c"), collected); 

這個例子也展示了本節中所有示例代碼的通用格式。首先由列表生成一個 Stream ,然後 進行一些 Stream 上的操作,繼而是 collect 操作,由 Stream 生成列表,最後使用斷言 判斷結果是否和預期一致。

3.3.2 map
在這裏插入圖片描述

使用普通的方式將數組中的數據改成大寫

List<String> collected = new ArrayList<>();
for (String string : asList("a", "b", "hello")) {
          String uppercaseString = string.toUpperCase();
          collected.add(uppercaseString);
      }
assertEquals(asList("A", "B", "HELLO"), collected);

使用stream.map

List<String> collected = Stream.of("a", "b", "hello").map(string -> string.toUpperCase()).collect(toList());
 assertEquals(asList("A", "B", "HELLO"), collected);

看源碼map的傳參是function,正好適用將一個值變爲另外一個值的場景
在這裏插入圖片描述

3.3.3 filter
在這裏插入圖片描述
filter的傳參是Predicate接口,傳入一個值,返回一個boolean判斷,適用於篩選的場景
在這裏插入圖片描述

3.3.4 flatmap

flatMap 方法可用 Stream 替換值,然後將多個 Stream 連接成一個 Stream

List list =  Stream.of(Arrays.asList("a,b,c"),Arrays.asList("d,e,f")).flatMap(strings -> {
    return strings.stream().map(string-> string.toUpperCase());
}).collect(Collectors.toList());
System.out.println(list);

flatMap 方法的相關函數接口和 map 方法的一樣,都是 Function 接口,只是方法的返回值 限定爲 Stream 類型罷了。

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

3.3.5 max和min

List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
                                 .min(Comparator.comparing(track -> track.getLength()))
                                 .get();
assertEquals(tracks.get(1), shortestTrack);

這裏的stream調用 max 和 min 會獲得一個 Optional對象,調用get纔會獲得具體的值
comparing的源碼,最終返回的是一個Comparator函數

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

3.3.7 reduce

使用 reduce 求和

int count = Stream.of(1, 2, 3)
                       .reduce(0, (acc, element) -> acc + element);
assertEquals(6, count);

3.4 重構遺留代碼

/**
 * 唱片篩選(參考函數式編程 3。4 重構遺留代碼)
 */
public class AlbumScreen {

    /**
     * 專輯
     */
    static class Album{

        public Album(String albumName, List<Track> trackList) {
            this.albumName = albumName;
            this.trackList = trackList;
        }

        //專輯名稱
        private String albumName;

        //曲目列表
        private List<Track> trackList;

        public String getAlbumName() {
            return albumName;
        }

        public void setAlbumName(String albumName) {
            this.albumName = albumName;
        }

        public List<Track> getTrackList() {
            return trackList;
        }

        public void setTrackList(List<Track> trackList) {
            this.trackList = trackList;
        }
    }

    /**
     * 曲目
     */
    static class Track{

        public Track(String trackName,Long seconds) {
            this.trackName = trackName;
            this.seconds = seconds;
        }

        //曲目名稱
        private String trackName;

        //時長 秒
        private Long seconds;

        public Long getSeconds() {
            return seconds;
        }

        public void setSeconds(Long seconds) {
            this.seconds = seconds;
        }

        public String getTrackName() {
            return trackName;
        }

        public void setTrackName(String trackName) {
            this.trackName = trackName;
        }

    }

    /**
     * 篩選時長大於60秒以上的曲目
     * @param albumList
     * @return
     */
    public static Set<String> screen(List<Album> albumList){
        Set<String> trackNames = new HashSet<>();
        for(Album album : albumList) {
            for (Track track : album.getTrackList()) {
                if (track.seconds > 60) {
                    String name = track.getTrackName();
                    trackNames.add(name);
                }
            }
        }
        return trackNames;
    }

    /**
     * 篩選時長大於60秒以上的曲目(lambda版)
     * @param albumList
     * @return
     */
    public static Set<String> screenLambda(List<Album> albumList){
        return albumList.stream().flatMap(tracks -> tracks.trackList.stream()) //flatmap 將多個流合併成一個
                .filter(track -> track.seconds>60)  //filter 進行篩選
                .map(track -> track.trackName)    //map 通過一個值獲取另外一個值,這裏根據曲目對象獲取名稱
                .collect(Collectors.toSet());  //創建 set
    }

    public static void main(String[] args){
        List<Track> trackList1 =
                Stream.of(new Track("燃燒我的卡路里",180L)
                        ,new Track("我已經愛上你",59L)
                        ,new Track("好漢歌",100L))
                        .collect(Collectors.toList());
        List<Track> trackList2 =
                Stream.of(new Track("一百萬個可能",90L)
                        ,new Track("答案",30L)
                        ,new Track("一個人去巴黎",120L))
                        .collect(Collectors.toList());

        List<Album> albumList =
                Arrays.asList(new Album("火箭隊",trackList1),new Album("銀河隊",trackList2));

        System.out.println(screen(albumList));
        System.out.println(screenLambda(albumList));
    }

}

3.8 要點回顧

內部迭代將更多控制權交給了集合類。
和Iterator類似,Stream是一種內部迭代方式。
將Lambda表達式和Stream上的方法結合起來,可以完成很多常見的集合操作。

4 類庫

4.1 在代碼中使用lambda表達式

使用 isDebugEnabled 方法降低日誌性能開銷

Logger logger = new Logger(); if (logger.isDebugEnabled()) {
         logger.debug("Look at this: " + expensiveOperation());
 }

//使用lambda表達式簡化日誌
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());

public void debug(Supplier<String> message) { if (isDebugEnabled()) {
             debug(message.get());
         }
}

4.2 基本類型

由於裝箱類型是對象,因此在內存中存在額外開銷。比如,整型在內存中佔用 4 字節,整型對象卻要佔用 16 字節。這一情況在數組上更加嚴重,整型數組中的每個元素 只佔用基本類型的內存,而整型對象數組中,每個元素都是內存中的一個指針,指向 Java 堆中的某個對象。在最壞的情況下,同樣大小的數組,Integer[] 要比 int[] 多佔用 6 倍 內存。

爲了減小這些性能開銷,Stream 類的某些方法對基本類型和裝箱類型做了區分。圖 4-1 所 示的高階函數mapToLong和其他類似函數即爲該方面的一個嘗試

    //Stream 中的源碼
    LongStream mapToLong(ToLongFunction<? super T> mapper);

在這裏插入圖片描述

//LongStream 中的源碼
    <U> Stream<U> mapToObj(LongFunction<? extends U> mapper);

在這裏插入圖片描述

4.3 重載解析

Lambda 表達式作爲參數時,其類型由它的目標類型推導得出,推導過程遵循 如下規則:

􏰂 如果只有一個可能的目標類型,由相應函數接口裏的參數類型推導得出;
􏰂 如果有多個可能的目標類型,由最具體的類型推導得出;
􏰂 如果有多個可能的目標類型且最具體的類型不明確,則需人爲指定類型。

4.4 @FunctionalInterface

該註釋會強制 javac 檢查一個接口是否符合函數接口的標準。如果該註釋添加給一個枚舉 類型、類或另一個註釋,或者接口包含不止一個抽象方法,javac 就會報錯。重構代碼時, 使用它能很容易發現問題。

4.6 默認方法

因爲接口的改造,接口方法的增加,會導致用舊的jdk編譯的類有不兼容的問題,所以採用了default關鍵字,接口提供一個默認的實現方法

默認方法示例:forEach 實現方式

 default void forEach(Consumer<? super T> action) { 
              for(Tt:this){
                  action.accept(t);
              }
    }

和類不同,接口沒有成員變量,因此默認方法只能通過調用子類的方法來修改子類本身, 避免了對子類的實現做出各種假設。

4.7 多重繼承

public interface Jukebox {
        public default String rock() { return "... all over the world!";
        } 
    }
    
    public interface Carriage {
        public default String rock() { return "... from side to side";
        } 
    }
    
    
    public class MusicalCarriage
    implements Carriage, Jukebox {
    
        @Override
            public String rock() {
                   return Carriage.super.rock();
             }
    }

javac 並不明確應該繼承哪個接口中的方法,因此編譯器會報錯:class Musical Carriage inherits unrelated defaults for rock() from types Carriage and Jukebox。當然,在類 中實現 rock 方法就能解決這個問題

三定律
如果對默認方法的工作原理,特別是在多重繼承下的行爲還沒有把握,如下三條簡單的定 律可以幫助大家。

  1. 類勝於接口。如果在繼承鏈中有方法體或抽象的方法聲明,那麼就可以忽略接口中定義 的方法。
  2. 子類勝於父類。如果一個接口繼承了另一個接口,且兩個接口都定義了一個默認方法, 那麼子類中定義的方法勝出。
  3. 沒有規則三。如果上面兩條規則不適用,子類要麼需要實現該方法,要麼將該方法聲明 爲抽象方法。
    其中第一條規則是爲了讓代碼向後兼容。

4.9 接口的靜態方法

Stream 是個接口, Stream.of是接口的靜態方法。這也是Java 8中添加的一個新的語言特性,旨在幫助編寫 類庫的開發人員,但對於日常應用程序的開發人員也同樣適用。

4.10 Optional

Optional 是爲核心類庫新設計的一個數據類型,用來替換 null 值。
使用 Optional 對象有兩個目的:首先,Optional 對象鼓勵程序員適時檢查 變量是否爲空,以避免代碼缺陷;其次,它將一個類的 API 中可能爲空的值文檔化,這比 閱讀實現代碼要簡單很多。

 //創建某個值的 Optional 對象
    Optional<String> a = Optional.of("a");
         assertEquals("a", a.get());
    
    //創建一個空的 Optional 對象,並檢查其是否有值
    Optional emptyOptional = Optional.empty();
         Optional alsoEmpty = Optional.ofNullable(null);
         assertFalse(emptyOptional.isPresent());
    
    //使用 orElse 和 orElseGet 方法
    assertEquals("b", emptyOptional.orElse("b"));
    assertEquals("c", emptyOptional.orElseGet(() -> "c"));

4.11 要點回顧
􏰂 使用爲基本類型定製的Lambda表達式和Stream,如IntStream可以顯著提升系統性能。
􏰂 默認方法是指接口中定義的包含方法體的方法,方法名有default關鍵字做前綴。
􏰂 在一個值可能爲空的建模情況下,使用Optional對象能替代使用null值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章