Java 函式式介面 lamada 應用

zyfsuzy發表於2019-04-20

函式式介面

理解Functional Interface(函式式介面,以下簡稱FI)是學習Java8 Lambda表示式的關鍵所在,所以放在最開始討論。FI的定義其實很簡單:任何介面,如果只包含唯一一個抽象方法,那麼它就是一個FI。為了讓編譯器幫助我們確保一個介面滿足FI的要求(也就是說有且僅有一個抽象方法),Java8提供了@FunctionalInterface註解。舉個簡單的例子,Runnable介面就是一個FI,下面是它的原始碼:

@Functional Interface
public interface Runnable{
    public abstract  void run ();
}

public interface Runnable {
    public abstract  void run (); 
}
複製程式碼

ps: 上述兩種都是函式式介面,在Java 8 提供新的註解Function Interface 宣告一個介面為函式式介面,宣告之後這個介面必須符合函式式介面的規範。@FunctionalInterface 對於介面是不是函式式介面沒有影響,但該註解知識提醒編譯器去檢查該介面是否僅包含一個抽象方法。

下面的用法就是錯誤:

@Function Interface
public interface test{
    public void test1();
    public void test2();
複製程式碼

ps: 介面中宣告的方法預設是抽象的,使用註解後,編譯器自動檢查發現存在兩個抽象方法,會報錯。

函式式介面的規範

  1. 函式式介面裡是可以包含預設方法,因為預設方法不是抽象方法,其有一個預設實現,所以是符合函式式介面的定義的;
    @FunctionalInterface
    interface GreetingService
    {
        void sayMessage(String message);

        default void doSomeMoreWork1()
        {
            // Method body
        }

        default void doSomeMoreWork2()
        {
            // Method body
        }
    }
複製程式碼
  1. 函式式介面裡是可以包含靜態方法,因為靜態方法不能是抽象方法,是一個已經實現了的方法,所以是符合函式式介面的定義的;
    @FunctionalInterface
    interface GreetingService 
    {
        public void sayMessage(String message);
        public static void printHello(){
            System.out.println("Hello");
        }
    }
複製程式碼
  1. 函式式介面裡是可以包含Object裡的public方法,這些方法對於函式式介面來說,不被當成是抽象方法(雖然它們是抽象方法);因為任何一個函式式介面的實現,預設都繼承了Object類,包含了來自java.lang.Object裡對這些抽象方法的實現。
    @FunctionalInterface
    interface GreetingService  
    {
        void sayMessage(String message);
        
        @Override
        boolean equals(Object obj);
    }
複製程式碼

Java 內部類

為什麼講內部類,因為在 lamada 表示式又與 Java 內部類應用存在相似之處,特別是匿名內部類。在學習這部分前專門又複習了一遍內部類的概念。

分類

成員內部類
區域性內部類
靜態內部類
匿名內部類

成員內部類

  1. 定義成員內部類後在建立該內部類的物件是不同於普通類的,成員內部類是其外部類的屬性。因此在建立時必須首先建立其外部類物件,再建立內部類的物件。內部類 物件名 = 外部類物件.new 內部類( );
  2. 外部類是不能直接使用內部類的成員和方法滴,可先建立內部類的物件,然後通過內部類的物件來訪問其成員變數和方法。
  3. 可先建立內部類的物件,然後通過內部類的物件來訪問其成員變數和方法。
public class Outer {
    
    private static int i = 1;
    private int j = 10;
    private int k = 20;


    public static void outerF1() {
    }

    /**
     * 外部類的靜態方法訪問成員內部類,與在外部類外部訪問成員內部類一樣
     */
    public static void outerF4() {
        //step1 建立外部類物件
        Outer out = new Outer();
        //step2 根據外部類物件建立內部類物件
        Inner inner = out.new Inner();
        //step3 訪問內部類的方法
        inner.innerF1();
    }

    public static void main(String[] args) {

        /*
         * outerF4();該語句的輸出結果和下面三條語句的輸出結果一樣
         *如果要直接建立內部類的物件,不能想當然地認為只需加上外圍類Outer的名字,
         *就可以按照通常的樣子生成內部類的物件,而是必須使用此外圍類的一個物件來
         *建立其內部類的一個物件:
         *Outer.Inner outin = out.new Inner()
         *因此,除非你已經有了外圍類的一個物件,否則不可能生成內部類的物件。因為此
         *內部類的物件會悄悄地連結到建立它的外圍類的物件。如果你用的是靜態的內部類,
         *那就不需要對其外圍類物件的引用。
         */
        Outer out = new Outer();
        Outer.Inner outin = out.new Inner();
        outin.innerF1();
    }

    public void outerF2() {
    }

    /**
     * 外部類的非靜態方法訪問成員內部類
     */
    public void outerF3() {
        Inner inner = new Inner();
        inner.innerF1();
    }

    /**
     * 成員內部類中,不能定義靜態成員
     * 成員內部類中,可以訪問外部類的所有成員
     */
    class Inner {
        // static int innerI = 100;內部類中不允許定義靜態變數
        // 內部類和外部類的例項變數可以共存
        int j = 100;
        int innerI = 1;


        void innerF1() {
            System.out.println(i);
            //在內部類中訪問內部類自己的變數直接用變數名
            System.out.println(j);
            //在內部類中訪問內部類自己的變數也可以用this.變數名
            System.out.println(this.j);
            //在內部類中訪問外部類中與內部類同名的例項變數用外部類名.this.變數名
            System.out.println(Outer.this.j);
            //如果內部類中沒有與外部類同名的變數,則可以直接用變數名訪問外部類變數
            System.out.println(k);
            outerF1();
            outerF2();
        }
    }
}
複製程式碼
  1. 區域性內部類

在方法中定義的內部類稱為區域性內部類。與區域性變數類似,區域性內部類不能有訪問說明符,因為它不是外圍類的一部分,但是它可以訪問當前程式碼塊內的常量,和此外圍類所有的成員。

public class Outer {

    private int s = 100;
    private int outI = 1;

    public static void main(String[] args) {
        // 訪問區域性內部類必須先有外部類物件
        Outer out = new Outer();
        out.f(3);
    }

    public void f(final int k) {
        final int s = 200;
        int i = 1;
        final int j = 10;


        /**
         * 定義在方法內部
         */
        class Inner {
            // 可以定義與外部類同名的變數
            int s = 300;
            int innerI = 100;

            // static int m = 20; 不可以定義靜態變數
            Inner(int k) {
                innerF(k);
            }
            void innerF(int k) {
                // java如果內部類沒有與外部類同名的變數,在內部類中可以直接訪問外部類的例項變數
                System.out.println(outI);
                // 可以訪問外部類的區域性變數(即方法內的變數),但是變數必須是final的
                System.out.println(j);
                //System.out.println(i);
                // 如果內部類中有與外部類同名的變數,直接用變數名訪問的是內部類的變數
                System.out.println(s);
                // 用this.變數名訪問的也是內部類變數
                System.out.println(this.s);
                // 用外部類名.this.內部類變數名訪問的是外部類變數
                System.out.println(Outer.this.s);
            }
        }
        new Inner(k);
    }
}
複製程式碼
  1. 靜態內部類(巢狀類)

如果你不需要內部類物件與其外圍類物件之間有聯絡,那你可以將內部類宣告為static。這通常稱為巢狀類(nested class)。想要理解static應用於內部類時的含義,你就必須記住,普通的內部類物件隱含地儲存了一個引用,指向建立它的外圍類物件。然而,當內部類是static的時,就不是這樣了。 要建立巢狀類的物件,並不需要其外圍類的物件。 不能從巢狀類的物件中訪問非靜態的外圍類物件。

public class Outer {
    private static int i = 1;
    private int j = 10;

    public static void outerF1() {
    }

    public static void main(String[] args) {
        new Outer().outerF3();
    }

    public void outerF2() {
    }

    public void outerF3() {
        // 外部類訪問內部類的靜態成員:內部類.靜態成員
        System.out.println(Inner.inner_i);
        Inner.innerF1();
        // 外部類訪問內部類的非靜態成員:例項化內部類即可
        Inner inner = new Inner();
        inner.innerF2();
    }

    /**
     * 靜態內部類可以用public,protected,private修飾
     * 靜態內部類中可以定義靜態或者非靜態的成員
     */
    static class Inner {
        static int inner_i = 100;
        int innerJ = 200;

        static void innerF1() {
            // 靜態內部類只能訪問外部類的靜態成員(包括靜態變數和靜態方法)
            System.out.println("Outer.i" + i);
            outerF1();
        }


        void innerF2() {
            // 靜態內部類不能訪問外部類的非靜態成員(包括非靜態變數和非靜態方法)
            // System.out.println("Outer.i"+j);
            // outerF2();
        }
    }
}
複製程式碼

匿名內部類

這是我們今天的主角,匿名內部類, 字面意思沒有名字的類 {}。匿名內部作為最特殊的內部類,需要講解的內容。(think in java)

為什麼使用匿名內部類:

  1. 只用到類的一個例項
  2. 類在定義後馬上用到
  3. 類非常小(SUN推薦是在4行程式碼以下)
  4. 給類命名並不會導致你的程式碼更容易被理解

在使用匿名內部類時,要記住以下幾個原則:

  1. 匿名內部類一般不能有構造方法。
  2. 匿名內部類不能定義任何靜態成員、方法和類。
  3. 匿名內部類不能是public,protected,private,static。
  4. 只能建立匿名內部類的一個例項。
  5. 一個匿名內部類一定是在new的後面,用其隱含實現一個介面或實現一個類。
  6. 因匿名內部類為區域性內部類,所以區域性內部類的所有限制都對其生效。

你可能見過如下的程式碼:

    List<Integer> var1 = new ArrayList<Integer>()
    {
        {
            add(1);
            add(2);
        }
    };
複製程式碼

就是用到匿名類的語法糖。

// 在方法中返回一個匿名內部類
public class Parcel6 {
    public static void main(String[] args) {
        Parcel6 p = new Parcel6();
        Contents c = p.cont();
    }

    public Contents cont() {
        return new Contents() {
            private int i = 11;


            public int value() {
                return i;
            }
        }; // 在這裡需要一個分號
    }
}
複製程式碼

cont()方法將下面兩個動作合併在一起:返回值的生成,與表示這個返回值的類的定義。 return new Contents() 但是,在到達語句結束的分號之前,你卻說:“等一等,我想在這裡插入一個類的定義”:
這種奇怪的語法指的是:“建立一個繼承自Contents的匿名類的物件。”通過new 表示式返回的引用被自動向上轉型為對Contents的引用。匿名內部類的語法是下面例子的簡略形式:

class MyContents implements Contents {
    private int i = 11;

    public int value() {
        return i;
    }
}
return new MyContents();
複製程式碼

上述這類寫法是最常見。

在Java中,通常就是編寫另外一個類或類庫的人規定一個介面,然後你來實現這個介面,然後把這個介面的一個物件作為引數傳給別人的程式,別人的程式必要時就會通過那個介面來呼叫你編寫的函式,執行後續的一些方法。

public class CallBack {

    public static void main(String[] args) {
        CallBack callBack = new CallBack();
        callBack.toDoSomethings(100, new CallBackInterface() {
            public void execute() {
                System.out.println("我的請求處理成功了");
            }
        });

    }

    public void toDoSomethings(int a, CallBackInterface callBackInterface) {
        long start = System.currentTimeMillis();
        if (a > 100) {
            callBackInterface.execute();
        } else {
            System.out.println("a < 100 不需要執行回撥方法");
        }
        long end = System.currentTimeMillis();
        System.out.println("該介面回撥時間 : " + (end - start));
    }
}
public interface CallBackInterface {

    void execute();
}

複製程式碼

Java裡的回撥,可以說是匿名內部類精彩表演,優美的編碼風格,真是讓人陶醉~ this is so amazing 。

經過上述的鋪墊引出下面的主角 lamada 表示式實現函式式介面。

Lambda語法糖

為了能夠方便、快捷、幽雅的建立出FI的例項,Java8提供了Lambda表示式這顆語法糖。下面我用一個例子來介紹Lambda語法。假設我們想對一個List按字串長度進行排序,那麼在Java8之前,可以藉助匿名內部類來實現:

List<String> words = Arrays.asList("apple", "banana", "pear");
words.sort(new Comparator<String>() {
 
    @Override
    public int compare(String w1, String w2) {
        return Integer.compare(w1.length(), w2.length());
    }
 
});

複製程式碼

上面的匿名內部類簡直可以用醜陋來形容,唯一的一行邏輯被五行垃圾程式碼淹沒。根據前面的定義(並檢視Java原始碼)可知,Comparator是個FI,所以,可以用Lambda表示式來實現:

words.sort((String w1, String w2) -> {
    return Integer.compare(w1.length(), w2.length());
});
複製程式碼

ps: 看起來像一個匿名的方法,實際就是一個匿名類物件的引用,程式碼看起來更加簡潔。可以認為 lamada表示式實現了介面的抽象方法,因為函式式介面預設只有一個抽象方法。

參考文獻:
函式式介面概念
詳解內部類

相關文章