細說Java內部類, 靜態內部類, 區域性類, 匿名內部類
前言
自己看了一眼oracle官網關於內部類的介紹, 不多, 但是有些點還是要注意的. 當然這些知識只能說是面試有用, 平時使用內部類, 如果違反規則, 編譯器會提示你的, 所以看看就行, 沒必要背熟.
名詞介紹
先把我們用的名詞說清楚.
我們說的內部類, 官方的叫法是巢狀類(Nested Classes), 巢狀類包括兩種, 分別是靜態巢狀類(Static Nested Classes)和內部類(Inner Classes), 其中內部類又有三種形式, 第一種就是我們常見的內部類, 其他兩種特殊形式的內部類分別是區域性類(Local Classes)和匿名類(Anonymous Classes)
下面分別介紹他們.
內部類
為了介紹方便, 我們統一使用內部類來稱呼這些東西, 內部類分成靜態內部類和非靜態內部類, 非靜態內部類有額外兩種特殊形式, 一種叫區域性類, 另一種叫匿名內部類. 同時, 我們把包裹內部類的類稱為外圍類.
內部類在作為外圍類的成員時, 比如下面這種形式:
class OuterClass {
...
static class StaticNestedClass {
...
}
class InnerClass {
...
}
}
內部類可以用private
, protected
public
或者package private
(什麼都不寫, 稱之為package private, 也就是包許可權)修飾. 回憶一下, 外圍類只能使用
public
或者package private
修飾.
靜態內部類
靜態內部類除了訪問許可權修飾符比外圍類多以外, 和外圍類沒有區別, 只是程式碼上將靜態內部類組織在了外圍類裡面. 如果在外圍類外部引用靜態內部類, 需要帶上外圍類的名字, 比如, 我想new一個靜態內部類的例項:
OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
注意, 雖然靜態內部類程式碼是寫在外圍類裡面的, 但是並不能訪問外圍類的非公開成員, 因為實際上它們就是兩個不同的類, 不同的類之間當然不能訪問對方的非公開成員.
可以這麼理解, 雖然靜態內部類程式碼寫在外圍類裡面, 但它是在外圍類的外面, 外圍類對它來說仍然是一個黑盒.
非靜態內部類
我們這裡討論的是作為類成員存在的非靜態內部類, 也就是形如:
class OuterClass {
class InnerClass {
...
}
}
非靜態內部類和靜態內部類不同, 非靜態內部類能訪問外圍類的一切成員, 包括私有成員, 就好像它確實是在外圍類的裡面, 能看到外圍類這個黑盒內部的細節.
而很容易被忽視的一點是, 外部類雖然不能直接訪問內部類的成員, 但是可以通過內部類的例項訪問, 注意, 外部類能訪問非靜態內部類私有成員.
非靜態內部類實際上是和它的外圍類例項相關聯的, 換句話說, 它隱式持有一個外圍類的引用, 雖然我們程式碼裡面沒寫, 但是Java在生成位元組碼的時候會給非靜態內部類新增一些人造的構造方法, 會使非靜態內部類例項化時拿到外圍類例項的引用並作為成員變數儲存起來, 有興趣的同學可以看下反編譯出來的smali程式碼, 裡面可以看到我說的這些.
注意非靜態內部類是一定要和外圍類相關聯的, 也就是說, 只要有一個非靜態內部類, 它就一定會持有一個外圍類的例項引用, 它不能脫離外圍類的例項存在, 在程式碼上表現出來是我們不能直接new一個非靜態內部類. 即便是要直接new, 也必須先new一個外圍類, 通過外圍類來建立非靜態內部類的例項, 像下面這樣:
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
注意上面這行程式碼的new的寫法是object.new
形式.
不止這個限制, 由於非靜態內部類不能脫離外圍類例項單獨存在, 它不能有static成員. static成員是和類相關的, 不和例項相關, 而非靜態內部類必須依賴一個例項才能存在, 所以它不能有static成員也是很容易理解的.
當然我這裡說的static成員, 除了一種, 那就是static final
形式的常量. 很奇怪吧, 非靜態內部類竟然允許定義常量, 事實確實是這樣.
題外話: 說到常量的宣告,
static final int a
和final int b
有什麼區別呢? a是真正的常量, b應該叫不可變數, a這種定義形式不可能出現在方法體裡, 而b這種形式可以. b如果定義在類成員中, 應該是每個例項都有一個b, 而a則是一個類僅有一個a.
非靜態內部類裡面不能定義介面, 因為介面是隱式static的.
另外靜態初始化塊也是不允許出現在非靜態內部類中的.
區域性類
定義在語句塊裡的非靜態內部類叫做區域性類. 通常來說, 我們在方法體裡定義區域性類.
有人問為什麼沒有靜態內部類的區域性類, Java方法體裡面就沒法定義static修飾的東西, 更別提靜態內部類了.
題外話: C/C++裡面倒是可以在方法體裡面定義static變數, 那種變數可見性是方法體內, 但生命週期卻超過了定義它的方法體.
下面這段程式碼展示了一個區域性類PhoneNumber
:
/*
code from https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html
*/
public class LocalClassExample {
static String regularExpression = "[^0-9]";
public static void validatePhoneNumber(String phoneNumber1, String phoneNumber2) {
final int numberLength = 10;
// Valid in JDK 8 and later:
// int numberLength = 10;
class PhoneNumber {
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber){
// numberLength = 7;
String currentNumber = phoneNumber.replaceAll(regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
public String getNumber() {
return formattedPhoneNumber;
}
// Valid in JDK 8 and later:
// public void printOriginalNumbers() {
// System.out.println("Original numbers are " + phoneNumber1 +
// " and " + phoneNumber2);
// }
}
PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
// Valid in JDK 8 and later:
// myNumber1.printOriginalNumbers();
if (myNumber1.getNumber() == null)
System.out.println("First number is invalid");
else
System.out.println("First number is " + myNumber1.getNumber());
if (myNumber2.getNumber() == null)
System.out.println("Second number is invalid");
else
System.out.println("Second number is " + myNumber2.getNumber());
}
public static void main(String... args) {
validatePhoneNumber("123-456-7890", "456-7890");
}
}
作為非靜態內部類的一種特殊形式, 非靜態內部類的所有限制對區域性類同樣成立.
區域性類能訪問外圍類的所有成員, 此外, 由於它是定義在方法體裡的, 它甚至可以訪問方法體的區域性變數, 但必須是final
修飾的區域性變數.
題外話: 寫過Android可能知道, 區域性類/匿名類如果想訪問方法的引數, 我們需要在方法引數上手動新增
final
從Java SE 8開始, 區域性類不僅可以訪問標記為final
的方法區域性變數和方法引數, 還可以訪問實際上是final
(effectively final)的的區域性變數或方法引數, 什麼叫effectively final呢, 就是說我們沒有改變過它的值的變數或引數, 最簡單的方法, 就是我們手動給它加上final
, 編譯器不報錯, 就說明它是effectively final的.
比如下面這段程式碼:
PhoneNumber(String phoneNumber) {
//numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
phoneNumber
就是effectively final的, 如果我們把賦值語句前面的註釋去掉, 那它就不是effectively final了.
簡單來講就是Java SE 8開始, 允許區域性類允許訪問隱式final的區域性變數或方法引數.
題外話: 方法體也裡面不能定義介面, 因為介面是隱式static的.
匿名內部類
匿名內部類可以看成是一種沒有名字的區域性類, 它可以讓我們的類的定義和例項化同時進行.
下面程式碼中展示了局部類和匿名內部類的使用.
public class HelloWorldAnonymousClasses {
interface HelloWorld {
public void greet();
}
public void sayHello() {
class EnglishGreeting implements HelloWorld {
public void greet() {
greetSomeone("world");
}
}
HelloWorld englishGreeting = new EnglishGreeting();
HelloWorld spanishGreeting = new HelloWorld() {
public void greet() {
greetSomeone("mundo");
}
};
englishGreeting.greet();
spanishGreeting.greet();
}
public static void main(String... args) {
HelloWorldAnonymousClasses myApp =
new HelloWorldAnonymousClasses();
myApp.sayHello();
}
}
匿名內部類的定義和例項化往往是在一條語句裡完成的, 所以我們會看到匿名內部類的最後面還會有一個分號.
匿名內部類的訪問外部變數的規則和區域性類相同.
匿名內部類不能定義構造方法, 這很容易理解, 因為連類名都沒有, 構造方法連名字都不知道, 更別提定義了.
遮蔽
在非靜態內部類中定義的變數會遮蔽外圍的同名變數, 比如下面這段程式碼:
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
}
}
public static void main(String... args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
輸出是:
x = 23
this.x = 1
ShadowTest.this.x = 0
注意訪問各個層次的同命變數的不同寫法.
同時, 注意在區域性類或匿名內部類的方法中, 如果把外圍方法的區域性變數遮蔽了, 就沒法在當前方法中訪問那個區域性變量了.