Java內部類你真的會嗎?

超級小小黑發表於2019-07-16

一、四種內部類

1.1、成員內部類

成員內部類是最普通的內部類,它的定義為位於另一個類的內部,形如下面的形式:

 1 public class OuterAndInnerClass {
 2     public static void main(String[] args) {
 3         Outer outer = new Outer();
 4         //建立內部類的兩種方式 (1)在外部
 5         //Outer.Inner inner = outer.new Inner();
 6         //建立內部類的兩種方式 (1)在內部類所依附的外部類中建立
 7         Outer.Inner inner = outer.getInnerClass();
 8         outer.out();
 9         inner.in();
10         inner.testInner();
11     }
12 }
13 class Outer{//外部類
14     public Inner getInnerClass(){
15         return new Inner();
16     }
17     String outName = "外部類";
18     String sameName = "同名外部";
19     public void out(){
20         System.out.println("外部方法");
21     }
22     class Inner{//內部類
23         String inName = "內部類";
24         String sameName = "同名內部";
25         String name = "內部類變數";
26         public void in(){
27             System.out.println("內部方法");
28         }
29         public void testInner(){
30             String name = "區域性變數";
31             System.out.println(name);//#內部類變數
32             System.out.println(this.name);//#區域性變數
33             System.out.println("outName:" + outName);//#outName:外部類
34             System.out.println("inName:" + inName);//#inName:內部類
35             System.out.println("sameName:" + sameName);//#sameName:同名內部
36             System.out.println("sameName:" + this.sameName);//#sameName:同名內部,this指向Inner
37             System.out.println("sameName:" + Outer.this.sameName);//#sameName:同名外部
38         }
39     }
40 }

1.1.1,建立成員內部類的方法有兩種

  雖然成員內部類可以無條件地訪問外部類的成員,而外部類想訪問成員內部類的成員卻不是這麼隨心所欲了。在外部類中如果要訪問成員內部類的成員,必須先建立一個成員內部類的物件,再通過指向這個物件的引用來訪問:

Outer outer = new Outer();
第一種方式:Outer.Inner inner = outer.new Inner();
第二種方式:Outer.Inner inner = outer.getInnerClass();

 


1.1.2,成員內部類的訪問控制修飾符

內部類就如同外部類的成員變數一樣。四種訪問控制符都是可以的,public,default,protected,private。
內部類可以擁有private訪問許可權、protected訪問許可權、public訪問許可權及包訪問許可權。比如上面的例子,如果成員內部類Inner用private修飾,則只能在外部類的內部訪問,如果用public修飾,則任何地方都能訪問;如果用protected修飾,則只能在同一個包下或者繼承外部類的情況下訪問;如果是預設訪問許可權,則只能在同一個包下訪問。這一點和外部類有一點不一樣,外部類只能被public和包訪問兩種許可權修飾。我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種許可權修飾。

1.1.3,成員內部類呼叫外部類的成員變數或者方法

呼叫內部類的成員變數:

System.out.println("inName:" + inName);//#inName:內部類

呼叫外部類的成員變數:(同呼叫內部類的成員變數)

System.out.println("outName:" + outName);//#outName:外部類
TIPS:
    特殊情況:當外部類和內部類的成員變數同名的情況
    當成員內部類擁有和外部類同名的成員變數或者方法時,會發生隱藏現象,即預設情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要以下面的形式進行訪問:
    外部類.this.成員變數
    外部類.this.成員方法

1.2、區域性內部類

區域性內部類是定義在一個方法或者一個作用域裡面的類,它和成員內部類的區別在於區域性內部類的訪問僅限於方法內或者該作用域內。

 1 //區域性內部類
 2 public class LocalInnerClass {
 3     public static void main(String[] args) {
 4         People people = new People();
 5         people.getWoman("形參變數");
 6     }
 7 }
 8 class People {
 9     String peopleName = "people";
10     String sameName = "外部同名變數";
11     public People getWoman(final String methodName){
12         final String localName = "區域性變數";
13         class Woman extends People{
14             String womanName = "woman";
15             String sameName = "區域性內部類同名變數";
16             public Woman(){
17                 //methodName = "";//編譯錯誤:Cannot assign a value to final variable 'methodName'
18                 //localName = "";//編譯錯誤:Variable 'localName' is accessed from within inner class, needs to be final or effectively final
19                 System.out.println(methodName);//#形參變數
20                 System.out.println(localName);//#區域性變數
21                 System.out.println(peopleName);//#people
22                 System.out.println(womanName);//#woman
23                 System.out.println(sameName);//#區域性內部類同名變數
24                 System.out.println(this.sameName);//#區域性內部類同名變數
25                 System.out.println(People.this.sameName);//#外部同名變數
26             }
27         }
28         return new Woman();
29     }
30 }

在區域性內部類中呼叫外部類的變數或者方法的方式和規則是一樣的。

TIPS
    值得注意的是,區域性內部類就像是方法裡面的一個區域性變數一樣,是不能有public、protected、private以及static修飾符的。

 

1.3、匿名內部類

  匿名內部類由於沒有名字,所以它的建立方式有點兒奇怪。建立格式如下:

new 父類構造器(引數列表)|實現介面()  
    {  
     //匿名內部類的類體部分  
    }

 

  在這裡我們看到使用匿名內部類我們必須要繼承一個父類或者實現一個介面,當然也僅能只繼承一個父類或者實現一個介面。同時它也是沒有class關鍵字,這是因為匿名內部類是直接使用new來生成一個物件的引用。當然這個引用是隱式的。

 1 //匿名內部類
 2 public class AnonInnerClass {
 3     public static void useRunnable(MyRunnable runnable){
 4         runnable.run();
 5     }
 6     public static void main(String[] args) {
 7         AnonInnerClass.useRunnable(new MyRunnable() {
 8             @Override
 9             public void run() {
10                 System.out.println("重寫run方法");
11             }
12         });
13         AnonInnerClass.useRunnable(new MyRunnable("name") {
14             @Override
15             public void run() {
16                 System.out.println("重寫run方法");
17             }
18         });
19     }
20 }
21 abstract class MyRunnable {
22     public MyRunnable(){
23         System.out.println("呼叫匿名內部類的無參構造器");
24     }
25     public MyRunnable(String name){
26         System.out.println("呼叫匿名內部類的有參構造器,引數為:" + name);
27     }
28     //抽象方法
29     public abstract void run();
30 }

 

  這裡我們能夠看到,useRunnable 方法要接受一個MyRunnable的例項引數,但是,MyRunnable是一個抽象的類,不能被例項化,所以只能建立一個新的類繼承這個MyRunnable類,然後指向MyRunnable,從而拿到MyRunnable的例項引數。
  在這裡JVM會建立一個繼承自MyRunnable類的匿名類的物件,該物件轉型為對MyRunnable型別的引用。
  對於匿名內部類的使用它是存在一個缺陷的,就是它僅能被使用一次,建立匿名內部類時它會立即建立一個該類的例項,該類的定義會立即消失,所以匿名內部類是不能夠被重複使用。對於上面的例項,如果我們需要對test()方法裡面內部類進行多次使用,建議重新定義類,而不是使用匿名內部類。

TIPS:
    1、使用匿名內部類時,我們必須是繼承一個類或者實現一個介面,但是兩者不可兼得,同時也只能繼承一個類或者實現一個介面。
    2、匿名內部類中是不能定義建構函式的。(類都是匿名的,沒法定義構造方法)
    3、匿名內部類中不能存在任何的靜態成員變數和靜態方法。(類是匿名的,當然沒有類方法或類變數)
    4、匿名內部類為區域性內部類,所以區域性內部類的所有限制同樣對匿名內部類生效。
    5、匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的介面的所有抽象方法。
    6、我們給匿名內部類傳遞引數的時候,若該形參在內部類中需要被使用,那麼該形參必須要為final。也就是說:當所在的方法的形參需要被內部類裡面使用時,該形參必須為final。
    7、匿名內部類的初始化(使用構造程式碼塊)

 

我們一般都是利用構造器來完成某個例項的初始化工作的,但是匿名內部類是沒有構造器的!那怎麼來初始化匿名內部類呢?使用構造程式碼塊!利用構造程式碼塊能夠達到為匿名內部類建立一個構造器的效果。

 1 public class InitAnonInnerClass {
 2     public static void main(String[] args) {
 3         OuterClass outer = new OuterClass();
 4         InnerClass inner1 = outer.getInnerClass(15, "變了");
 5         System.out.println(inner1.getStr());
 6         InnerClass inner2 = outer.getInnerClass(20, "變了");
 7         System.out.println(inner2.getStr());
 8     }
 9 }
10 
11 class OuterClass {
12     public InnerClass getInnerClass(final int num, final String str){
13         return new InnerClass() {
14             int num_ ;
15             String str_ ;
16             //使用構造程式碼塊完成初始化
17             {
18                 if(0 < num && num < 18){
19                     //str = "";//編譯錯誤Variable 'str' is accessed from within inner class, needs to be final or effectively final
20                     str_ = str;
21                 }else {
22                     str_ = "沒變啊";
23                 }
24             }
25             public String getStr(){
26                 return str_;
27             }
28         };
29     }
30 }
31 
32 abstract class InnerClass {
33     public abstract String getStr();
34 }
out:

  變了
  沒變啊

 

 

1.4、靜態內部類

靜態內部類也是定義在另一個類裡面的類,只不過在類的前面多了一個關鍵字static。靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非static成員變數或者方法,這點很好理解,因為在沒有外部類的物件的情況下,可以建立靜態內部類的物件,如果允許訪問外部類的非static成員就會產生矛盾,因為外部類的非static成員必須依附於具體的物件。

 1 //靜態內部類
 2 public class StaticInnerClass {
 3     public static void main(String[] args) {
 4         //初始化靜態內部類,注意和其它內部類的初始化方式的區別
 5         OuterClass1.InnerClass inner = new OuterClass1.InnerClass();
 6         inner.test();
 7     }
 8 }
 9 class OuterClass1 {
10     String outName = "我是外部類";
11     static String outType = "外部類";
12     static class InnerClass {
13         String innerName = "我是內部類";
14         static String innerType = "靜態內部類";
15         public InnerClass (){
16             //System.out.println(outName);//編譯錯誤:Non-static field 'outName' cannot be referenced from a static context
17             System.out.println(outType);
18         }
19         public void test(){
20             System.out.println("呼叫內部類方法");
21         }
22     }
23 }

 

如上所示建立靜態內部類物件的一般形式為: 

外部類類名.內部類類名 xxx = new 外部類類名.內部類類名()

 

二、深入理解內部類

2.1.為什麼成員內部類可以無條件訪問外部類的成員?

  在此之前,我們已經討論過了成員內部類可以無條件訪問外部類的成員,那具體究竟是如何實現的呢?下面通過反編譯位元組碼檔案看看究竟。事實上,編譯器在進行編譯的時候,會將成員內部類單獨編譯成一個位元組碼檔案,下面是OuterAndInnerClass.java的程式碼:

 1 public class OuterAndInnerClass {
 2     public static void main(String[] args) {
 3         Outer outer = new Outer();
 4         Outer.Inner inner = outer.getInnerClass();
 5         outer.out();
 6     }
 7 }
 8 
 9 class Outer{
10     public Inner getInnerClass(){
11         return new Inner();
12     }
13     public void out(){
14         System.out.println("外部方法");
15     }
16     class Inner{
17         public void in(){
18             System.out.println("內部方法");
19         }
20     }
21 }

 

編譯之後,出現了兩個位元組碼檔案:

  編譯器會預設為成員內部類新增了一個指向外部類物件的引用,那麼這個引用是如何賦初值的呢?
雖然我們在定義的內部類的構造器是無參構造器,編譯器還是會預設新增一個引數,該引數的型別為指向外部類物件的一個引用,所以成員內部類中的Outter this&0 指標便指向了外部類物件,因此可以在成員內部類中隨意訪問外部類的成員。從這裡也間接說明了成員內部類是依賴於外部類的,如果沒有建立外部類的物件,則無法對Outter this&0引用進行初始化賦值,也就無法建立成員內部類的物件了。

 2.2、為什麼區域性內部類和匿名內部類只能訪問區域性final變數?

 1 public class Test {
 2     public static void main(String[] args)  {
 3         Test test = new Test();
 4         test.test(1);
 5     }
 6 
 7     public void test(final int b) {
 8         final int a = 10;
 9         new Thread(){
10             public void run() {
11 //                a = 2;//編譯錯誤:Cannot assign a value to final variable 'b'
12 //                b = 3;//編譯錯誤:Cannot assign a value to final variable 'b'
13                 System.out.println(a);
14                 System.out.println(b);
15             };
16         }.start();
17     }
18 }

  當test方法執行完畢之後,變數a的生命週期就結束了,而此時Thread物件的生命週期很可能還沒有結束,那麼在Thread的run方法中繼續訪問變數a就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?Java採用了 複製 的手段來解決這個問題。這個過程是在編譯期間由編譯器預設進行,如果這個變數的值在編譯期間可以確定,則編譯器預設會在匿名內部類(區域性內部類)的常量池中新增一個內容相等的字面量或直接將相應的位元組碼嵌入到執行位元組碼中。這樣一來,匿名內部類使用的變數是另一個區域性變數,只不過值和方法中區域性變數的值相等,因此和方法中的區域性變數完全獨立開。

  也就說如果區域性變數的值在編譯期間就可以確定,則直接在匿名內部裡面建立一個拷貝。如果區域性變數的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。
  從上面可以看出,在run方法中訪問的變數a根本就不是test方法中的區域性變數a。這樣一來就解決了前面所說的 生命週期不一致的問題。但是新的問題又來了,既然在run方法中訪問的變數a和test方法中的變數a不是同一個變數,當在run方法中改變變數a的值的話,會出現什麼情況?
  對,會造成資料不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題,java編譯器就限定必須將變數a限制為final變數,不允許對變數a進行更改(對於引用型別的變數,是不允許指向新的物件),這樣資料不一致性的問題就得以解決了。
  到這裡,想必大家應該清楚為何 方法中的區域性變數和形參都必須用final進行限定了。

 

  凌晨兩點了,向科比致敬。

 

 參考文章:

  https://www.cnblogs.com/dolphin0520/p/3811445.html(海子大神)

  https://www.cnblogs.com/chenssy/p/3390871.html

 

  如有錯誤的地方還請留言指正。

  原創不易,轉載請註明原文地址:https://www.cnblogs.com/hello-shf/p/11192571.html

  

 

相關文章