Java 乾貨之深刻理解Java內部類

能夠將一個類定義在另外一個類或方法中,這樣的類叫作內部類 --《Thinking in Java》java

提及內部類,你們並不陌生,而且會常常在實例化容器的時候使用到它。可是內部類的具體細節語法,原理以及實現是什麼樣的能夠很多人都還挺陌生,這裏做一篇總結,但願經過這篇總結提升對內部類的認識。程序員


內部類是什麼?

由文章開頭可知,內部類的定義爲:定義在另外一個類或方法中的類。而根據使用場景的不一樣,內部類還能夠分爲四種:成員內部類,局部內部類,匿名內部類和靜態內部類。每一種的特性和注意事項都不一樣,下面咱們一一說明。編程

成員內部類

顧名思義,成員內部類是定義在類內部,做爲類的成員的類。以下:bash

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. 局部內部類不能有訪問權限修飾符ide

  2. 局部內部類不能被定義爲static函數

  3. 局部內部類不能定義static成員ui

  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.classOuter$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.classOuter$1Inner.classOuter$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.classOuter$1.class放入IDEA中反編譯:

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出來了兩個對象:outerlist,而在離開此方法時,咱們只將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 中引入內部類的意義?
成員內部類裏面爲何不能有靜態成員和方法?

相關文章
相關標籤/搜索