1. 程式人生 > >重新認識java(九) ---- 內部類

重新認識java(九) ---- 內部類

注意注意!!!前排提示!!!本篇文章過長,最好收藏下來慢慢看,如果你之前對內部類不是很熟悉,一次性看完,大概你會懵逼。。。

1. 內部類概述

一個類的定義放在另一個類的內部,這個類就叫做內部類。內部類是一種非常有用的特性,因為它允許你把一些邏輯相關的類組織在一起。

內部類大體上可以分為四種:

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

我們先來詳細的看一下這四種內部類。

2. 成員內部類

成員的內部類,就是最基礎的內部類,沒有那些花裡胡哨的修飾:

//外部類
public class Outer {
    private String a = "a"
; public int i = 1; //內部類 class Inner{ private String b = "b"; public String c = "c"; public int getInt(){ return i; // 內部類可以訪問外部類變數 } private String getString(){ return a + b + c; // 內部類可以訪問外部類的private變數 } } public
String getParam(){ Inner inner = new Inner(); inner.b = "bb"; // 外部類可以訪問內部類的private變數 inner.c = "cc"; return inner.getInt() + inner.getString(); } } //測試類 class Test { public static void main(String[] args) { Outer outer = new Outer(); System.out.println(outer.getParam()); // 輸出:1abbcc
Outer.Inner oi = outer.new Inner(); oi.c = "ccc"; //oi.b = "bbb"; 編譯失敗 System.out.println(oi.getInt()); // 輸出:1 //System.out.println(oi.getString()); 編譯失敗 } }

從這段程式碼中,我們總結一下普通內部類的要點:

  • 內部類可以訪問外部類變數,包括私有變數

  • 在外部類中使用內部類的方法需要new一個內部類的物件。

  • 在外部類中可以訪問到內部類的任何變數,包括私有變數。

  • 在其他類中建立內部類物件需要使用這樣的形式:
    OuterClassName.InnerClassName name = new OuterClassName().new InnerClassName()。

  • 在其他類中定義的內部類物件不能訪問內部類中的私有變數。

當然,除了以上知識點以外,在內部類中,可以通過【.this】訪問到外部類物件。

public class Outer {
    private int num ;
    public Outer(){}

    public Outer(int num){
        this.num = num;
    }

    private class Inner{
        public Outer getTest2(){
            return Outer.this; // Outer.this指的是外部類的物件
        }

        public Outer newTest2(){
            return new Outer();
        }
    }

    public static void main(String [] args){
        Outer test = new Outer(5);
        Outer.Inner inner = test.new Inner();
        Outer o1 = inner.getTest2();
        Outer o2 = inner.newTest2();
        System.out.println(o1.num); // 5
        System.out.println(o2.num); // 0
    }
}

注意通過.this得到的物件,和通過new出來的物件的區別。使用.this後,得到時建立該內部類時使用的外圍類物件的引用,new則是建立了一個新的引用。

到這裡了我們需要明確一點,內部類是個編譯時的概念,一旦編譯成功後,它就與外圍類屬於兩個完全不同的類(當然他們之間還是有聯絡的)。對於一個名為OuterClass的外圍類和一個名為InnerClass的內部類,在編譯成功後,會出現這樣兩個class檔案:OuterClass.class和OuterClass$InnerClass.class。

3. 內部類與向上轉型

到目前為止,你可能覺得內部類不過如此,沒什麼新奇的地方,畢竟如果只是為了隱藏一個類,java本身已經有了很好的隱藏機制——只給某個類包訪問許可權,而用不著把類建立為內部類。

但是,當一個內部類向上轉型為其基類,尤其是轉型為一個介面的時候,內部類就有了用武之地。這是因為這樣的內部類(某個介面的實現類)對於其他人來說完全不可見,並且不可用。所得到的只是指向基類或者介面的引用,所以能很方便的隱藏實現細節。

我們來看一段程式碼:

//定義兩個介面
public interface Run {
    void run();
}
public interface Eat {
    void eat();
}
//外部類
public class Person {
    //這裡是private
    private class PEat implements Eat {
        @Override
        public void eat() {
            System.out.println("eat with mouse");
        }
    }

    //這裡是protected
    protected class PRun implements Run{
        @Override
        public void run() {
            System.out.println("run with leg");
        }
    }

    public Eat howToEat(){
        return new PEat(); //向上轉型
    }

    public Run houToRun(){
        return new PRun(); //向上轉型
    }
}
//測試類
class TestPerson{
    public static void main(String[] args) {
        Person p = new Person();
        Eat e = p.howToEat();
        Run r = p.houToRun();

        e.eat();
        r.run();

        Person.PRun ppr = p.new PRun();
        //Person.PEat ppe = p.new PEat(); 編譯失敗,因為PEat是private的
    }
}

從這段程式碼可以看出,PEat是private,所以除了Person(它的外部類),沒有人能訪問到它。PRun是protected,所以只有Person及其子類、還有與Person同一個包中的類能訪問PRun,其他類不能訪問。

這意味著,如果客戶端程式設計師想要了解或者訪問這些成員是要受到限制的。除此之外,private內部類也不可以被向下轉型,因為不能訪問他的名字。

所以,private內部類給類的設計者提供了一種途徑,通過這樣的方式可以完全阻止任何依賴於類的編碼,並且完全隱藏了實現的細節。此外,從客戶端程式設計師的角度看,由於不能訪問任何新增加的、原本不屬於公共介面的方法,所以擴充套件介面是沒有價值的。這也個java編譯器提供了更高效程式碼的機會。

所以說,一般成員內部類,都會定義為private的。

普通的類(非內部類),不能宣告為private或protected,它們之恩給你被賦予public或者包訪問許可權。

4. 靜態內部類(巢狀類)

如果不需要內部類物件與其外圍類物件之間又聯絡,那麼可以將內部類宣告為static。這就是靜態內部類,也被稱為巢狀類。

普通的內部類物件隱式的儲存了一個指向它的外部類引用的變數,所以它可以無條件的使用外部類的變數。但是當內部類yogastatic修飾的時候,就不會有這個變量了。這意味著:

  • 要建立巢狀類的物件,並不需要其外圍類的物件。
  • 靜態內部類中不能訪問非靜態的外部類變數,但是尅訪問外部類的靜態變數。

除此之外,由於普通內部類的欄位與方法,只能放在類的外部層次上,所以普通的內部類不能有static方法和static變數,也不能在普通內部類中再包含靜態內部類。但是靜態內部類可以包含所有這些東西:

public class Outer {
    private int i = 1;
    public static String str = "str";

    static class StaClass implements inter{
        private String s = "s";
        static int j = 2;

        static int getInt(){
            //return i + j;
            return j;
        }

        private String getString(){
            return str + s;
        }

        @Override
        public void inter() {
            System.out.println("inter");
        }

        static class InStaClass{
            int x = 4;
            static int y = 5;
             static int getInt(){
                //return x; // x是非靜態變數 不可以在靜態方法中使用
                return y;
            }
        }
    }

    public inter getInter(){
        return new StaClass();
    }
}

class Test{
    public static void main(String[] args) {
        int a = Outer.StaClass.getInt();

        //Outer.StaClass.getString(); // getString()為非靜態方法,不能這樣呼叫

        int b = Outer.StaClass.InStaClass.getInt();

        System.out.println(a + "----" + b); // 輸出 2----5

        //new Outer().new StaClass(); 編譯失敗 StaClass是靜態的

        new Outer().getInter().inter(); // 輸出 inter


    }
}

通過這段程式碼,我們總結一下靜態內部類的要點:

  • 在靜態內部類中可以存在靜態成員

  • 靜態內部類只能訪問外圍類的靜態成員變數和方法,不能訪問外圍類的非靜態成員變數和方法

  • 靜態內部類中的靜態方法可以通過【外部類.內部類.方法名】直接呼叫

  • 靜態內部類在其他類中不能new出來。(new Outer().new StaClass()這樣是不行的)

  • 但是在外部類中,可以new一個靜態內部類的物件。

  • 靜態內部類中不能使用【.this】

5. 區域性內部類

在一個方法裡或者任意作用域裡定義的內部類叫做區域性內部類。

為什麼要這麼做呢?

如前所示,你實現了某型別的介面,於是你可以建立並返回對其的引用,你需要這樣的引用。
你要解決一個複雜的問題,想建立一個類來輔助你的解決方案,但是又不希望這個類是公共可用的。

聽起來有點費解,往下看你就明白了。

5.1 一個定義在方法中的類

public class Person {
    public Eat howToEat(){
        // 定義在方法中的類
        class EatWithMouth implements Eat{
            @Override
            public void eat() {
                System.out.println("eat with mouth");
            }
        }
        // 向上轉型
        return new EatWithMouth();
    }

    public static void main(String[] args) {
        Eat e = new Person().howToEat();
        e.eat(); // eat with mouth
    }
}

EatWithMouth是方法howToEat中的類而不是Person中的類。你甚至可以在同一個子目錄下的任意一個類中給任意一個內部類起EatWithMouth這個名字,而不會有命名衝突。所以,在howToEat方法外的任何地方都不能訪問到EatWithMouth類。

當然,這並不意味著一旦howToEat方法執行完畢,EatWithMouth類就不能用了。

5.2 在任意作用域嵌入一個內部類

可以在任意作用域中嵌入內部類:

public class EveryBlock {
    private String test(boolean b){
        if (b){
            class A{
                private String a = "a";
                String getString(){
                    return a;
                }
            }
            A a = new A();
            String s = a.getString();
            return s;
        }
        //A a = new A();  編譯失敗 超出作用域
        return null;
    }

    public static void main(String[] args) {
        EveryBlock eb = new EveryBlock();
        System.out.println(eb.test(true)); // a
    }
}

雖然類A在if語句中,但是這並不表明類A的建立時有條件的,它其實是和別的類一起編譯的。但是它在它定義的作用域之外的不可用的,除此之外與普通內部類一樣。

通過這樣的方式,就解決了上面提到的第二個問題:不希望這個類是公用的。

6. 匿名內部類

匿名內部類應該是使用的最多的了,尤其是在swing中。

先看一個例子:

public class OuterClass {
    public InnerClass getInnerClass(final int num,String str2){
        return new InnerClass(){
            int number = num + 3;
            public int getNumber(){
                return number;
            }
        };//注意:分號不能省
    }

    public static void main(String[] args) {
        OuterClass out = new OuterClass();
        InnerClass inner = out.getInnerClass(2, "chengfan");
        System.out.println(inner.getNumber());
    }
}

interface InnerClass {
    int getNumber();
}

這段程式碼裡有一段很奇怪的東西:

        return new InnerClass(){
            int number = num + 3;
            public int getNumber(){
                return number;
            }
        };

沒錯,就是它。InnerClass不是一個藉口麼,怎麼還能new呢?聰明的你一定知道,這就是匿名內部類,事實上,這段程式碼和下面的寫法是等價的:

public class OuterCla {

    class InnerClassImpl implements InnerClass{
        int number ;
        public InnerClassImpl(int num){
            number = num + 3;
        }
        public int getNumber(){
            return number;
        }
    }
    public InnerClass getInnerClass(final int num){
        return new InnerClassImpl(2);
    }

    public static void main(String[] args) {
        OuterCla out = new OuterCla();
        InnerClass inner = out.getInnerClass(2);
        System.out.println(inner.getNumber());
    }
}

這段程式碼你應該懂了。將兩段程式碼一比較,你大概也清楚了,上面那樣寫,意思是建立了一個實現了InnerClass的匿名類的物件。

匿名類可以建立介面、抽象類、與普通類的物件。建立介面和抽象類時,必須實現介面中所有方法。 建立匿名類時,可以是無參的,也可以有引數的,但是如果這個引數要在匿名類中使用,引數必須是final的,如果不使用,可以不被final修飾(程式碼中有體現)。

6.1 為什麼必須是final呢?

首先我們知道在內部類編譯成功後,它會產生一個class檔案,該class檔案與外部類並不是同一class檔案,僅僅只保留對外部類的引用。當外部類傳入的引數需要被內部類呼叫時,從java程式的角度來看是直接被呼叫:

public class OuterClass {
    public void display(final String name,String age){
        class InnerClass{
            void display(){
                System.out.println(name);
            }
        }
    }
}

從上面程式碼中看好像name引數應該是被內部類直接呼叫?其實不然,在java編譯之後實際的操作如下:

public class OuterClass$InnerClass {
    public InnerClass(String name,String age){
        this.InnerClass$name = name;
        this.InnerClass$age = age;
    }


    public void display(){
        System.out.println(this.InnerClass$name + "----" + this.InnerClass$age );
    }
}

所以從上面程式碼來看,內部類並不是直接呼叫方法傳遞的引數,而是利用自身的構造器對傳入的引數進行備份,自己內部方法呼叫的實際上時自己的屬性而不是外部方法傳遞進來的引數。

直到這裡還沒有解釋為什麼是final?在內部類中的屬性和外部方法的引數兩者從外表上看是同一個東西,但實際上卻不是,所以他們兩者是可以任意變化的,也就是說在內部類中我對屬性的改變並不會影響到外部的形參,而然這從程式設計師的角度來看這是不可行的,畢竟站在程式的角度來看這兩個根本就是同一個,如果內部類該變了,而外部方法的形參卻沒有改變這是難以理解和不可接受的,所以為了保持引數的一致性,就規定使用final來避免形參的不改變。

簡單理解就是,拷貝引用,為了避免引用值發生改變,例如被外部類的方法修改等,而導致內部類得到的值不一致,於是用final來讓該引用不可改變。

故如果定義了一個匿名內部類,並且希望它使用一個其外部定義的引數,那麼編譯器會要求該引數引用是final的。

6.2匿名內部類小結

  • 匿名內部類是沒有訪問修飾符的。

    匿名內部類中不能存在任何的靜態成員變數和靜態方法。

  • new 匿名內部類,這個類首先是要存在的。如果我們將那個InnerClass介面註釋掉,就會出現編譯出錯。

  • 當所在方法的形參需要被匿名內部類使用,那麼這個形參就必須為final。

  • 匿名內部類建立一個介面的引用時是沒有構造方法的。但是可以通過構造程式碼塊來模擬構造器,像下面這樣:
public A getA(){  
    return new A(){  
        int num = 0;  
        String str;  
        {  
            str = "這是構造程式碼塊!";  
            System.out.println("str 已經被初始化!");  
        }  
    };  
} 
  • 但是當匿名內部類建立一個抽象類或者實體類的引用時,如果有必要,是可以定義建構函式的:
public class Outer { 
    public static void main(String[] args) { 
        Outer outer = new Outer(); 
        Inner inner = outer.getInner("Inner", "gz"); 
        System.out.println(inner.getName()); 
    } 

    public Inner getInner(final String name, String city) { 
        return new Inner(name, city) { 
            private String nameStr = name; 

            public String getName() { 
                return nameStr; 
            } 
        }; 
    } 
} 

abstract class Inner { 
    Inner(String name, String city) { 
        System.out.println(city); 
    } 

    abstract String getName(); 
} 
//注意這裡的形參city,由於它沒有被匿名內部類直接使用,而是被抽象類Inner的建構函式所使用,所以不必定義為final。
  • 匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的介面的所有抽象方法。

事實上,建立匿名內部類要寫的模板程式碼太多了,java8中的lambda表示式能夠替代大部分的匿名類,優雅簡潔程式碼少,所以建議大家學習java8,當然,匿名內部類的知識還是要掌握的。

7.內部類的繼承

內部類的繼承,是指內部類被繼承,普通類 extents 內部類。而這時候程式碼上要有點特別處理,具體看以下例子:

public class InheritInner extends WithInner.Inner { 

    // InheritInner() 是不能通過編譯的,一定要加上形參 
    InheritInner(WithInner wi) { 
        wi.super(); 
    } 

    public static void main(String[] args) { 
        WithInner wi = new WithInner(); 
        InheritInner obj = new InheritInner(wi); 
    } 
} 

class WithInner { 
    class Inner { 

    } 
} 

可以看到子類的建構函式裡面要使用父類的外部類物件.super();而這個物件需要從外面建立並傳給形參。

8. 多重繼承

內部類是除了介面外實現多重繼承的又一有利工具。

利用介面實現多重繼承我們都知道,就是一次性實現很多介面。那麼,如何利用內部類實現多重繼承呢?

看程式碼:

//父親
public class Father {
    public int strong(){
        return 9;
    }
}
//母親
public class Mother {
    public int kind(){
        return 8;
    }
}
//兒子
public class Son {

    /**
     * 內部類繼承Father類
     */
    class Father_1 extends Father{
        public int strong(){
            return super.strong() + 1;
        }
    }

    class Mother_1 extends  Mother{
        public int kind(){
            return super.kind() - 2;
        }
    }

    public int getStrong(){
        return new Father_1().strong();
    }

    public int getKind(){
        return new Mother_1().kind();
    }
}

public class Test1 {

    public static void main(String[] args) {
        Son son = new Son();
        System.out.println("Son 的Strong:" + son.getStrong());
        System.out.println("Son 的kind:" + son.getKind());
    }

}

//輸出
//Son 的Strong:10
//Son 的kind:6

兒子繼承了父親,變得比父親更加強壯,同時也繼承了母親,只不過溫柔指數下降了。這裡定義了兩個內部類,他們分別繼承父親Father類、母親類Mother類,且都可以非常自然地獲取各自父類的行為,這是內部類一個重要的特性:內部類可以繼承一個與外部類無關的類,保證了內部類的獨立性,正是基於這一點,多重繼承才會成為可能。

9. 內部類的原理簡析

上面說過這樣兩點:

(1) 在外部類的作用範圍內可以任意建立內部類物件,即使內部類是私有的(私有內部類)。即內部類對包圍它的外部類可見。

(2) 在內部類中可以訪問其外部類的所有域,即使是私有域。即外部類對內部類可見。

問題來了:上面兩個特點到底如何辦到的呢?內部類的”內部”到底發生了什麼?

其實,內部類是Java編譯器一手操辦的。虛擬機器並不知道內部類與常規類有什麼不同。 編譯器是如何瞞住虛擬機器的呢?

我們用javac命令編譯一下下面的程式碼:

class Outer{   
       //外部類私有資料域   
       private int data=0;   
       //內部類   
       class Inner{   
           void print(){   
                 //內部類訪問外部私有資料域   
                 System.out.println(data);   
           }    
       }   
}  

可以看到這樣的結果:

這裡寫圖片描述

對內部類進行編譯後發現有兩個class檔案:Outer.class 、和Outer$Inner.class 。這說明內部類Inner仍然被編譯成一個獨立的類(Outer$Inner.class),而不是Outer類的某一個域。 虛擬機器執行的時候,也是把Inner作為一種常規類來處理的。

但問題又來了,即然是兩個常規類,為什麼他們之間可以互相訪問私有域那(最開始提到的兩個內部類特點)?這就要問問編譯器到底把這兩個類編譯成什麼東西了。

我們利用reflect反射機制來探查了一下內部類編譯後的情況:

//反編譯後的Outer$Inner
class Outer$Inner{   
        Outer$Inner(Outer,Outer$Inner);  //包可見構造器   
        private Outer$Inner(Outer);   //私有構造器將設定this$0域   
        final Outer this$0;   //外部類例項域this$0  
} 

好了,現在我們可以解釋上面的第一個內部類特點了: 為什麼外部類可以建立內部類的物件?並且內部類能夠方便的引用到外部類物件?

首先編譯器將外、內部類編譯後放在同一個包中。在內部類中附加一個包可見構造器。這樣, 虛擬機器執行Outer類中Inner in=new Inner(); 實際上呼叫的是包可見構造: new Outer$Inner(this,null)。因此即使是private內部類,也會通過隱含的包可見構造器成功的獲得私有內部類的構造許可權。

再者,Outer$Inner類中有一個指向外部類Outer的引用this$0,那麼通過這個引用就可以方便的得到外部類物件中可見成員。但是Outer類中的private成員是如何訪問到的呢?這就要看看下面Outer.class檔案中的祕密了。

class Outer{   
    static int access$0(Outer);  //靜態方法,返回值是外部類私有域 data 的值。   
}  

現在可以解釋第二個特點了:為什麼內部類可以引用外部類的私有域?

原因的關鍵就在編譯器在外圍類中添加了靜態方法access$0。 它將返回值作為引數傳遞給他的物件域data。這樣內部類Inner中的列印語句:System.out.println(data); 實際上執行的時候呼叫的是:System.out.println(this$0.access$0(Outer));

總結一下編譯器對類中內部類做的手腳吧:

  • (1) 在內部類中偷偷摸摸的建立了包可見構造器,從而使外部類獲得了建立許可權。

  • (2) 在外部類中偷偷摸摸的建立了訪問私有變數的靜態方法,從而 使 內部類獲得了訪問許可權。這樣,類中定義的內部類無論私有,公有,靜態都可以被包圍它的外部類所訪問。

反射的知識,只會會講,IDEA自帶將.class檔案反射的功能,但是它的功能強大到可以把Outer類完整的還原。。。

10. 總結

本篇文章過長,總結完以後也是身心俱疲,所以就不多囉嗦了。文章有什麼錯誤或者不足,請及時與我聯絡。

看完了,如果對你有幫助,隨手點個讚唄~~~