1. 程式人生 > 實用技巧 >Java8常用的內建函式式介面(一)Predicate、Consumer、Supplier、Function

Java8常用的內建函式式介面(一)Predicate、Consumer、Supplier、Function

Java8常用的內建函式式介面(一)

簡介

  • JDK 1.8 API中包含了很多內建的函式式介面。有些是在以前版本的Java中大家耳熟能詳的,例如Comparator介面,或者Runnable介面。對這些現成的介面進行實現,可以通過@FunctionalInterface 標註來啟用Lambda功能支援。

  • 此外,Java 8 API 還提供了很多新的函式式介面,來降低程式設計師的工作負擔。

  • 比如我們今天要了解到的四大常用的內建函式式介面:下表 | 序號 | 介面名 | 介面型別 | | ---- | -------------- | ----- | | 1 | Predicate | 斷言型介面 | | 2 | Consumer | 消費型介面 | | 3 | Supplier | 供給型介面 | | 4 | Function<T, R> | 函式式介面 |

  • JDK 1.8之前已有的函式式介面

    java.lang.Runnable java.util.concurrent.Callable java.security.PrivilegedAction java.util.Comparator java.io.FileFilter java.nio.file.PathMatcher java.lang.reflect.InvocationHandler java.beans.PropertyChangeListener java.awt.event.ActionListener javax.swing.event.ChangeListener

Predicate 斷言型介面


Predicate是一個斷言式的函式式介面,返回的是一個boolean值,用於進行判斷行為與引數是否相符。Java8Stream中的filter使用的就是此函式式介面。 Predicate是一個布林型別的函式,該函式只有一個輸入引數。Predicate介面包含了多種預設方法,用於處理複雜的邏輯動詞(and, or,negate)

Predicate常用方法

1. test()方法


test原始碼:


    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);// 用來判斷傳過來的引數是否符合規則

test方法的作用是:

1 . 評估引數裡面的表示式(說白了就是驗證傳進來的引數符不符合規則) 2 . 它的返回值是一個boolean型別(這點需要注意一下)。

測試示例程式碼:

	//傳入一個字串,test方法判斷該字串長度是否大於2
    @Test
    public void test1(){
        Predicate<String> predicate = (s) -> s.length() > 2;
        boolean foo = predicate.test("foo");// true
   }

2.and()方法(預設方法)


and原始碼:


    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * AND of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code false}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ANDed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * AND of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
        //通過原始碼我們可以看到,return最總返回的是並且的邏輯,必須滿足兩個條件,等同於我們的邏輯與&&,存在短路特性
    }
  • 注意:and 方法返回一個Predicate<? super T> 測試示例程式碼:
	//通過呼叫下面的方法,我們將and和test方法連續使用
        boolean b = testAndMethod("zhangssss",
                stringOne -> stringOne.equals("zhangsan"), stringTwo -> stringTwo.length() > 5);
        System.out.println("測試and方法列印結果:"+b);

    /**
     *
     * @param stringOne         待判斷的字串
     * @param predicateOne      斷定表示式1
     * @param predicateTwo      斷定表示式2
     * @return                    是否滿足兩個條件
     */
    public boolean testAndMethod(String stringOne, Predicate<String> predicateOne,Predicate<String> predicateTwo) {

        return predicateOne.and(predicateTwo).test(stringOne);//and 方法返回一個Predicate<? super T>
    }
列印結果:
	測試and方法列印結果:false 
	因為使用and方法同時需要滿足兩個條件

3.negate()方法(預設方法)


negate原始碼:

    /**
     * Returns a predicate that represents the logical negation of this
     * predicate.
     * @return a predicate that represents the logical negation of this
     * predicate
     */
    default Predicate<T> negate() {
        return (t) -> !test(t);
        //我們可以看到return返回的是test()方法的相反結果,等同於我們的邏輯非
    }

返回值一樣需要注意, 是Predicate 測試示例程式碼:

        //測試negate()和test()
        boolean f = testNageteMethod("zhangsan", stringOne -> stringOne.equals("zhangsan"));
        System.out.println("測試negate方法列印結果:"+f);


    public boolean testNageteMethod(String stringValue, Predicate<String> predicate) {
        return predicate.negate().test(stringValue);
    }
列印結果:
	測試negate方法列印結果:false

4.or()方法(預設方法)


or原始碼:

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * OR of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code true}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ORed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * OR of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
        //通過return我們可以看出返回的是多者選其一即可, 等同於我們的邏輯或
    }

返回值一樣需要注意,Predicate 測試示例程式碼:

        //測試or方法
        boolean a = testOrMethod("zhangsan"
                , stringOne -> stringOne.equals("zhangsan111")
                , stringTwo -> stringTwo.length() > 50
                , stringThree -> stringThree.length() % 2 == 0);
        System.out.println("測試or方法列印結果:"+a);
    public boolean testOrMethod(String stringOne, Predicate<String> predicateOne, Predicate<String> predicateTwo, Predicate<String> predicateThree) {

        return predicateOne.or(predicateTwo).or(predicateThree).test(stringOne);
    }

5. isEqual()方法(靜態方法)


判斷兩個物件是否相等—> 使用的是Objects裡面的equals()方法進行比較 isEqual原始碼:

    /**
     * Returns a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}.
     *
     * @param <T> the type of arguments to the predicate
     * @param targetRef the object reference with which to compare for equality,
     *               which may be {@code null}
     * @return a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}
     */
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

解釋如下: 本類由一些操作物件的靜態工具方法構成,這些工具方法包括了非空檢查、方法的非空引數檢查、比較物件的hashCode、為物件返回一個字串表示、比較兩個物件,說的很明顯了,比較的兩個物件的HashCode值 通俗一點解釋: 先判斷物件是否為NULL—> 這個由Objects裡面的isNull進行判斷,如果,不為Null的話,那麼接下來用java.lang.object裡面的equals()方法進行比較. 測試示例程式碼:

        //測試isEqual()方法
        System.out.println(testMethodIsEquals("zhangsan","zhangsan"));
        System.out.println("~~~   ~~~   ~~~   ~~~");
        System.out.println(testMethodIsEquals("zhangsan","lisi"));
        System.out.println("~~~   ~~~   ~~~   ~~~");
        System.out.println(testMethodIsEquals(null,"zhangsan")); /* 我們來Debug一下這個程式*/
        
        public boolean testMethodIsEquals(String strValue, String strValue2) {

        return Predicate.isEqual(strValue).test(strValue2);
    }

Consumer 消費型介面


Consumer的作用顧名思義,是給定義一個引數,對其進行(消費)處理,處理的方式可以是任意操作. 介面原始碼:

@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); };
    }
}

Consumer常用方法


1.accept()方法

accept原始碼:

	/**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

accept(T t),接受一個引數,沒有返回值 測試示例程式碼:

        List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        // 通過lambda表示式構造出Consumer物件
        Consumer listener = i -> System.out.println(i);
        list.forEach(listener);

上面是通過lambda表示式構造出Consumer物件,將list中每一個元素,傳遞給consumer,執行列印的操作,我再把這個例子做一個變化

	// 通過lambda表示式構造出Consumer物件
	list.forEach(i -> System.out.println(i * 2));

這裡列印的元素是乘2後的結果,這就說明了通過lambda表示式,我們傳遞的是行為accept(T t)方法只負責接收一個引數,至於要做什麼,是我們再呼叫的時候,把行為傳遞過去。 另外還可以使用方法引用的方式來呼叫Consumeraccept方法。

	// 通過方法引用的方式構造出Consumer物件
	list.forEach(System.out::println);

這裡也可以實現遍歷每一個元素並打印出來,這是通過方法引用的方式來構造出的Consumer物件。"::"這裡兩個連續的冒號,是jdk8支援的語法,可以自動定位到具體的函式式介面,這裡就可以自動定位到Consumer

2. andThen()方法


andThen原始碼:

	/**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an 		exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }

該方法預設實現,它接收一個Consumer物件,同時會返回一個Consumer物件,返回的Consumer物件還可以繼續呼叫andThen方法,這樣該方法就實現了將執行操作給序列化。舉個例子:

        print(list, item -> System.out.print(" consumer1-->:" + item * 2), item -> System.out.println(" consumer2-->:" + item * 3));


    /*
        andThen方法, 將引數傳遞給呼叫者執行accept方法,然後再傳給第二個consumer執行accept方法。
     */
    public void print(List<Integer> list, IntConsumer con1, IntConsumer con2) {
        list.forEach(item -> con1.andThen(con2).accept(item));
    }

該示例構造了兩個Consumer物件,通過consumerandThen方法,將兩個操作給序列起來,對於list中每個元素,都會先執行con1的appect方法,再執行con2的accept方法。

列印結果:
 consumer1-->:2 consumer2-->:3
 consumer1-->:4 consumer2-->:6
 consumer1-->:6 consumer2-->:9
 consumer1-->:8 consumer2-->:12
 consumer1-->:10 consumer2-->:15
 consumer1-->:12 consumer2-->:18
 consumer1-->:14 consumer2-->:21
 consumer1-->:16 consumer2-->:24
 consumer1-->:18 consumer2-->:27

與Consumer相關的介面

BiConsumer<T, U>

  • 處理一個兩個引數

DoubleConsumer

  • 處理一個double型別的引數

IntConsumer

  • 處理一個int型別的引數

LongConsumer

  • 處理一個long型別的引數

ObjIntConsumer

  • 處理兩個引數,且第二個引數必須為int型別

ObjLongConsumer

  • 處理兩個引數,且第二個引數必須為long型別

Supplier供給型介面


Supplier介面是物件例項的提供者,定義了一個名叫get的抽象方法,它沒有任何入參,並返回一個泛型T物件,具體原始碼如下:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

常用方法

1. get()方法

下面我們使用宣告一個Supplier的例項:

    /**
     *supplier供給型函式介面測試
     */
    @Test
    public void testSupplier() {
        Supplier<Person> supplier = Person::new;
        Person person = supplier.get();
        System.out.println("通過Supplier介面建立Person空物件:"+person);
        System.out.println("---------------------------");

        Supplier<Person> supplier1 =() -> new Person("Evonne", "Shari", "JAVA", "female", 40, 1800);
        Person person1 = supplier1.get();
        System.out.println("通過Supplier介面建立Person非空物件:"+person1);
    }
列印結果:
通過Supplier介面建立Person空物件:Person(firstName=null, lastName=null, job=null, gender=null, salary=0, age=0)
---------------------------
通過Supplier介面建立Person非空物件:Person(firstName=Evonne, lastName=Shari, job=JAVA, gender=female, salary=40, age=1800)

特別需要注意的是,本例中每一次呼叫get方法都會建立新的物件。

Function<T,R>函式式介面

Function介面可以建立更加複雜的Function介面例項。有兩個引數:T入參,R返參

常用方法

1. apply()方法


Function 就是一個函式,其作用類似於數學中函式的定義,所以Function中沒有具體的操作,具體的操作需要我們去為它指定,因此apply具體返回的結果取決於傳入的lambda表示式。 apply原始碼:

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

測試示例程式碼:

public void test(){
    Function<Integer,Integer> test=i->i+1;
    test.apply(5);
}
/**列印結果 print:6*/

解析:上面的示例用lambda表示式定義了一個行為使得i自增1,我們使用引數5(相當於傳入引數5)執行apply,最後列印結果6。這跟我們以前看待Java的眼光已經不同了,在函數語言程式設計之前我們定義一組操作首先想到的是定義一個方法,然後指定傳入引數,返回我們需要的結果。函數語言程式設計的思想是先不去考慮具體的行為,而是先去考慮引數,具體的方法我們可以後續再設定。 再舉個例子:

public void test(){
    Function<Integer,Integer> test1=i->i+1;
    Function<Integer,Integer> test2=i->i*i;
    
    System.out.println(calculate(test1,5));
    System.out.println(calculate(test2,5));
}
public static Integer calculate(Function<Integer,Integer> test,Integer number){
    return test.apply(number);
}
/** 列印結果print:6*/
/**列印結果 print:25*/

通過傳入不同的Function,實現了在同一個方法中實現不同的操作。在實際開發中這樣可以大大減少很多重複的程式碼,比如我在實際專案中有個新增使用者的功能,但是使用者分為VIP和普通使用者,且有兩種不同的新增邏輯。那麼此時我們就可以先寫兩種不同的邏輯。除此之外,這樣還讓邏輯與資料分離開來,我們可以實現邏輯的複用。

當然實際開發中的邏輯可能很複雜,比如兩個方法F1,F2都需要兩個個邏輯AB,但是F1需要A->B,F2方法需要B->A。這樣的我們用剛才的方法也可以實現,原始碼如下:

public void test(){
    Function<Integer,Integer> A=i->i+1;
    Function<Integer,Integer> B=i->i*i;
    System.out.println("F1:"+B.apply(A.apply(5)));
    System.out.println("F2:"+A.apply(B.apply(5)));
}
/** F1:36 */
/** F2:26 */

2 . compose()和andThen()方法

接著上面的例子,假如我們F1,F2需要四個邏輯ABCD,那我們還這樣寫就會變得很麻煩了。 composeandThen可以解決我們的問題。先看compose的原始碼:

  default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));//before,先執行傳入的V,在執行原有的
   }

compose接收一個Function引數,返回時先用傳入的邏輯執行apply,然後使用當前Function的apply。

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));//after,先執行原有的,再執行傳入的引數
    }

andThen跟compose正相反,先執行當前的邏輯,再執行傳入的邏輯 換個說法: compose等價於B.apply(A.apply(5)),而andThen等價於A.apply(B.apply(5))。

public void test(){
    Function<Integer,Integer> A=i->i+1;
    Function<Integer,Integer> B=i->i*i;
    System.out.println("F1:"+B.apply(A.apply(5)));
    System.out.println("F1:"+B.compose(A).apply(5));
    System.out.println("F2:"+A.apply(B.apply(5)));
    System.out.println("F2:"+B.andThen(A).apply(5));
}
/** F1:36 */
/** F1:36 */
/** F2:26 */
/** F2:26 */

我們可以看到上述兩個方法的返回值都是一個Function,這樣我們還可以使用建造者模式(待研究)的操作來使用。

  Integer apply1 = B1.compose(A1).compose(A1).andThen(A1).apply(5);//待研究

結束語

至此,對於函式式介面有了一個初步認識,革命向未成功,同志仍需努力。

推薦參考blog: Function介面的使用 死磕Lambda表示式