問:Java 常見的內(nèi)部類有哪幾種,簡單說說其特征?
答:靜態(tài)內(nèi)部類、成員內(nèi)部類、方法內(nèi)部類(局部內(nèi)部類)、匿名內(nèi)部類。
靜態(tài)內(nèi)部類是定義在另一個類里面用 static 修飾 class 的類,靜態(tài)內(nèi)部類不需要依賴于外部類(與類的靜態(tài)成員屬性類似)且無法使用其外部類的非 static 屬性或方法(因為在沒有外部類對象的情況下可以直接創(chuàng)建靜態(tài)內(nèi)部類的對象,如果允許訪問外部類的非 static 屬性或者方法就會產(chǎn)生矛盾)。
成員內(nèi)部類是沒有用 static 修飾且定義在在外部類類體中的類,是最普通的內(nèi)部類,可以看做是外部類的成員,可以無條件訪問外部類的所有成員屬性和成員方法(包括 private 成員和靜態(tài)成員),而外部類無法直接訪問成員內(nèi)部類的成員和屬性,要想訪問必須得先創(chuàng)建一個成員內(nèi)部類的對象然后通過指向這個對象的引用來訪問;當(dāng)成員內(nèi)部類擁有和外部類同名的成員變量或者方法時會發(fā)生隱藏現(xiàn)象(即默認(rèn)情況下訪問的是成員內(nèi)部類的成員,如果要訪問外部類的同名成員需要通過 OutClass.this.XXX 形式訪問);成員內(nèi)部類的 class 前面可以有 private 等修飾符存在。
方法內(nèi)部類(局部內(nèi)部類)是定義在一個方法里面的類,和成員內(nèi)部類的區(qū)別在于方法內(nèi)部類的訪問僅限于方法內(nèi);方法內(nèi)部類就像是方法里面的一個局部變量一樣,所以其類 class 前面是不能有 public、protected、private、static 修飾符的,也不可以在此方法外對其實例化使用。
匿名內(nèi)部類是一種沒有構(gòu)造器的類(實質(zhì)是繼承類或?qū)崿F(xiàn)接口的子類匿名對象),由于沒有構(gòu)造器所以匿名內(nèi)部類的使用范圍非常有限,大部分匿名內(nèi)部類用于接口回調(diào),匿名內(nèi)部類在編譯的時候由系統(tǒng)自動起名為 OutClass$1.class,一般匿名內(nèi)部類用于繼承其他類或?qū)崿F(xiàn)接口且不需要增加額外方法的場景(只是對繼承方法的實現(xiàn)或是重寫);匿名內(nèi)部類的 class 前面不能有 pravite 等修飾符和 static 修飾符;匿名內(nèi)部類訪問外部類的成員屬性時外部類的成員屬性需要添加 final 修飾(1.8 開始可以不用)。
問:下面說法哪些不正確,為什么?
內(nèi)部類不能有自己的成員方法和成員變量。
內(nèi)部類可用 abstract 修飾符定義為抽象類,也可以用 private 或 protected 定義。
內(nèi)部類可作為其他類的成員,而且可以訪問它所在類的成員。
除 static 內(nèi)部類外,不能直接在內(nèi)部類中聲明 static 成員(static 常量除外)。
答:A 的描述是錯誤的,其他都正確。
因為內(nèi)部類是指在一個類的內(nèi)部嵌套定義的類,與普通類一樣具有自己的成員方法和成員變量,成員和方法是類存在且有意義的基礎(chǔ)。
問:下面的內(nèi)部類哪些是正確的,哪些是錯誤的?
public class OutClass {
class InnerClass1 {
public static int func() {
return 1;
}
}
public class InnerClass2 {
static int func() {
return 1;
}
}
private class InnerClass3 {
int func() {
return 1;
}
}
static class InnerClass4 {
protected int func() {
return 1;
}
}
abstract class InnerClass5 {
public abstract int func();
}
}
答:由于靜態(tài)內(nèi)部類可以有靜態(tài)成員或者靜態(tài)方法,而非靜態(tài)內(nèi)部類不能有靜態(tài)成員或者靜態(tài)方法,所以 InnerClass1、InnerClass2 是錯的。其他 InnerClass3、InnerClass4、InnerClass5 都是正確的,解釋如第一題答案。
問:Java 中為什么成員內(nèi)部類可以直接訪問外部類的成員?
答:成員內(nèi)部類可以無條件訪問外部類的成員或者方法的原因解釋我們可以通過下面例子來說明。
public class OutClass {
public class InnerClass {
}
}
我們執(zhí)行命令 javac OutClass.java 編譯會發(fā)現(xiàn)得到兩個 class 文件,分別為 OutClass.class 和 OutClass$InnerClass.class,所以編譯器在進(jìn)行編譯的時候會把成員內(nèi)部類單獨編譯成一個字節(jié)碼文件,我們接著通過 javap [-v] OutClass$InnerClass 看下編譯后的成員內(nèi)部類的字節(jié)碼,如下:
Compiled from "OutClass.java" public class OutClass$InnerClass {
final OutClass this $0;
public OutClass$InnerClass(OutClass);
}
可以看到編譯后的成員內(nèi)部類中有一個指向外部類對象的引用,且成員內(nèi)部類編譯后構(gòu)造方法也多了一個指向外部類對象的引用參數(shù),所以說編譯器會默認(rèn)為成員內(nèi)部類添加了一個指向外部類對象的引用并且在成員內(nèi)部類構(gòu)造方法中對其進(jìn)行賦值操作,因此我們可以在成員內(nèi)部類中隨意訪問外部類的成員,同時也說明成員內(nèi)部類是依賴于外部類的,如果沒有創(chuàng)建外部類的對象則也無法創(chuàng)建成員內(nèi)部類的對象。
問:Java 1.8 之前為什么方法內(nèi)部類和匿名內(nèi)部類訪問局部變量和形參時必須加 final?
答:在 Java 1.8 以下因為對于普通局部變量或者形參的作用域是方法內(nèi),當(dāng)方法結(jié)束時局部變量或者形參就要隨之消失,而其匿名內(nèi)部類或者方法內(nèi)部類的生命周期又沒結(jié)束,匿名內(nèi)部類或者方法內(nèi)部類如果想繼續(xù)使用方法的局部變量就需要一些手段,所以 Java 在編譯匿名內(nèi)部類或者方法內(nèi)部類時就有一個規(guī)定來解決生命周期問題,即如果訪問的外部類方法的局部變量值在編譯期能確定則直接在匿名內(nèi)部類或者方法內(nèi)部類里面創(chuàng)建一個常量拷貝,如果訪問的外部類方法的局部變量值無法在編譯期確定則通過構(gòu)造器傳參的方式來對拷貝進(jìn)行初始化賦值。由此說明在匿名內(nèi)部類或者方法內(nèi)部類中訪問的外部類方法的局部變量或者形參是內(nèi)部類自己的一份拷貝,和外部類方法的局部變量或者形參不是一份,所以如果在匿名內(nèi)部類或者方法內(nèi)部類對變量做修改操作就一定會導(dǎo)致數(shù)據(jù)不一致性(外部類方法的參數(shù)不會跟著被修改,引用類型僅是引用,值修改不存在問題),為了杜絕數(shù)據(jù)不一致性導(dǎo)致的問題 Java 就要求使用 final 來保證,所以必須是 final 的。在 Java 1.8 開始我們可以不加 final 修飾符了,系統(tǒng)會默認(rèn)添加,Java 將這個功能稱為 Effectively final。
上面這段話可以通過下面的例子說明(對于非 final 無法編譯通過,所以不再舉例),如下:
public class OutClass {
private int out = 1;
public void func(final int param) {
final int in = 2;
new Thread() {
@Override
public void run() {
out = param;
out = in;
}
}.start();
}
}
上面類文件在 java 1.8 以下通過 javac 編譯后執(zhí)行 javap -l -v OutClass$1.class 查看匿名內(nèi)部類的字節(jié)碼可以發(fā)現(xiàn)如下情況:
......
class OutClass$1 extends java.lang.Thread
......
{
//匿名內(nèi)部類有了自己的 param 屬性成員。
final int val$param;
......
//匿名內(nèi)部類持有了外部類的引用作為一個屬性成員。
final OutClass this $0;
......
//匿名內(nèi)部類編譯后構(gòu)造方法自動多了兩個參數(shù),一個為外部類引用,一個為 param 參數(shù)。
OutClass$1(OutClass, int);
......
public void run();
......
Code:
stack=2, locals=, args_size=1
//out = param;語句,將匿名內(nèi)部類自己的 param 屬性賦值給外部類的成員 out。
0: aload_0
1: getfield #1 // Field this$0:LOutClass;
4: aload_0
5: getfield #2 // Field val$param:I
8: invokestatic #4 // Method OutClass.access$002:(LOutClass;I)I
11: pop //out = in;語句,將匿名內(nèi)部類常量 2 (in在編譯時確定值)賦值給外部類的成員 out。
12: aload_0
13: getfield #1 // Field this$0:LOutClass;
//將操作數(shù)2壓棧,因為如果這個變量的值在編譯期間可以確定則編譯器默認(rèn)會在
//匿名內(nèi)部類或方法內(nèi)部類的常量池中添加一個內(nèi)容相等的字面量或直接將相應(yīng)的
//字節(jié)碼嵌入到執(zhí)行字節(jié)碼中。
16: iconst_2
17: invokestatic #4 // Method OutClass.access$002:(LOutClass;I)I
20: pop
21: return
......
}
......
通過字節(jié)碼指令我想不用再多解釋了吧,上面字節(jié)碼包含了訪問局部變量編譯時可確定值和不可確定值的兩種情況,自己可以再琢磨下。
問:下面關(guān)于成員內(nèi)部類 InnerClass 的子類實現(xiàn) ChildInnerClassX 中哪些是可以編譯運行的?
class OutClass {
class InnerClass {
}
}
class ChildInnerClass1 extends OutClass.InnerClass {
}
class ChildInnerClass2 extends OutClass.InnerClass {
public ChildInnerClass2() {
super();
}
}
class ChildInnerClass3 extends OutClass.InnerClass {
public ChildInnerClass3(OutClass outClass) {
super();
}
}
class ChildInnerClass4 extends OutClass.InnerClass {
public ChildInnerClass3(OutClass outClass) {
outClass.super();
}
}
答:只有 ChildInnerClass4 子類是可以編譯運行的,其他都無法編譯通過。(雖然開發(fā)中很少會遇到這種)因為成員內(nèi)部類的繼承語法格式要求繼承引用方式為 Outter.Inner 形式且繼承類的構(gòu)造器中必須有指向外部類對象的引用,并通過這個引用調(diào)用 super(),其實這個要求就是因為成員內(nèi)部類默認(rèn)持有外部類的引用,外部類不先實例化則無法實例化自己。
問:下面程序的運行結(jié)果是什么?為什么?
List list1 = new ArrayList();
List list2 = new ArrayList() {
};
List list3 = new ArrayList() {{
}};
List list4 = new ArrayList() {
{
}
{
}
{
}
};
//1
System.out.println(list1.getClass() == list2.getClass());
// 2
System.out.println(list1.getClass() == list3.getClass());
// 3
System.out.println(list1.getClass() == list4.getClass());
// 4
System.out.println(list2.getClass() == list3.getClass());
// 5
System.out.println(list2.getClass() == list4.getClass());
// 6
System.out.println(list3.getClass() == list4.getClass());

答:程序運行返回 6 個 false。
首先 list1 指向一個 ArrayList 對象實例;list2 指向一個繼承自 ArrayList 的匿名類內(nèi)部類對象;list3 也指向一個繼承自 ArrayList 的匿名內(nèi)部類(里面一對括弧為初始化代碼塊)對象;list4 也指向一個繼承自 ArrayList 的匿名內(nèi)部類(里面多對括弧為多個初始化代碼塊)對象;由于這些匿名內(nèi)部類都出現(xiàn)在同一個類中,所以編譯后其實得到的是 Demo$1、Demo$2、Demo$3 的形式,所以自然都互補相等了,不信你可以通過 listX.getClass().getName() 進(jìn)行驗證。
問:開發(fā)中使用 Java 匿名內(nèi)部類有哪些注意事項(經(jīng)驗)?
答:常見的注意事項如下。
使用匿名內(nèi)部類時必須是繼承一個類或?qū)崿F(xiàn)一個接口(二者不可兼得且只能繼承一個類或者實現(xiàn)一個接口)。
匿名內(nèi)部類中是不能定義構(gòu)造函數(shù)的,如需初始化可以通過構(gòu)造代碼塊處理。
匿名內(nèi)部類中不能存在任何的靜態(tài)成員變量和靜態(tài)方法。
匿名內(nèi)部類為局部內(nèi)部類,所以局部內(nèi)部類的所有限制同樣對匿名內(nèi)部類生效。
匿名內(nèi)部類不能是抽象類,必須要實現(xiàn)繼承的類或者實現(xiàn)接口的所有抽象方法。
問:Java 匿名內(nèi)部類在使用時如何初始化嗎?
答:匿名內(nèi)部類無法通過構(gòu)造方法初始化,所以我們只能通過構(gòu)造代碼塊進(jìn)行初始化。
問:非靜態(tài)內(nèi)部類里面為什么不能有靜態(tài)屬性和靜態(tài)方法?
答:static 類型的屬性和方法在類加載的時候就會存在于內(nèi)存中,要使用某個類的 static 屬性或者方法的前提是這個類已經(jīng)加載到 JVM 中,非 static 內(nèi)部類默認(rèn)是持有外部類的引用且依賴外部類存在,所以如果一個非 static 的內(nèi)部類一旦具有 static 的屬性或者方法就會出現(xiàn)內(nèi)部類未加載時卻試圖在內(nèi)存中創(chuàng)建內(nèi)部類的 static 屬性和方法,這自然是錯誤的,類都不存在(沒被加載)卻希望操作它的屬性和方法。從另一個角度講非 static 的內(nèi)部類在實例化的時候才會加載(不自動跟隨主類加載),而 static 的語義是類能直接通過類名來訪問類的 static 屬性或者方法,所以如果沒有實例化非 static 的內(nèi)部類就等于非 static 的內(nèi)部類沒有被加載,所以無從談起通過類名訪問 static 屬性或者方法。
問:Java 匿名內(nèi)部類為什么不能直接使用構(gòu)造方法,匿名內(nèi)部類有沒有構(gòu)造方法?
答:因為類是匿名的(相當(dāng)于沒有名字),而且每次創(chuàng)建的匿名內(nèi)部類同時被實例化后只能使用一次,所以就無從創(chuàng)建一個同名的構(gòu)造方法了,但是可以直接調(diào)用父類的構(gòu)造方法(譬如 new InnerClass(xxx, xxx) {})。
實質(zhì)上匿名內(nèi)部類是有構(gòu)造方法的,是通過編譯器在編譯時幫忙生成的,如下代碼:
class InnerClass {
}
public class OutClass {
InnerClass clazz = new InnerClass() {
};
}
通過編譯后生成了 InnerClass.class、OutClass$1.class、OutClass.class,可以看見 OutClass$1.class 就是我們匿名內(nèi)部類的字節(jié)碼名字,我們通過 javap -v OutClass$1.class 可以看到如下:
......
{
final OutClass this$0;
......
OutClass$1(OutClass);
descriptor: (LOutClass;)
flags:
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LOutClass;
5: aload_0
6: invokespecial #2 // Method InnerClass."<init>":()V
9: return
LineNumberTable:
line 8: 0
}
......
可以很明顯看到內(nèi)部類的字節(jié)碼中編譯器為我們生成了參數(shù)為外部類引用的構(gòu)造方法,其構(gòu)造方法和普通類的構(gòu)造方法沒有區(qū)別,都是執(zhí)行 <init> 方式。
問:Java 中非靜態(tài)內(nèi)部類和靜態(tài)內(nèi)部類有什么區(qū)別?
答:常見的區(qū)別如下。
非靜態(tài)內(nèi)部類默認(rèn)持有外部類的引用,靜態(tài)內(nèi)部類不存在該特性。
非靜態(tài)內(nèi)部類中不能定義靜態(tài)成員或者方法,靜態(tài)內(nèi)部類中可以隨便定義。
非靜態(tài)內(nèi)部類可以直接訪問外部類的成員變量或者方法,靜態(tài)內(nèi)部類只能直接訪問外部類的靜態(tài)成員或者方法(實質(zhì)是持有外部類名)。
非靜態(tài)內(nèi)部類可以定義在外部類的任何位置(方法里外均可,在方法外面定義的內(nèi)部類的 class 訪問類型可以是 public、protected 等,方法里的只能是默認(rèn) class,類似局部變量),靜態(tài)內(nèi)部類只能定義在外部類中最外層,class 修飾符可以是 public、protected 等。
非靜態(tài)內(nèi)部類創(chuàng)建實例時必須先創(chuàng)建外部類實例,靜態(tài)內(nèi)部類不依賴外部類實例。
靜態(tài)方法中定義的內(nèi)部類是靜態(tài)內(nèi)部類(這時不能在類前面加 static 關(guān)鍵字),靜態(tài)方法中的靜態(tài)內(nèi)部類與普通方法中的內(nèi)部類使用類似,除了可以直接訪問外部類的 static 成員變量或者方法外還可以訪問靜態(tài)方法中的局部變量(java 1.8 以前局部變量前必須加 final 修飾符)。
問:Java 中內(nèi)部類有什么好處(即 Java 的內(nèi)部類有什么作用)?
答:具體好處如下。
內(nèi)部類可以很好的實現(xiàn)隱蔽,一般的非內(nèi)部類,是不允許有 private 與 protected 等權(quán)限的,但內(nèi)部類(除過方法內(nèi)部類)可以通過這些修飾符來實現(xiàn)隱藏。
內(nèi)部類擁有外部類的的訪問權(quán)限(分靜態(tài)非靜態(tài)情況),通過這一特性可以比較好的處理類之間的關(guān)聯(lián)性,將一類事物的流程放在一起內(nèi)部處理。
通過內(nèi)部類可以實現(xiàn)多重繼承,java 默認(rèn)是單繼承,我們可以通過多個內(nèi)部類繼承實現(xiàn)多個父類,接著由于外部類完全可訪問內(nèi)部類,所以就實現(xiàn)了類似多繼承的效果。
通過內(nèi)部類可以避免修改接口而實現(xiàn)同一個類中兩種同名方法的調(diào)用(譬如你的類 A 中有一個參數(shù)為 int 的 func 方法,現(xiàn)在類 A 需要繼承實現(xiàn)一個接口 B,而接口 B 中也有一個參數(shù)為 int 的 func 方法,此時如果直接繼承實現(xiàn)就會出現(xiàn)同名方法矛盾問題,這時候如果不允許修改 A、B 類的 func 方法名則可以通過內(nèi)部類來實現(xiàn) B 接口,因為內(nèi)部類對外部類來說是完全可訪問的)。