Java程序員必備基礎:內部類解析


前言

整理了一下內部類的相關知識,算是比較全,比較基礎的,希望大家一起學習進步。

一、什么是內部類?

在Java中,可以將一個類的定義放在另外一個類的定義內部,這就是內部類。內部類本身就是類的一個屬性,與其他屬性 定義方式一致。

一個內部類的例子:

public class Outer {

    private int radius = 1;
    public static int count = 2;

    public Outer() {
    }

    class inner{
        public void visitOuter() {
            System.out.println("visit outer private member variable:" + radius);
            System.out.println("visit outer static variable:" + count);
        }
    }
}

二、內部類的種類

內部類可以分為四種:成員內部類、局部內部類、匿名內部類和靜態內部類

靜態內部類

定義在類內部的靜態類,就是靜態內部類。

public class Outer {

    private static int radius = 1;

    static class StaticInner {
        public void visit() {
            System.out.println("visit outer static  variable:" + radius);
        }
    }
}

靜態內部類可以訪問外部類所有的靜態變量,而不可訪問外部類的非靜態變量;靜態內部類的創建方式,new 外部類.靜態內部類(),如下:

Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();

成員內部類

定義在類內部,成員位置上的非靜態類,就是成員內部類。

public class Outer {

    private static  int radius = 1;
    private int count =2;
    
     class Inner {
        public void visit() {
            System.out.println("visit outer static  variable:" + radius);
            System.out.println("visit outer   variable:" + count);
        }
    }
}

成員內部類可以訪問外部類所有的變量和方法,包括靜態和非靜態,私有和公有。成員內部類依賴於外部類的實例,它的創建方式外部類實例.new 內部類() ,如下:

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();

局部內部類

定義在方法中的內部類,就是局部內部類。

public class Outer {

    private  int out_a = 1;
    private static int STATIC_b = 2;

    public void testFunctionClass(){
        int inner_c =3;
        class Inner {
            private void fun(){
                System.out.println(out_a);
                System.out.println(STATIC_b);
                System.out.println(inner_c);
            }
        }
        Inner  inner = new Inner();
        inner.fun();
    }
    public static void testStaticFunctionClass(){
        int d =3;
        class Inner {
            private void fun(){
                // System.out.println(out_a); 編譯錯誤,定義在靜態方法中的局部類不可以訪問外部類的實例變量
                System.out.println(STATIC_b);
                System.out.println(d);
            }
        }
        Inner  inner = new Inner();
        inner.fun();
    }
}

定義在實例方法中的局部類可以訪問外部類的所有變量和方法,定義在靜態方法中的局部類只能訪問外部類的靜態變量和方法。局部內部類的創建方式,在對應方法內,new 內部類(),如下:

 public static void testStaticFunctionClass(){
    class Inner {
    }
    Inner  inner = new Inner();
 }

匿名內部類

匿名內部類就是沒有名字的內部類,日常開發中使用的比較多。

public class Outer {

    private void test(final int i) {
        new Service() {
            public void method() {
                for (int j = 0; j < i; j++) {
                    System.out.println("匿名內部類" );
                }
            }
        }.method();
    }
 }
 //匿名內部類必須繼承或實現一個已有的接口 
 interface Service{
    void method();
}

除了沒有名字,匿名內部類還有以下特點:

  • 匿名內部類必須繼承一個抽象類或者實現一個接口。
  • 匿名內部類不能定義任何靜態成員和靜態方法。
  • 當所在的方法的形參需要被匿名內部類使用時,必須聲明為 final。
  • 匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的接口的所有抽象方法。

匿名內部類創建方式:

new 類/接口{ 
  //匿名內部類實現部分
}

三、內部類的優點

我們為什么要使用內部類呢?因為它有以下優點:

  • 一個內部類對象可以訪問創建它的外部類對象的內容,包括私有數據!
  • 內部類不為同一包的其他類所見,具有很好的封裝性;
  • 內部類有效實現了“多重繼承”,優化 java 單繼承的缺陷。
  • 匿名內部類可以很方便的定義回調。

一個內部類對象可以訪問創建它的外部類對象的內容,包括私有數據!

public class Outer {

    private  int radius = 1;
    
    protected void test(){
        System.out.println("我是外部類方法");
    }

    class Inner {
        public void visit() {
            System.out.println("訪問外部類變量" + radius);
            test();
        }
    }
}

我們可以看到,內部類Inner是可以訪問外部類Outer的私有變量radius或者方法test的。

內部類不為同一包的其他類所見,具有很好的封裝性

當內部類使用 private修飾時,這個類就對外隱藏了。當內部類實現某個接口,並且進行向上轉型,對外部來說,接口的實現已經隱藏起來了,很好體現了封裝性。

//提供的接口
interface IContent{
    String getContents();
}

public class Outer {
     //私有內部類屏蔽實現細節
     private class PContents implements IContent{
         @Override
         public String getContents() {
             System.out.println("獲取內部類內容");
             return "內部類內容";
         }
     }

    //對外提供方法
    public IContent getIContent() {
        return new PContents();
    }

    public static void main(String[] args) {
        Outer outer=new Outer();
        IContent a1=outer.getIContent();
        a1.getContents();
    }
}

我們可以發現,Outer外部類對外提供方法getIContent,用內部類實現細節,再用private修飾內部類,屏蔽起來,把Java的封裝性表現的淋漓盡致。

內部類有效實現了“多重繼承”,優化 java 單繼承的缺陷。

我們知道Java世界中,一個類只能有一個直接父類,即以單繼承方式存在。但是內部類讓“多繼承”成為可能:

  • 一般來說,內部類繼承某個類或者實現某個接口,內部類的代碼操作創建它的外圍類的對象。內部類提供了某種進入其外圍類的窗口。
  • 每個內部類都可以隊里的繼承自一個(接口的)實現,所以無論外圍類是否已經繼承了某個(接口的)實現,對於內部類沒有影響
  • 接口解決了部分問題,一個類可以實現多個接口,內部類允許繼承多個非接口類型(類或抽象類)。

一份來自Java編程思想,內部類實現“多繼承”的溫暖如下:

class D {}
abstract class E{}
class Z extends D {
E makeE(){ return new E() {}; }
}

public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args){
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
} 

代碼中出現了一個類D,一個抽象類E。然后,用類Z繼承D,內部類構造返回E。因此,當你不管要的是D還是E,Z都可以應付,“多繼承”的特點完美表現出來。

匿名內部類可以很方便的定義回調。

什么是回調?假設有兩個類A和B,在A中調用B的一個方法b,而b在執行又調用了A的方法c,則c就稱為回調函數。


當然,回調函數也可以是a函數,這就是同步回調,最簡單的回調方式。
回調應用場景挺多的,如android中的事件監聽器。匿名內部類可以很方便的定義回調,看個例子

//定義一個CallBack接口
public interface CallBack {
    void execute();
}

public class TimeTools {

    /**
     * 測試函數調用時長,通過定義CallBack接口的execute方法
     * @param callBack
     */
    public   void  testTime(CallBack callBack) {
        long  beginTime = System.currentTimeMillis(); //記錄起始時間
        callBack.execute(); ///進行回調操作
        long  endTime = System.currentTimeMillis(); //記錄結束時間
        System.out.println("[use time]:"  + (endTime - beginTime)); //打印使用時間
    }

    public   static   void  main(String[] args) {
        TimeTools tool = new  TimeTools();
        tool.testTime(new  CallBack(){
            //匿名內部類,定義execute方法
            public   void  execute(){
                TestTimeObject testTimeObject = new TestTimeObject();
                testTimeObject.testMethod();
            }
        });
    }
}

在調用testTime()測時間的時候,用匿名內部類實現一個方法execute(),在該方法內搞事情(執行目標函數),執行完后,又回到testTime方法,很好了實現測試函數調用時長的功能。顯然,匿名內部類讓回調實現變得簡單

四、內部類的底層

內部類標志符

每個內部類都會產生一個.class文件,其中包含了如何創建該類型的對象的全部信息。內部類也必須生成一個.class文件以包含它們的Class對象信息。內部類文件的命名有嚴格規則:外圍類的名字+$+內部類的名字。

一個簡單例子:

public class Outer {
    class Inner{
    }
}

javac Outer.java編譯完成后, 生成的class文件如下:

如果內部類是匿名的,編譯器會簡單地產生一個數字作為其標識符。如果內部類是嵌套在別的內部類之中(靜態內部類),只需直接將它們的名字加在其外圍類標志符與“$”的后面。

為什么內部類可以訪問外部類的成員,包括私有數據?

由上一小節,我們知道內部類可以訪問外部類的成員,包括私有數據。那么它是怎么做到的呢?接下來揭曉答案。

先看這個簡單地例子:

public class Outer {

    private int i = 0;
    
    class Inner{
        void method(){
            System.out.println(i);
        }
    }
}

一個外部類Outer,一個外部類私有屬性i,一個內部類Inner,一個內部類方法method。內部類方法訪問了外部類屬性i。

先編譯,javac Outer.java,生成.class文件,如下:

javap -classpath . -v Outer$Inner,反編譯Outter$Inner.class文件得到以下信息:

我們可以看到這一行,它是一個指向外部類對象的指針:

final innerclass.Outer this$0;

雖然編譯器在創建內部類時為它加上了一個指向外部類的引用, 但是這個引用是怎樣賦值的呢?編譯器會為內部類的構造方法添加一個參數,進行初始化, 參數的類型就是外部類的類型,如下:

innerclass.Outer$Inner(innerclass.Outer);

成員內部類中的Outter this&0 指針便指向了外部類對象,因此可以在成員內部類中隨意訪問外部類的成員。

局部內部類和匿名內部類訪問局部變量的時候,為什么變量必須要加上final?

局部內部類和匿名內部類訪問局部變量的時候,為什么變量必須要加上final呢?它內部原理是什么呢?

先看這段代碼:

public class Outer {

    void outMethod(){
        final int a =10;
        class Inner {
            void innerMethod(){
                System.out.println(a);
            }

        }
    }
}

反編譯(Outer$1Inner)得到以下信息

我們在內部類innerMethod方法中,可以看到以下這條指令:

3: bipush   10
  • 它表示將常量10壓入棧中,表示使用的是一個本地局部變量。
  • 其實,如果一個變量的值在編譯期間可以確定(demo中確定是10了),則編譯器會默認在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的字節碼嵌入到執行字節碼中。
  • 醬紫可以確保局部內部類使用的變量與外層的局部變量區分開,它們只是值相等而已。

以上例子,為什么要加final呢?是因為生命周期不一致, 局部變量直接存儲在棧中,當方法執行結束后,非final的局部變量就被銷毀。而局部內部類對局部變量的引用依然存在,如果局部內部類要調用局部變量時,就會出錯。加了final,可以確保局部內部類使用的變量與外層的局部變量區分開,解決了這個問題。

我們再來看一段代碼,其實就是把變量a挪到傳參方式進來

public class Outer {

    void outMethod(final int a){
        class Inner {
            void innerMethod(){
                System.out.println(a);
            }
        }
    }
}

反編譯可得

我們看到匿名內部類Outer$1Inner的構造器含有兩個參數,一個是指向外部類對象的引用,一個是int型變量,很顯然,這里是將變量innerMethod方法中的形參a以參數的形式傳進來對匿名內部類中的拷貝(變量a的拷貝)進行賦值初始化。

那么,新的問題又來了,既然在innerMethod方法中訪問的變量a和outMethod方法中的變量a不是同一個變量,當在innerMethod方法中修改a會怎樣?那就會造成數據不一致的問題了。

怎么解決呢?使用final修飾符,final修飾的引用類型變量,不允許指向新的對象,這就解決數據不一致問題。注意: 在Java8 中,被局部內部類引用的局部變量,默認添加final,所以不需要添加final關鍵詞。

五、內部類的應用場景。

一般我們在哪些場景下使用內部類呢?

場景之一:一些多算法場合

一些算法多的場合,也可以借助內部類,如:

Arrays.sort(emps,new Comparator(){
  Public int compare(Object o1,Object o2)
  {
   return ((Employee)o1).getServedYears()-((Employee)o2).getServedYears();
  }
});

場景二:解決一些非面向對象的語句塊。

如果一些語句塊,包括if…else語句,case語句等等比較多,不好維護擴展,那么就可以借助內部類+設計模式解決。

場景之三:適當使用內部類,使得代碼更加靈活和富有擴展性。

適當的使用內部類,可以使得你的代碼更加靈活和富有擴展性。如JDK的lamda表達式,用內部類非常多,代碼優雅很多。如下

// JDK8 Lambda表達式寫法
new Thread(() -> System.out.println("Thread run()")).start();

場景四:當某個類除了它的外部類,不再被其他的類使用時。

如果一個類,不能為其他的類使用;或者出於某種原因,不能被其他類引用。那我們就可以考慮把它實現為內部類。數據庫連接池就是這樣一個典型例子。

六、內部類常見面試題

最后,我們來看一道經典內部類面試題吧。

public class Outer {
    private int age = 12;

    class Inner {
        private int age = 13;
        public void print() {
            int age = 14;
            System.out.println("局部變量:" + age);
            System.out.println("內部類變量:" + this.age);
            System.out.println("外部類變量:" + Outer.this.age);
        }
    }

    public static void main(String[] args) {
        Outer.Inner in = new Outer().new Inner();
        in.print();
    }

}

運行結果:

參考與感謝

個人公眾號

  • 如果你是個愛學習的好孩子,可以關注我公眾號,一起學習討論。
  • 如果你覺得本文有哪些不正確的地方,可以評論,也可以關注我公眾號,私聊我,大家一起學習進步哈。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM