Java語言十五講(第七講 InnerClass)

 

同窗們,這一次講座,咱們講一下Inner Class內部類。
咱們平時寫的程序是由一個個類構成的,這些類之間是相互獨立的關係。咱們說過,這種思路源自對現實世界的模擬,拉近了「問題空間」和「解決空間」。所以簡化了系統的設計。
而Inner class 內部類是指一個類是另外一個類的內部成員,定義在某個類的內部的,對外可能可見也可能不可見。java

基本形式仍是蠻簡單的,咱們看一個例子:編程

public class OuterClass {
    private String outerName;
    public void display(){
        System.out.println("OuterClass display...");
        System.out.println(outerName);
    }
    public class InnerClass{
        private String innerName;
        InnerClass(){
            innerName = "inner class";
        }
        public void display(){
               System.out.println("InnerClass display...");
            System.out.println(innerName);
        }
    }
}

如上面的代碼所示,這樣在OuterClass裏面就定義了一個InnerClass類。使用的時候能夠這麼用:安全

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        outerClass.display();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.display();
    }

從代碼能夠看出,內部類跟一個普通屬性同樣使用,只要在外部類建立好的狀況下,就能夠去使用,能夠明顯看出這種被包含的關係。
並且,外部的程序能這麼用的一個前提是內部類聲明爲public,這樣纔會爲外部程序所見,這個跟普通屬性也是同樣的。固然,這個public不是約束包含的那個外部類的,不管把內部類聲明爲public仍是private,對包含它的外部類都是可見的。閉包

外部類和內部類的這種包含關係,決定了互相能夠調用。代碼以下(OuterClass.java):異步

public class OuterClass {
    private String outerName;

    public OuterClass(){
        outerName="OurClass Default Name";
    }
    public void display(){
        System.out.println("OuterClass display...");
        System.out.println(outerName);
    }
    public void displayInner(){
        InnerClass innerClass=new InnerClass();
        innerClass.display();
    }

    private class InnerClass{
        private String innerName;
        InnerClass(){
            outerName="outer class new name";
            innerName = "inner class default name";
        }
        public void displayOuter(){
            System.out.println(outerName);
        }        
        public void display(){
            System.out.println("InnerClass display...");
            System.out.println(innerName);
        }
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        outerClass.display();
        outerClass.displayInner();

        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.display();
        innerClass.displayOuter();
    }
}

咱們在OuterClass裏面用下面兩行建立了內部類並調用方法:ide

        InnerClass innerClass=new InnerClass();
        innerClass.display();

而在內部類裏面,咱們直接使用了外部內的屬性。這一段演示了外部類內部類的互操做。編譯器會作一個處理,對上面的內部類方法:函數式編程

public void displayOuter(){
            System.out.println(outerName);
        }  

編譯以後的代碼會變成:函數

OuterClass.this.outerName。

好奇的人通常會去bin目錄下看一眼編譯以後的結果,生成了兩個class文件,OuterClass.class和OuterClassthis

除了上述的基本型內部類,內部類還能夠定義成靜態的,或者在一個方法體內進行定義,如:spa

   void method1() {
      class InnerClass {
         public void print() {
            System.out.println("method inner class.");       
         }   
      }
      InnerClass inner = new InnerClass();
      inner.print();
   }

這個簡單,這裏再也不完整舉例。下面舉一個靜態內部類的例子,代碼以下(Employee.java):

public class Employee{    
    public String empName;    
    public Company company;    

    public Employee(String empName){    
         this.empName = empName;    
    }    
    public static class Company{    
         public String compName;    
         public String compRegion;    

         public Company(String compName,String compRegion){    
             this.compName = compName;    
             this.compRegion = compRegion;    
         }    
    }    

    public static void main(String[] args) {    
        Company company = new Employee.Company("Sun/Oracle", "China");  
        Employee alice = new Employee("Alice");    
        Employee bob = new Employee("Bob");    
        alice.company = company;    
        bob.company = company;    
    } 
}   

上面的Employee裏面包含一個靜態內部類Company,這樣跟一個普通類相似了,外部程序能夠直接建立這個內部類。並且多個外部類employee能夠共享同一個內部類company。要注意的是,這個時候,外部類能夠訪問內部類,而內部類訪問不了外部類。大家能夠本身動手試一下,在Employee中增長一個test(),而後試着在Company中調用,會有編譯錯誤:

Cannot make a static reference to the non-static method test() from the type Employee。

有了內部類的這些基礎知識,下面咱們要討論進階一點的內容了。
有一種頗有用的場合時匿名內部類。它看起來就是一個常規的內部類,可是不顯式起名,所以叫匿名類。這個場景,定義和實例化是寫在一塊兒同時的。當咱們須要實現一個接口或者抽象類的時候,咱們常常這麼用。
咱們看一個監聽器響應事件觸發的例子。代碼以下:
先定義一個Listener接口:

public interface ClickListener {
    void onClick();
}

再定義一個Button類,裏面包含一個Listener匿名內部類,代碼以下(Button.java):

public class Button {
    public void click(){
        new ClickListener(){
            public void onClick(){
                System.out.println("click ...");
            }
        }.onClick();
    }
    public static void main(String[] args) {
        Button button=new Button();
        button.click();
    }
}

仔細看看上面的代碼,按照常規,咱們應該在click()方法中定義這個內部類,而後new一個實例,再調用方法。而使用匿名內部類,咱們用一句話一鼓作氣:

new ClickListener(){
      public void onClick(){
          System.out.println("click ...");
      }
}.onClick();

之因此這麼用,是由於其實咱們只是想實現onClick()方法,至於這個類叫什麼名字,咱們並不關心,因此就匿名了。這種需求在些事件響應程序式會常常用到。這個匿名能夠當作一種簡寫,對編譯器來說,他仍是規規矩矩地生成了一個內部類的class文件ButtonInnerClass.class。爲何須要內部類?用兩個外部類不是同樣的嗎?從程序功能上來講,確實是同樣的。我我的理解的是,採用內部類技術,隱藏細節和內部結構,封裝性更好,讓程序結構更加合理優雅。在現實世界裏,一個事物內部都由不少部件組成,每一個部件也還可能包含子部件,這些子部件不須要暴露出來。內部類的思想就是借鑑了這個現實世界,概念的同一性讓它很好理解。這種思想更貼近現實世界,「問題空間」與「解決空間」更接近了。

除了上述的基本型內部類,內部類還能夠定義成靜態的,或者在一個方法體內進行定義,如:¨G6G這個簡單,這裏再也不完整舉例。下面舉一個靜態內部類的例子,代碼以下(Employee.java):¨G7G上面的Employee裏面包含一個靜態內部類Company,這樣跟一個普通類相似了,外部程序能夠直接建立這個內部類。並且多個外部類employee能夠共享同一個內部類company。要注意的是,這個時候,外部類能夠訪問內部類,而內部類訪問不了外部類。大家能夠本身動手試一下,在Employee中增長一個test(),而後試着在Company中調用,會有編譯錯誤:¨G8G有了內部類的這些基礎知識,下面咱們要討論進階一點的內容了。有一種頗有用的場合時匿名內部類。它看起來就是一個常規的內部類,可是不顯式起名,所以叫匿名類。這個場景,定義和實例化是寫在一塊兒同時的。當咱們須要實現一個接口或者抽象類的時候,咱們常常這麼用。

咱們看一個監聽器響應事件觸發的例子。代碼以下:先定義一個Listener接口:¨G9G再定義一個Button類,裏面包含一個Listener匿名內部類,代碼以下(Button.java):¨G10G仔細看看上面的代碼,按照常規,咱們應該在click()方法中定義這個內部類,而後new一個實例,再調用方法。而使用匿名內部類,咱們用一句話一鼓作氣:¨G11G之因此這麼用,是由於其實咱們只是想實現onClick()方法,至於這個類叫什麼名字,咱們並不關心,因此就匿名了。這種需求在些事件響應程序式會常常用到。這個匿名能夠當作一種簡寫,對編譯器來說,他仍是規規矩矩地生成了一個內部類的class文件Button1.class。
咱們要的是這個方法,而接口裏面也只有着一個方法,這種場景叫函數式接口,Java8以後能夠用函數式編程進一步簡化上面的代碼:

    public void click(){
        ClickListener listener = ()->{System.out.println("click ...");};
        listener.onClick();
    }

函數式接口與匿名類是不一樣的實現方式。編譯器並不會生成一個內部類class文件。

把匿名類當成方法的參數是常見的使用方式。
咱們寫一個程序,一個事件處理器響應事件的發生,響應完以後發一個消息通知。
先定義一個發消息的接口:

public interface IMessenger {
    void sendMessage(String string);
}

再定義事件處理器,代碼以下(Handler.java):

public class Handler {
    public void handleEvent(IMessenger messenger) {
        new Thread(new Runnable() {
             public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                messenger.sendMessage("SUCCESS.");
            }
        }).start();
    }
}

這個處理器的主體方法是handleEvent(IMessenger),異步處理事件花費兩秒鐘,以後發消息。
下面咱們使用這個事件處理器,代碼以下(HandlerTest.java):

public class HandlerTest {
    public static void main(String[] args) {
        Handler handler = new Handler();
        handler.handleEvent(new IMessenger() {
            public void sendMessage(String msg) {
                System.out.println("event handled. " + msg);
            }
        });
        System.out.println("------event handling test--------");
    }

}

注意上面代碼的主體部分:

        handler.handleEvent(new IMessenger() {
            public void sendMessage(String msg) {
                System.out.println("event handled. " + msg);
            }
        });

這裏就是生成了一個實現IMessenger接口的匿名類,而後傳給handleEvent()方法做爲參數使用。天然,編譯器會自動生成一個匿名類class文件。
再一次說起函數式編程,由於這個IMessenger是一個只有惟一一個方法的接口,因此此處咱們能夠再次用函數式編程簡化代碼:

    public static void main(String[] args) {
        Handler handler = new Handler();
        handler.handleEvent((msg) ->{System.out.println("event handled. " + msg);});
        System.out.println("------event handling test--------");
    }

匿名內部類來自外部閉包環境的自由變量必須是final的。這一點讓人費解,不少人在編譯器提示錯誤的時候就自動修改一下,並不深究。這個與Java對Closure閉包的實現有關,咱們來初步探究一下。這是比進階更加高級的課題了。
閉包是包含自由變量的函數,這些變量是在定義函數的環境中定義的,而函數自己也當成一個參數進行傳遞和返回,返回後,這個自由變量還同函數一同存在。在JavaScript和別的語言中流行。
你們也一直但願Java實現閉包。Java是經過內部類實現的,可是實現的不完整。
先看現象。咱們在上面的HandlerTest程序的主體部分增長一個自由變量,代碼以下:

        int i = 0;
        test.handleEvent(new IMessenger() {
            public void sendMessage(String msg) {
                System.out.println("event handled. " + msg);
                int j=0;
                j = i+j;
            }
        });

在JDK7以前,會有編譯錯誤:Local variable i defined in an enclosing scope must be final。這個錯誤出如今j=i+j;這一行。必須讓外面的這個自由變量i定義成final。(JDK8以後不會出錯。可是實際上仍是有這個限制,你試着把上面的代碼修改一下,給i進行一個賦值就會看出來了。)
把i定義成final以後,意味着咱們在匿名類中不能給自由變量i賦值了。
這兒在類裏面定義了內部類,而內部類又引用了外部的自由變量,這就構成了典型的閉包。Java並無徹底實現閉包,在生成匿名類的時候,它把外部自由變量i的value傳入,其實是一個拷貝,所以內部其實不能修改外部的自由變量了。Java團隊此處又偷了一個懶,乾脆就規定外部的這個自由變量爲final。
咱們能夠反編譯這個類,看看編譯器處理以後的代碼,爲:

      public void sendMessage(String msg) {
        System.out.println("event handled. " + msg);
        int j = 0;
        j = this.val$i + j;
      }

注意編譯器把j=i+j;變成了j = this.val$i + j;,只是一個外部i的拷貝。對於普通的內部類,爲何又不用呢?由於普通的內部類有構造函數,在構造函數過程當中把外部類傳進來。可是匿名內部類是沒有構造函數的。Java這樣處理,引發了多年的爭論。我我的仍是比較贊同Java團隊的作法的,這樣雖然限制不少,可是代碼安全,而且預留了之後徹底支持閉包的可能性。之後我會有專題講解閉包,此次講解只是就着匿名內部類帶一下。大家看到了,Inner Classs起步於一個簡單的模型,基本內容很簡單,可是一步步深刻,就會牽扯出高級的話題。這就有點像推倒多米諾骨牌,第一下只是很小的一個動做,後面卻又大動做等着。不少技術都是這樣的,越研討越深,不斷髮現新挑戰,這也是一種樂趣。正如咱們探索未知的山川,層巒疊嶂,接應不暇,這山望見那山高,前路永遠有精彩的風景。也正如咱們進入桃園洞口,剛開頭武陵人只是緣溪捕魚,到了半道,忽逢桃花林,復前行,林盡水源卻得一山,從口入,行數十步,豁然開朗,得此桃源仙境。學問上這種探索的樂趣,王國維先生曾言:衆裏尋他千百度,慕然回首,那人卻在,燈火闌珊處。咦!微斯人,吾誰與歸

相關文章
相關標籤/搜索