Java8新特性探索之函數式接口

一、為什麼引入函數式接口

作為Java函數式編程愛好者,我們都知道方法引用和 Lambda 表達式都必須被賦值,同時賦值需要類型信息才能使編譯器保證類型的正確性。

我們先看一個Lambda代碼示例:

x -> x.toString()

我們清楚這裡返回類型必須是 String,但 x 是什麼類型呢?

Lambda 表達式包含類型推導(編譯器會自動推導出類型信息,避免了程序員顯式地聲明),編譯器必須能夠以某種方式推導出 x 的類型以生成正確的代碼。

同樣方法引用也存在此問題,假設你要傳遞 System.out :: println 到你正在編寫的方法 ,你怎麼知道傳遞給方法的參數的類型?

為了解決上述問題,Java 8 引入了函數式接口,在 java.util.function 包,它包含一組接口,這些接口是 Lambda 表達式和方法引用的目標類型,每個接口只包含一個抽象方法,稱為函數式方法。只有確保接口中有且僅有一個抽象方法,Lambda表達式的類型信息才能順利地進行推導。

二、如何使用函數式接口

在編寫接口時,可以使用 @FunctionalInterface 註解強制執行此函數式方法模式:

  1. 在接口上使用註解 @FunctionalInterface ,一旦使用該註解來定義接口,編譯器將會強制檢查該接口是否確實有且僅有一個抽象方法,否則將會報錯。

    @FunctionalInterface
    public interface MyFunction {
     /**
      * 自定義的抽象方法
      */

     void run();
    }
  2. 在函數式接口,有且僅有一個抽象方法,Objectpublic方法除外

    @FunctionalInterface
    public interface MyFunction {
     
     /**
      * 自定義的抽象方法
      */

     void run();
     
     /**
      * Object的equals方法
      * @param obj
      * @return
      */

     @Override
     boolean equals(Object obj);
     
     /**
      * Object的toString方法
      * @return
      */

     @Override
     String toString();
     
     /**
      * Object的hashCode方法
      * @return
      */

     @Override
     int hashCode();
     
    }
  3. 在函數式接口中,我們可以使用default修飾符定義默認方法,使用static修飾符定義靜態方法

    @FunctionalInterface
    public interface MyFunction {
     
     /**
      * 自定義的抽象方法
      */

     void run();
     
     /**
      * static修飾符定義靜態方法
      */

        static void staticRun() {
            System.out.println("接口中的靜態方法");
        }
     
        /**
         * default修飾符定義默認方法
         */

        default void defaultRun() {
            System.out.println("接口中的默認方法");
        }
        
    }
  • 為大家演示下自定義無泛型的函數式接口測試實例:

    /**
    * 自定義的無泛型函數式接口
    */

    @FunctionalInterface
    public interface MyFunction {
     
     /**
      * 自定義的抽象方法
      * @param x
      */

     void run(Integer x);
     
        /**
         * default修飾符定義默認方法
         * @param x
         */

        default void defaultMethod(Integer x) {
            System.out.println("接口中的默認方法,接收參數是:" + x);
        }
        
    }

    /**
    * 測試類
    */

    public class MyFunctionTest {

     @Test
     public void functionTest() {
      test(6, (x) -> System.out.println("接口中的抽象run方法,接收參數是:" + x));
     }
     
     public void test(int n, MyFunction function) {
      System.out.println(n);
      function.defaultMethod(n);
      function.run(n);
     }
     
    }

    輸出結果:

    6
    接口中的默認方法,接收參數是:6
    接口中的抽象run方法,接收參數是:6
  • 為大家演示下自定義有泛型的函數式接口測試實例:

    /**
     * 自定義的有泛型函數式接口
     */

    @FunctionalInterface
    public interface MyFunctionGeneric<T{

     /**
      * 轉換值
      * @param t
      * @return
      */

     convertValue(T t);
     
    }

    /**
    * 測試類
    */

    public class MyFunctionGenericTest {

     @Test
     public void convertValueTest() {
      String result = toLowerCase((x) -> x.toLowerCase(), "ABC");
      System.out.println(result);
     }
     
     public String toLowerCase(MyFunctionGeneric<String> functionGeneric, String value) {
      return functionGeneric.convertValue(value);
     }
     
    }

    輸出結果:

    abc

    注意:作為參數傳遞 Lambda 表達式:為了將 Lambda 表達式作為參數傳遞,接收Lambda 表達式的參數類型必須是與該 Lambda 表達式兼容的函數式接口
    的類型。

三、Java8四大內置核心函數式接口

首先總覽下四大函數式接口的特點說明:

接口 參數類型 返回類型 方法 說明
Consumer T void void accept(T t) 消費型接口,對類型T參數操作,無返回結果
Supplier T T get() 供給型接口,創造T類型參數
Function T R R apply(T t) 函數型接口,對類型T參數操作,返回R類型參數
Predicate T boolean boolean test(T t) 斷言型接口,對類型T進行條件篩選操作

消費型接口Consumer<T>

java.util.function.Consumer<T> 接口是消費一個數據,其數據類型由泛型決定。

接口源碼:

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Consumer<T{

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}
  1. 抽象方法:void accept(T t),接收並消費一個指定泛型的數據,無需返回結果。
  2. 默認方法:default Consumer<T> andThen(Consumer<? super T> after),如果一個方法的參數和返回值全都是 Consumer 類型,那麼就可以實現效果:消費數據的時候,首先做一個操作,然後再做一個操作,實現組合
public class ConsumerTest {

 /**
  * 先計算總分,再計算平均分
  */

 @Test
 public void calculate() {
  Integer[] fraction = new Integer[] { 657685928899 };
  consumer(fraction, x -> System.out.println(Arrays.stream(x).mapToInt(Integer::intValue).sum()),
    y -> System.out.println(Arrays.stream(y).mapToInt(Integer::intValue).average().getAsDouble()));
 }
 
 public void consumer(Integer[] fraction, Consumer<Integer[]> x, Consumer<Integer[]> y) {
  x.andThen(y).accept(fraction);
 }
 
}

輸出結果:

505
84.16666666666667

由於Consumerdefault方法所帶來的嵌套調用(連鎖調用),對行為的抽象的函數式編程理念,展示的淋漓盡致。

其他的消費型函數式接口匯總說明:

接口名稱 方法名稱 方法簽名
DoubleConsumer accept (double) -> void
IntConsumer accept (int) -> void
LongConsumer accept (long) -> void
ObjDoubleConsumer accept (T, double) -> void
ObjIntConsumer accept (T, int) -> void
ObjLongConsumer accept (T, long) -> void

供給型接口Supplier<T>

java.util.function.Supplier<T> 接口僅包含一個無參的方法: T get() ,用來獲取一個泛型參數指定類型的對象數據。

接口源碼:

package java.util.function;

@FunctionalInterface
public interface Supplier<T{
    get();
}

由於這是一個函數式接口,意味着對應的Lambda表達式需要對外提供一個符合泛型類型的對象數據。

public class SupplierTest {

 public int getMax(Supplier<Integer> supplier) {
  return supplier.get();
 }
 
 /**
  * 獲取數組元素最大值
  */

 @Test
 public void getMaxTest() {
  Integer[] data = new Integer[] { 546321 };
  int result = getMax(() -> {
   int max = 0;
   for (int i = 0; i < data.length; i++) {
    max = Math.max(max, data[i]);
   }
   return max;
  });
  System.out.println(result);
 }
 
}

其他的供給型函數式接口匯總說明:

接口名稱 方法名稱 方法簽名
BooleanSupplier getAsBoolean () -> boolean
DoubleSupplier getAsDouble () -> double
IntSupplier getAsInt () -> int
LongSupplier getAsLong () -> long

函數型接口Function

java.util.function.Function<T,R> 接口用來根據一個類型的數據得到另一個類型的數據,前者稱為前置條件,後者稱為後置條件。

接口源碼:

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Function<TR{

    apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}
  1. 抽象方法 apply(T t):該方法接收入參是一個泛型T對象,並返回一個泛型T對象。

  2. 默認方法

    andThen(Function<? super R, ? extends V> after):該方法接受一個行為,並將父方法處理過的結果作為參數再處理。

    compose(Function<? super V, ? extends T> before):該方法正好與andThen相反,它是先自己處理然後將結果作為參數傳給父方法執行。

    @Test
    public void andThenAndComposeTest() {
        // 計算公式相同
        Function<Integer, Integer> andThen1 = x -> x + 1;
        Function<Integer, Integer> andThen2 = x -> x * 2;
        Function<Integer, Integer> compose1 = y -> y + 1;
        Function<Integer, Integer> compose2 = y -> y * 2;
        // 注意調用的先後順序
        // 傳入參數2後,先執行andThen1計算,將結果再傳入andThen2計算
        System.out.println(andThen1.andThen(andThen2).apply(2));
        // 傳入參數2後,先執行compose2計算,將結果再傳入compose1計算
        System.out.println(compose1.compose(compose2).apply(2));
    }

    輸出結果:

    6
    5
  3. 靜態方法identity():獲取到一個輸入參數和返回結果一樣的Function實例。

來一個自駕九寨溝的代碼示例:

public class FunctionTest {
 
 @Test
 public void findByFunctionTest() {
  Function<BigDecimal, BigDecimal> getMoney = m -> m.add(new BigDecimal(1000));
  BigDecimal totalCost = getMoney.apply(new BigDecimal(500));
  System.out.println("張三的錢包原本只有500元,自駕川西得去銀行再取1000元,取錢後張三錢包總共有" +           Function.identity().apply(totalCost) + "元");
  BigDecimal surplus = cost(totalCost, (m) -> {
   System.out.println("第二天出發前發現油不足,加油前有" + m + "元");
   BigDecimal lubricate = m.subtract(new BigDecimal(300));
   System.out.println("加油300後還剩餘" + lubricate + "元");
   return lubricate;
  }, (m) -> {
   System.out.println("到達景區門口,買景區票前有" + m + "元");
   BigDecimal tickets = m.subtract(new BigDecimal(290));
   System.out.println("買景區票290後還剩餘" + tickets + "元");
   return tickets;
  });
  System.out.println("最後張三返程到家還剩餘" + surplus + "元");
 }

 public BigDecimal cost(BigDecimal money, Function<BigDecimal, BigDecimal> lubricateCost,
   Function<BigDecimal, BigDecimal> ticketsCost)
 
{
  Function<BigDecimal, BigDecimal> firstNight = (m) -> {
   System.out.println("第一晚在成都住宿前有" + m + "元");
   BigDecimal first = m.subtract(new BigDecimal(200));
   System.out.println("交完200住宿費還剩餘" + first + "元");
   return first;
  };
  Function<BigDecimal, BigDecimal> secondNight = (m) -> {
   System.out.println("第二晚在九寨縣住宿前有" + m + "元");
   BigDecimal second = m.subtract(new BigDecimal(200));
   System.out.println("交完200住宿費還剩餘" + second + "元");
   return second;
  };
  return lubricateCost.andThen(ticketsCost).andThen(secondNight).compose(firstNight).apply(money);
 }

}

輸出結果:

張三的錢包原本只有500元,自駕川西得去銀行再取1000元,取錢後張三錢包總共有1500
第一晚在成都住宿前有1500
交完200住宿費還剩餘1300
第二天出發前發現油不足,加油前有1300
加油300後還剩餘1000
到達景區門口,買景區票前有1000
買景區票290後還剩餘710
第二晚在九寨縣住宿前有710
交完200住宿費還剩餘510
最後張三返程到家還剩餘510

其他的函數型函數式接口匯總說明:

接口名稱 方法名稱 方法簽名
BiFunction apply (T, U) -> R
DoubleFunction apply (double) -> R
DoubleToIntFunction applyAsInt (double) -> int
DoubleToLongFunction applyAsLong (double) -> long
IntFunction apply (int) -> R
IntToDoubleFunction applyAsDouble (int) -> double
IntToLongFunction applyAsLong (int) -> long
LongFunction apply (long) -> R
LongToDoubleFunction applyAsDouble (long) -> double
LongToIntFunction applyAsInt (long) -> int
ToDoubleFunction applyAsDouble (T) -> double
ToDoubleBiFunction applyAsDouble (T, U) -> double
ToIntFunction applyAsInt (T) -> int
ToIntBiFunction applyAsInt (T, U) -> int
ToLongFunction applyAsLong (T) -> long
ToLongBiFunction applyAsLong (T, U) -> long

斷言型接口Predicate<T>

java.util.function.Predicate<T> 接口中包含一個抽象方法: boolean test(T t) ,用於條件判斷的場景。默認方法:and or nagte (取反)。

接口源碼:

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T{

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

既然是條件判斷,就會存在與、或、非三種常見的邏輯關係。其中將兩個 Predicate 條件使用邏輯連接起來實現並且的效果時,類始於 Consumer接口 andThen()函數 其他三個雷同。

public class PredicateTest {
 /**
  * 查找在渝北的Jack
  */

 @Test
 public void findByPredicateTest() {
  List<User> list = Lists.newArrayList(new User("Johnson""渝北"), new User("Tom""渝中"), new User("Jack""渝北"));
  getNameAndAddress(list, (x) -> x.getAddress().equals("渝北"), (x) -> x.getName().equals("Jack"));
 }
 
 public void getNameAndAddress(List<User> users, Predicate<User> name, Predicate<User> address) {
  users.stream().filter(user -> name.and(address).test(user)).forEach(user -> System.out.println(user.toString()));
 }
}

輸出結果:

User [name=Jack, address=渝北]

其他的斷言型函數式接口匯總說明:

接口名稱 方法名稱 方法簽名
BiPredicate test (T, U) -> boolean
DoublePredicate test (double) -> boolean
IntPredicate test (int) -> boolean
LongPredicate test (long) -> boolean

四、總結

Lambda 表達式和方法引用並沒有將 Java 轉換成函數式語言,而是提供了對函數式編程的支持。這對 Java 來說是一個巨大的改進,因為這允許你編寫更簡潔明了,易於理解的代碼。

Tags: