天天看點

Java 幹貨之深入了解Java内部類

可以将一個類定義在另一個類或方法中,這樣的類叫做内部類 --《Thinking in Java》

說起内部類,大家并不陌生,并且會經常在執行個體化容器的時候使用到它。但是内部類的具體細節文法,原理以及實作是什麼樣的可以不少人都還挺陌生,這裡作一篇總結,希望通過這篇總結提高對内部類的認識。

内部類是什麼?

由文章開頭可知,内部類的定義為:定義在另一個類或方法中的類。而根據使用場景的不同,内部類還可以分為四種:成員内部類,局部内部類,匿名内部類和靜态内部類。每一種的特性和注意事項都不同,下面我們一一說明。

成員内部類

顧名思義,成員内部類是定義在類内部,作為類的成員的類。如下:

public class Outer {
    
   public class Inner{
       
   }

}

           

特點如下:

  1. 成員内部類可以被權限修飾符(eg.

    public,private等

    )所修飾
  2. 成員内部類可以通路外部類的所有成員,(包括

    private

    )成員
  3. 成員内部類是預設包含了一個指向外部類對象的引用
  4. 如同使用

    this

    一樣,當成員名或方法名發生覆寫時,可以使用外部類的名字加.this指定通路外部類成員。如:

    Outer.this.name

  5. 成員内部類不可以定義

    static

    成員
  6. 成員内部類建立文法:
Outer outer=new Outer();
Outer.Inner inner=outer.new Inner();
           

局部内部類

局部内部類是定義在方法或者作用域中類,它和成員内部類的差別僅在于通路權限的不同。

public class Outer{
    public void test(){
        class Inner{
            
        }
    }
}
           
  1. 局部内部類不能有通路權限修飾符
  2. 局部内部類不能被定義為

    static

  3. 局部内部類不能定義

    static

  4. 局部内部類預設包含了外部類對象的引用
  5. 局部内部類也可以使用

    Outer.this

    文法制定通路外部類成員
  6. 局部内部類想要使用方法或域中的變量,該變量必須是

    final

    在JDK1.8 以後,沒有

    final

    修飾,

    effectively final

    的即可。什麼意思呢?就是沒有

    final

    修飾,但是如果加上

    final

    編譯器也不會報錯即可。

匿名内部類

匿名内部類是與繼承合并在一起的沒有名字的内部類

public class Outer{
    public List<String> list=new ArrayList<String>(){
        {
            add("test");
        }
    };
}
           

這是我們平時最常用的文法。

匿名内部類的特點如下:

  1. 匿名内部類使用單獨的塊表示初始化塊

    {}

  2. 匿名内部類想要使用方法或域中的變量,該變量必須是

    final

    修飾的,JDK1.8之後

    effectively final

    也可以
  3. 匿名内部類預設包含了外部類對象的引用
  4. 匿名内部類表示繼承所依賴的類

嵌套類

嵌套類是用

static

修飾的成員内部類

public class Outer {
    
   public static class Inner{
       
   }

}
           
  1. 嵌套類是四種類中唯一一個不包含對外部類對象的引用的内部類
  2. 嵌套類可以定義

    static

  3. 嵌套類能通路外部類任何靜态資料成員與方法。
    構造函數可以看作靜态方法,是以可以通路。

為什麼要有内部類?

從上面可以看出,内部類的特性和類方差不多,但是内部類有許多繁瑣的細節文法。既然内部類有這麼多的細節要注意,那為什麼Java還要支援内部類呢?

1. 完善多重繼承
  1. 在早期C++作為面向對象程式設計語言的時候,最難處理的也就是多重繼承,多重繼承對于代碼耦合度,代碼使用人員的了解來說,并不怎麼友好,并且還要比較出名的死亡菱形的多重繼承問題。是以Java并不支援多繼承。
  2. 後來,Java設計者發現,沒有多繼承,一些代碼友好的設計與程式設計問題變得十分難以解決。于是便産生了内部類。内部類具有:隐式包含外部類對象并且能夠與之通信的特點,完美的解決了多重繼承的問題。
2. 解決多次實作/繼承問題
  1. 有時候在一個類中,需要多次通過不同的方式實作同一個接口,如果沒有内部類,必須多次定義不同數量的類,但是使用内部類可以很好的解決這個問題,每個内部類都可以實作同一個接口,即實作了代碼的封裝,又實作了同一接口不同的實作。
  2. 内部類可以将組合的實作封裝在内部中。

為什麼内部類的文法這麼繁雜

這一點是本文的重點。内部類文法之是以這麼繁雜,是因為它是新資料類型加文法糖的結合。想要了解内部類,還得從本質上出發.

内部類根據應用場景的不同分為4種。其應用場景完全可以和類方法對比起來。

下面我們通過類方法對比的模式一一解答為什麼内部類會有這樣的特點

成員内部類——>成員方法

成員内部類的設計完全和成員方法一樣。

調用成員方法:

outer.getName()

建立内部類對象:

outer.new Inner()

它們都是要依賴對象而被調用。

正如《Thinking in Java》所說,

outer.getName()

正真的形似是

Outer.getName(outer)

,也就是将調用對象作為參數傳遞給方法。

建立一個内部類也是這樣:

Outer.new Inner(outer)

下面,我們用實際情況證明:

建立一個包含内部類的類:

public class Outer {

    private int m = 1;

    public class Inner {
    
        private void test() {
            //通路外部類private成員
            System.out.println(m);
        }
    }
}
           

編譯,會發現會在編譯目标目錄生成兩個.class檔案:

Outer.class

Outer$Inner.class

PS:不知道為什麼Java總是和$過不去,就連變量命名規則都要比C++多一個能由$組成 :)

Outer$Inner.class

放入IDEA中打開,會自動反編譯,檢視結果:

public class Outer$Inner {
    public Outer$Inner(Outer this$0) {
        this.this$0 = this$0;
    }

    private void test() {
        System.out.println(Outer.access$000(this.this$0));
    }
}
           

可以看見,編譯器已經自動生成了一個預設構造器,這個預設構造器是一個帶有外部類型引用的參數構造器。

可以看到外部類成員對象的引用:Outer是由

final

修飾的。

是以:

  1. 成員内部類作為類級成員,是以能被通路修飾符所修飾
  2. 成員内部類中包含建立内部類時對外部類對象的引用,是以成員内部類能通路外部類的所有成員。
  3. 文法規定:因為它作為外部類的一部分成員,是以即使

    private

    的對象,内部類也能通路。。通過Outer.access$ 指令通路
  4. 如同非靜态方法不能通路靜态成員一樣,非靜态内部類也被設計的不能擁有靜态變量,是以内部類不能定義

    static

    對象和方法。
但是可以定義

static final

變量,這并不沖突,因為所定義的

final

字段必須是編譯時确定的,而且在編譯類時會将對應的變量替換為具體的值,是以在JVM看來,并沒有通路内部類。

局部内部類——> 局部代碼塊

局部内部類可以和局部代碼塊相了解。它最大的特點就是隻能通路外部的

final

變量。

先别着急問為什麼。

定義一個局部内部類:

public class Outer {

    private void test() {

        int  m= 3;
        class Inner {
            private void print() {
                System.out.println(m);
            }
        }
    }

}
           

編譯,發現生成兩個.class檔案

Outer.class

Outer$1Inner.class

Outer$1Inner.class

放入IDEA中反編譯:

class Outer$1Inner {
    Outer$1Inner(Outer this$0, int var2) {
        this.this$0 = this$0;
        this.val$m = var2;
    }

    private void print() {
        System.out.println(this.val$m);
    }
}

           

可以看見,編譯器自動生成了帶有兩個參數的預設構造器。

看到這裡,也許應該能明了:我們将代碼轉換下:

public class Outer {
    private void test() {
        int  m= 3;
        Inner inner=new Outer$1Inner(this,m);
        
        inner.print();
        }
    }

}
           

也就是在Inner中,其實是将m的值,拷貝到内部類中的。

print()

方法隻是輸出了m,如果我們寫出了這樣的代碼:

private void test() {

        int  m= 3;

        class Inner {

            private void print() {
               m=4;
            }
        }
        
       System.out.println(m);  
    }
           

在我們看來,m的值應該被修改為4,但是它真正的效果是:

private void test(){
    int m = 3;
    
    print(m);
    
    System.out.println(m);
}

private void print(int m){
    m=4;
}
           

m被作為參數拷貝進了方法中。是以修改它的值其實沒有任何效果,是以為了不讓程式員随意修改m而卻沒達到任何效果而迷惑,m必須被

final

修飾。

繞了這麼大一圈,為什麼編譯器要生成這樣的效果呢?

其實,了解閉包的概念的人應該都知道原因。而Java中各種詭異的文法一般都是由生命周期帶來的影響。上面的程式中,m是一個局部變量,它被定義在棧上,而

new Outer$1Inner(this,m);

所生成的對象,是定義在堆上的。如果不将m作為成員變量拷貝進對象中,那麼離開m的作用域,

Inner

對象所指向的便是一個無效的位址。是以,編譯器會自動将局部類所使用的所有參數自動生成成員。

為什麼其他語言沒有這種現象呢?

這又回到了一個經典的問題上:Java是值傳遞還是引用傳遞。由于Java always pass-by-value,對于真正的引用,Java是無法傳遞過去的。而上面的問題核心就在與m如果被改變了,那麼其它的m的副本是無法感覺到的。而其他語言都通過其他的途徑解決了這個問題。

對于C++就是一個指針問題。

了解了真正的原因,便也能知道什麼時候需要

final

,什麼時候不需要

final

了。

public class Outer {
    private void test() {
        class Inner {
        int m=3;
            private void print() {
                System.out.println(m);//作為參數傳遞,本身都已經 pass-by-value。不用final
                int c=m+1; //直接使用m,需要加final
                
            }
        }
    }

}
           

而在Java 8 中,已經放寬政策,允許是

effectively final

的變量,實際上,就是編譯器在編譯的過程中,幫你加上

final

而已。而你應該保證允許編譯器加上

final

後,程式不報錯。

  1. 局部内部類還有個特點就是不能有權限修飾符。就好像局部變量不能有通路修飾符一樣
  2. 由上面可以看到,外部對象同樣是被傳入局部類中,是以局部類可以通路外部對象

嵌套類——>靜态方法

嵌套類沒什麼好說的,就好像靜态方法一樣,他可以被直接通路,他也能定義靜态變量。同時不能通路非靜态成員。

值得注意的是《Think in Java》中說過,可以将構造函數看作為靜态方法,是以嵌套類可以通路外部類的構造方法。

匿名類——>局部方法+繼承的文法糖

匿名類可以看作是對前3種類的再次擴充。具體來說匿名類根據應用場景可以看作:

  • 成員内部類+繼承
  • 局部内部類+繼承
  • 嵌套内部類+繼承

匿名類文法為:

new 繼承類名(){
  
  //Override 重載的方法    
    
}
           

傳回的結果會向上轉型為繼承類。

聲明一個匿名類:

public class Outer {

    private  List<String> list=new ArrayList<String>(){
        {
            add("test");
        }
    };

}
           

這便是一個經典的匿名類用法。

同樣編譯上面代碼會看到生成了兩個.class檔案

Outer.class

,

Outer$1.class

Outer$1.class

class Outer$1 extends ArrayList<String> {
    Outer$1(Outer this$0) {
        this.this$0 = this$0;
        this.add("1");
    }
}

           

可以看到匿名類的完整文法便是繼承+内部類。

由于匿名類可以申明為成員變量,局部變量,靜态成員變量,是以它的組合便是幾種内部類加繼承的文法糖,這裡不一一證明。

在這裡值得注意的是匿名類由于沒有類名,是以不能通過文法糖像正常的類一樣聲明構造函數,但是編譯器可以識别

{}

,并在編譯的時候将代碼放入構造函數中。

{}

可以有多個,會在生成的構造函數中按順序執行。

怎麼正确的使用内部類

在第二小節中,我們已經讨論過内部類的應用場景,但是如何優雅,并在正确的應用場景使用它呢?本小節将會詳細讨論。

1.注意記憶體洩露

《Effective Java》第二十四小節明确提出過。優先使用靜态内部類。這是為什麼呢?

由上面的分析我們可以知道,除了嵌套類,其他的内部類都隐式包含了外部類對象。這便是Java記憶體洩露的源頭。看代碼:

定義Outer:

public class Outer{

    public  List<String> getList(String item) {

        return new ArrayList<String>() {
            {
                add(item);
            }
        };
    }
}
           

使用Outer:

public class Test{

   public static List<String> getOutersList(){
   
    Outer outer=new Outer();
    //do something
    List<String> list=outer.getList("test");
   
    return list;    
   }
   public static void main(String[] args){
       List<String> list=getOutersList();
       
      
      //do something with list
   }
   
}

           

相信這樣的代碼一定有同學寫出來,這涉及到一個習慣的問題:

不涉及到類成員方法和成員變量的方法,最好定義為static

我們先研究上面的代碼,最大的問題便是帶來的記憶體洩露:

在使用過程中,我們定義

Outer

對象完成一系列的動作

  • 使用

    outer

    得到了一個

    ArraList

    對象
  • ArrayList

    作為結果傳回出去。

正常來說,在

getOutersList

方法中,我們

new

出來了兩個對象:

outer

list

,而在離開此方法時,我們隻将

list

對象的引用傳遞出去,

outer

的引用随着方法棧的退出而被銷毀。按道理來說,

outer

對象此時應該沒有作用了,也應該在下一次記憶體回收中被銷毀。

然而,事實并不是這樣。按上面所說的,建立的

list

對象是預設包含對

outer

對象的引用的,是以隻要

list

不被銷毀,

outer

對象将會一直存在,然而我們并不需要

outer

對象,這便是記憶體洩露。

怎麼避免這種情況呢?

很簡單:不涉及到類成員方法和成員變量的方法,最好定義為static

public class Outer{

    public static List<String> getList(String item) {

        return new ArrayList<String>() {
            {
                add(item);
            }
        };
    }
}
           

這樣定義出來的類便是嵌套類+繼承,并不包含對外部類的引用。

2.應用于隻實作一個接口的實作類

  • 優雅工廠方法模式

我們可以看到,在工廠方法模式中,每個實作都會需要實作一個Fractory來實作産生對象的接口,而這樣接口其實和原本的類關聯性很大的,是以我們可以将Fractory定義在具體的類中,作為内部類存在

  • 簡單的實作接口
new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("test");
           }
       }

       ).start();
    }
           

盡量不要直接使用Thread,這裡隻做示範使用

Java 8 的話建議使用lambda代替此類應用

  • 同時實作多個接口
public class imple{

    public static Eat getDogEat(){
        return new EatDog();
    }

    public static Eat getCatEat(){
        return new EatCat();
    }

    private static class EatDog implements Eat {
        @Override
        public void eat() {
            System.out.println("dog eat");
        }
    }
    private static class EatCat implements Eat{
        @Override
        public void eat() {
            System.out.println("cat eat");
        }
    }
}
           

3.優雅的單例類

public class Imple {

    public static Imple getInstance(){
        return ImpleHolder.INSTANCE;
    }


    private static class ImpleHolder{
        private static final Imple INSTANCE=new Imple();
    }
}
           

4.反序列化JSON接受的JavaBean

有時候需要反序列化嵌套JSON

{
    "student":{
        "name":"",
        "age":""
    }
}
           

類似這種。我們可以直接定義嵌套類進行反序列化

public JsonStr{
    
    private Student student;
    
    public static Student{
        private String name;
        private String age;
        
        //getter & setter
    }

    //getter & setter
}

           

但是注意,這裡應該使用嵌套類,因為我們不需要和外部類進行資料交換。

核心思想:

  • 嵌套類能夠通路外部類的構造函數
  • 将第一次通路内部類放在方法中,這樣隻有調用這個方法的時候才會第一次通路内部類,實作了懶加載

内部類還有很多用法,這裡不一一列舉。

總結

内部類的了解可以按照方法來了解,但是内部類很多特性都必須剝開文法糖和明白為什麼需要這麼做才能完全了解,明白内部類的所有特性才能更好使用内部類,在内部類的使用過程中,一定記住:能使用嵌套類就使用嵌套類,如果内部類需要和外部類聯系,才使用内部類。最後不涉及到類成員方法和成員變量的方法,最好定義為static可以防止内部類記憶體洩露。

尊重勞動成果,轉載請标注出處。

如果覺得寫得不錯,歡迎關注微信公衆号:逸遊Java ,每天不定時釋出一些有關Java幹貨的文章,感謝關注

Java 幹貨之深入了解Java内部類

參考文章:

Java 中引入内部類的意義?

成員内部類裡面為什麼不能有靜态成員和方法?