java8 函數式編程一

  一、函數接口
  
  二、Lambda 表達式
  
  三、默認方法
  
  四、其他
  
  回到頂部
  
  一、函數接口
  
  接口    參數    返回類型    描述
  
  Predicate<T>    T    boolean    用來比較操作
  
  Consumer<T>    T    void    沒有返回值的函數
  
  Function<T, R>    T    R    有返回值的函數
  
  Supplier<T>    None    T    工廠方法-返回一個對象
  
  UnaryOperator<T>    T    T    入參和出參都是相同對象的函數
  
  BinaryOperator<T>    (T,T)    T    求兩個對象的操作結果
  
  爲什麼要先從函數接口說起呢?因爲我覺得這是 java8 函數式編程的入口呀!每個函數接口都帶有 @FunctionalInterface 註釋,有且僅有一個未實現的方法,表示接收 Lambda 表達式,它們存在的意義在於將代碼塊作爲數據打包起來。
  
  沒有必要過分解讀這幾個函數接口,完全可以把它們看成普通的接口,不過他們有且僅有一個抽象方法(因爲要接收 Lambda 表達式啊)。
  
  @FunctionalInterface 該註釋會強制 javac 檢查一個接口是否符合函數接口的標準。 如果該註釋添加給一個枚舉類型、 類或另一個註釋, 或者接口包含不止一個抽象方法, javac 就會報錯。
  
  回到頂部
  
  二、Lambda 表達式
  
  1、Lambda 表達式和匿名內部類
  
  先來複習一下匿名內部類的知識:
  
  如果是接口,相當於在內部返回了一個接口的實現類,並且實現方式是在類的內部進行的;
  
  如果是普通類,匿名類相當於繼承了父類,是一個子類,並可以重寫父類的方法。
  
  需要特別注意的是,匿名類沒有名字,不能擁有一個構造器。如果想爲匿名類初始化,讓匿名類獲得一個初始化值,或者說,想使用匿名內部類外部的一個對象,則編譯器要求外部對象爲final屬性,否則在運行期間會報錯。
  
  new Thread(new Runnable() {
  
  @Override
  
  public void run() {
  
  System.out.println(123);
  
  }
  
  }).start();
  
  new Thread(()-> System.out.println(123)).start();
  
  如上,和傳入一個實現某接口的對象不同, 我們傳入了一段代碼塊 —— 一個沒有名字的函數。() 是參數列表, 和上面匿名內部類示例中的是一樣的。 -> 將參數和 Lambda 表達式的主體分開, 而主體是之後操作會運行的一些代碼。
  
  Lambda 表達式簡化了匿名內部類的寫法,省略了函數名和參數類型。即參數列表 () 中可以僅指定參數名而不指定參數類型。
  
  Java 是強類型語言,爲什麼可以不指定參數類型呢?這得益於 javac 的類型推斷機制,編譯器能夠根據上下文信息推斷出參數的類型,當然也有推斷失敗的時候,這時就需要手動指明參數類型了。javac 的類型推斷機制如下:
  
  對於類中有重載的方法,javac 在推斷類型時,會挑出最具體的類型。
  
  如果只有一個可能的目標類型, 由相應函數接口裏的參數類型推導得出;
  
  如果有多個可能的目標類型, 由最具體的類型推導得出;
  
  如果有多個可能的目標類型且最具體的類型不明確, 則需人爲指定類型。
  
  2、Lambda 表達式和集合
  
  java8 在 java.util 包中引入了一個新的類 —— Stream.java。java8 之前我們迭代集合,都只能依賴外部迭代器 Iterator 對集合進行串行化處理。而 Stream 支持對集合順序和並行聚合操作,將更多的控制權交給集合類,是一種內部迭代方式。這有利於方便用戶寫出更簡單的代碼,明確要達到什麼轉化,而不是如何轉化。
  
  Stream 的操作有兩種,一種是描述 Stream ,如 filter、map 等最終不產生結果的行爲稱爲"惰性求值";另外一種像 foreach、collect 等是從 Stream 中產生結果的行爲稱爲"及早求值"。
  
  接下來讓我們瞧瞧 Stream 如何結合 Lambda 表達式優雅的處理集合...
  
  foreach - 迭代集合
  
  list.forEach(e -> System.out.println(e));
  
  map.forEach((k, v) -> {
  
  System.out.println(k);
  
  System.out.println(v);
  
  });
  
  collect(toList()) - 由Stream裏的值生成一個列表。
  
  List<String> list = Stream.of("java", "C++", "Python").collect(Collectors.toList());
  
  等價於:
  
  List<String> asList = Arrays.asList("java", "C++", "Python");
  
  filter - 遍歷並檢查過濾其中的元素。
  
  long count = list.stream().filter(x -> "java".equals(x)).count();
  
  map、mapToInt、mapToLong、mapToDouble - 將流中的值轉換成一個新的值。
  
  List<String> mapList = list.stream().map(str -> str.toUpperCase()).collect(Collectors.toList());
  
  List<String> list = Stream.of("java", "javascript", "python").collect(Collectors.toList());
  
  IntSummaryStatistics intSummaryStatistics = list.stream().mapToInt(e -> e.length()).summaryStatistics();
  
  System.out.println("最大值:" + intSummaryStatistics.getMax());
  
  System.out.println("最小值:" + intSummaryStatistics.getMin());
  
  System.out.println("平均值:" + intSummaryStatistics.getAverage());
  
  System.out.println("總數:" + intSummaryStatistics.getSum());
  
  mapToInt、mapToLong、mapToDouble 和 map 操作類似,只是把函數接口的返回值改爲 int、long、double 而已。
  
  flatMap - 將多個 Stream 連接成一個 Stream
  
  List<String> streamList = Stream.of(list, asList).flatMap(x -> x.stream()).collect(Collectors.toList());
  
  flatMap 方法的相關函數接口和 map 方法的一樣, 都是 Function 接口, 只是方法的返回值限定爲 Stream 類型罷了。
  
  Max-求最大值、Min-求最小值
  
  String maxStr = list.stream(www.michenggw.com).max(Comparator.comparing(e -> e.length())).get();
  
  String minStr = list.stream(www.yigouyule2.cn ).min(Comparator.comparing(e -> e.length())).get();
  
  reduce - 聚合操作,從一組元素中生成一個值,sum()、max()、min()、count() 等都是reduce操作,將他們單獨設爲函數只是因爲常用。
  
  Integer sum1 = Stream.of(1, 2, 3).reduce(0, (acc, e) -> acc + e);
  
  上述執行求和操作,有兩個參數: 傳入 Stream 中初始值和 acc。 將兩個參數相加,acc 是累加器,保存着當前的累加結果。
  
  待續...
  
  回到頂部
  
  三、默認方法
  
  java8 中新增了 Stream 操作,那麼第三方類庫中的自定義集合 MyList 要怎麼做到兼容呢?總不能升級完 java8,第三方類庫中的集合實現全都不能用了吧?
  
  爲此,java8 在接口中引入了"默認方法"的概念!默認方法是指接口中定義的包含方法體的方法,方法名有 default 關鍵字做前綴。默認方法的出現是爲了 java8 能夠向後兼容。
  
  public interface Iterable<T> {
  
  /**
  
  * Performs the given action for each element of the {@code Iterable}
  
  * until all elements have been processed or the action throws an
  
  * exception.  Unless otherwise specified by the implementing class,
  
  * actions are performed in www.trgj888.com/  the order of iteration (if an iteration order
  
  * is specified).  Exceptions thrown by the action are relayed to the
  
  * caller.
  
  *
  
  * @implSpec
  
  * <p>The default implementation behaves as if:
  
  * <pre>{@code
  
  *     for (T t : this)
  
  *         action.accept(t);
  
  * }</pre>
  
  *
  
  * @param action The action to be performed for each element
  
  * @throws NullPointerException if the specified action is null
  
  * @since 1.8
  
  */
  
  default void forEach(Consumer<? super T> action) {
  
  Objects.requireNonNull(www.yongshiyule178.com  action);
  
  for (T t : this) {
  
  action.accept(t);
  
  }
  
  }
  
  }
  
  看 java8 中的這個 Iterable.java 中的默認方法 forEach(Consumer<? super T> action),表示“如果你們沒有實現 forEach 方法,就使用我的吧”。
  
  默認方法除了添加了一個新的關鍵字 default,在繼承規則上和普通方法也略有差別:
  
  類勝於接口。如果在繼承鏈中有方法體或抽象的方法聲明,那麼就可以忽略接口中定義的方法。
  
  子類勝於父類。果一個接口繼承了另一個接口, 且兩個接口都定義了一個默認方法,那麼子類中定義的方法勝出。
  
  如果上面兩條規則不適用, 子類要麼需要實現該方法, 要麼將該方法聲明爲抽象方法。
  
  回到頂部
  
  四、其他
  
  使用 Lambda 表達式,就是將複雜性抽象到類庫的過程。
  
  面向對象編程是對數據進行抽象, 而函數式編程是對行爲進行抽象。
  
  Java8 雖然在匿名內部類中可以引用非 final 變量, 但是該變量在既成事實上必須是final。即如果你試圖給該變量多次賦值, 然後在 Lambda 表達式中引用它, 編譯器就會報錯。
  
  Stream 是用函數式編程方式在集合類上進行復雜操作的工具。
  
  對於需要大量數值運算的算法來說, 裝箱和拆箱的計算開銷, 以及裝箱類型佔用的額外內存, 會明顯減緩程序的運行速度。爲了減小這些性能開銷, Stream 類的某些方法對基本類型和裝箱類型做了區分。比如 IntStream、LongStream 等。
  
  Java8 對爲 null 的字段也引進了自己的處理,既不用一直用 if 判斷對象是否爲 null,來看看?
  
  public static List<AssistantVO> getAssistant(Long tenantId) {
  
  // ofNullable 如果 value 爲null,會構建一個空對象。
  
  Optional<List<AssistantVO>www.quwanyule157.com> assistantVO = Optional.ofNullable(ASSISTANT_MAP.get(tenantId));
  
  // orElse 如果 value 爲null,選擇默認對象。
  
  assistantVO.orElse(ASSISTANT_MAP.www.mingcheng178.comget(DEFAULT_TENANT));
  
  return assistantVO.get();  一、函數接口
  
  二、Lambda 表達式
  
  三、默認方法
  
  四、其他
  
  回到頂部
  
  一、函數接口
  
  接口    參數    返回類型    描述
  
  Predicate<T>    T    boolean    用來比較操作
  
  Consumer<T>    T    void    沒有返回值的函數
  
  Function<T, R>    T    R    有返回值的函數
  
  Supplier<T>    None    T    工廠方法-返回一個對象
  
  UnaryOperator<T>    T    T    入參和出參都是相同對象的函數
  
  BinaryOperator<T>    (T,T)    T    求兩個對象的操作結果
  
  爲什麼要先從函數接口說起呢?因爲我覺得這是 java8 函數式編程的入口呀!每個函數接口都帶有 @FunctionalInterface 註釋,有且僅有一個未實現的方法,表示接收 Lambda 表達式,它們存在的意義在於將代碼塊作爲數據打包起來。
  
  沒有必要過分解讀這幾個函數接口,完全可以把它們看成普通的接口,不過他們有且僅有一個抽象方法(因爲要接收 Lambda 表達式啊)。
  
  @FunctionalInterface 該註釋會強制 javac 檢查一個接口是否符合函數接口的標準。 如果該註釋添加給一個枚舉類型、 類或另一個註釋, 或者接口包含不止一個抽象方法, javac 就會報錯。
  
  回到頂部
  
  二、Lambda 表達式
  
  1、Lambda 表達式和匿名內部類
  
  先來複習一下匿名內部類的知識:
  
  如果是接口,相當於在內部返回了一個接口的實現類,並且實現方式是在類的內部進行的;
  
  如果是普通類,匿名類相當於繼承了父類,是一個子類,並可以重寫父類的方法。
  
  需要特別注意的是,匿名類沒有名字,不能擁有一個構造器。如果想爲匿名類初始化,讓匿名類獲得一個初始化值,或者說,想使用匿名內部類外部的一個對象,則編譯器要求外部對象爲final屬性,否則在運行期間會報錯。
  
  new Thread(new Runnable() {
  
  @Override
  
  public void run() {
  
  System.out.println(123);
  
  }
  
  }).start();
  
  new Thread(()-> System.out.println(123)).start();
  
  如上,和傳入一個實現某接口的對象不同, 我們傳入了一段代碼塊 —— 一個沒有名字的函數。() 是參數列表, 和上面匿名內部類示例中的是一樣的。 -> 將參數和 Lambda 表達式的主體分開, 而主體是之後操作會運行的一些代碼。
  
  Lambda 表達式簡化了匿名內部類的寫法,省略了函數名和參數類型。即參數列表 () 中可以僅指定參數名而不指定參數類型。
  
  Java 是強類型語言,爲什麼可以不指定參數類型呢?這得益於 javac 的類型推斷機制,編譯器能夠根據上下文信息推斷出參數的類型,當然也有推斷失敗的時候,這時就需要手動指明參數類型了。javac 的類型推斷機制如下:
  
  對於類中有重載的方法,javac 在推斷類型時,會挑出最具體的類型。
  
  如果只有一個可能的目標類型, 由相應函數接口裏的參數類型推導得出;
  
  如果有多個可能的目標類型, 由最具體的類型推導得出;
  
  如果有多個可能的目標類型且最具體的類型不明確, 則需人爲指定類型。
  
  2、Lambda 表達式和集合
  
  java8 在 java.util 包中引入了一個新的類 —— Stream.java。java8 之前我們迭代集合,都只能依賴外部迭代器 Iterator 對集合進行串行化處理。而 Stream 支持對集合順序和並行聚合操作,將更多的控制權交給集合類,是一種內部迭代方式。這有利於方便用戶寫出更簡單的代碼,明確要達到什麼轉化,而不是如何轉化。
  
  Stream 的操作有兩種,一種是描述 Stream ,如 filter、map 等最終不產生結果的行爲稱爲"惰性求值";另外一種像 foreach、collect 等是從 Stream 中產生結果的行爲稱爲"及早求值"。
  
  接下來讓我們瞧瞧 Stream 如何結合 Lambda 表達式優雅的處理集合...
  
  foreach - 迭代集合
  
  list.forEach(e -> System.out.println(e));
  
  map.forEach((k, v) -> {
  
  System.out.println(k);
  
  System.out.println(v);
  
  });
  
  collect(toList()) - 由Stream裏的值生成一個列表。
  
  List<String> list = Stream.of("java", "C++", "Python").collect(Collectors.toList());
  
  等價於:
  
  List<String> asList = Arrays.asList("java", "C++", "Python");
  
  filter - 遍歷並檢查過濾其中的元素。
  
  long count = list.stream().filter(x -> "java".equals(x)).count();
  
  map、mapToInt、mapToLong、mapToDouble - 將流中的值轉換成一個新的值。
  
  List<String> mapList = list.stream().map(str -> str.toUpperCase()).collect(Collectors.toList());
  
  List<String> list = Stream.of("java", "javascript", "python").collect(Collectors.toList());
  
  IntSummaryStatistics intSummaryStatistics = list.stream().mapToInt(e -> e.length()).summaryStatistics();
  
  System.out.println("最大值:" + intSummaryStatistics.getMax());
  
  System.out.println("最小值:" + intSummaryStatistics.getMin());
  
  System.out.println("平均值:" + intSummaryStatistics.getAverage());
  
  System.out.println("總數:" + intSummaryStatistics.getSum());
  
  mapToInt、mapToLong、mapToDouble 和 map 操作類似,只是把函數接口的返回值改爲 int、long、double 而已。
  
  flatMap - 將多個 Stream 連接成一個 Stream
  
  List<String> streamList = Stream.of(list, asList).flatMap(x -> x.stream()).collect(Collectors.toList());
  
  flatMap 方法的相關函數接口和 map 方法的一樣, 都是 Function 接口, 只是方法的返回值限定爲 Stream 類型罷了。
  
  Max-求最大值、Min-求最小值
  
  String maxStr = list.stream(www.michenggw.com).max(Comparator.comparing(e -> e.length())).get();
  
  String minStr = list.stream(www.yigouyule2.cn ).min(Comparator.comparing(e -> e.length())).get();
  
  reduce - 聚合操作,從一組元素中生成一個值,sum()、max()、min()、count() 等都是reduce操作,將他們單獨設爲函數只是因爲常用。
  
  Integer sum1 = Stream.of(1, 2, 3).reduce(0, (acc, e) -> acc + e);
  
  上述執行求和操作,有兩個參數: 傳入 Stream 中初始值和 acc。 將兩個參數相加,acc 是累加器,保存着當前的累加結果。
  
  待續...
  
  回到頂部
  
  三、默認方法
  
  java8 中新增了 Stream 操作,那麼第三方類庫中的自定義集合 MyList 要怎麼做到兼容呢?總不能升級完 java8,第三方類庫中的集合實現全都不能用了吧?
  
  爲此,java8 在接口中引入了"默認方法"的概念!默認方法是指接口中定義的包含方法體的方法,方法名有 default 關鍵字做前綴。默認方法的出現是爲了 java8 能夠向後兼容。
  
  public interface Iterable<T> {
  
  /**
  
  * Performs the given action for each element of the {@code Iterable}
  
  * until all elements have been processed or the action throws an
  
  * exception.  Unless otherwise specified by the implementing class,
  
  * actions are performed in www.trgj888.com/  the order of iteration (if an iteration order
  
  * is specified).  Exceptions thrown by the action are relayed to the
  
  * caller.
  
  *
  
  * @implSpec
  
  * <p>The default implementation behaves as if:
  
  * <pre>{@code
  
  *     for (T t : this)
  
  *         action.accept(t);
  
  * }</pre>
  
  *
  
  * @param action The action to be performed for each element
  
  * @throws NullPointerException if the specified action is null
  
  * @since 1.8
  
  */
  
  default void forEach(Consumer<? super T> action) {
  
  Objects.requireNonNull(www.yongshiyule178.com  action);
  
  for (T t : this) {
  
  action.accept(t);
  
  }
  
  }
  
  }
  
  看 java8 中的這個 Iterable.java 中的默認方法 forEach(Consumer<? super T> action),表示“如果你們沒有實現 forEach 方法,就使用我的吧”。
  
  默認方法除了添加了一個新的關鍵字 default,在繼承規則上和普通方法也略有差別:
  
  類勝於接口。如果在繼承鏈中有方法體或抽象的方法聲明,那麼就可以忽略接口中定義的方法。
  
  子類勝於父類。果一個接口繼承了另一個接口, 且兩個接口都定義了一個默認方法,那麼子類中定義的方法勝出。
  
  如果上面兩條規則不適用, 子類要麼需要實現該方法, 要麼將該方法聲明爲抽象方法。
  
  回到頂部
  
  四、其他
  
  使用 Lambda 表達式,就是將複雜性抽象到類庫的過程。
  
  面向對象編程是對數據進行抽象, 而函數式編程是對行爲進行抽象。
  
  Java8 雖然在匿名內部類中可以引用非 final 變量, 但是該變量在既成事實上必須是final。即如果你試圖給該變量多次賦值, 然後在 Lambda 表達式中引用它, 編譯器就會報錯。
  
  Stream 是用函數式編程方式在集合類上進行復雜操作的工具。
  
  對於需要大量數值運算的算法來說, 裝箱和拆箱的計算開銷, 以及裝箱類型佔用的額外內存, 會明顯減緩程序的運行速度。爲了減小這些性能開銷, Stream 類的某些方法對基本類型和裝箱類型做了區分。比如 IntStream、LongStream 等。
  
  Java8 對爲 null 的字段也引進了自己的處理,既不用一直用 if 判斷對象是否爲 null,來看看?
  
  public static List<AssistantVO> getAssistant(Long tenantId) {
  
  // ofNullable 如果 value 爲null,會構建一個空對象。
  
  Optional<List<AssistantVO>www.quwanyule157.com> assistantVO = Optional.ofNullable(ASSISTANT_MAP.get(tenantId));
  
  // orElse 如果 value 爲null,選擇默認對象。
  
  assistantVO.orElse(ASSISTANT_MAP.www.mingcheng178.comget(DEFAULT_TENANT));
  
  return assistantVO.get();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章