一、四種內部類
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