這個系列是幫助復習 Java 的基礎知識的,但是并不會按照一個特定的順序?,F(xiàn)在開始復習下內(nèi)部類的相關(guān)知識。
0. 簡介
內(nèi)部類的定義很簡單,它其實就是在一個類里面定義了另一個類,但是這個定義還是有很多細節(jié)需要掌握的。
1. 非靜態(tài)內(nèi)部類
1.1 定義
非靜態(tài)內(nèi)部類就是在一個類的內(nèi)部里面定義了一個沒有用 static 修飾的類。
1.2 訪問控制符
| 內(nèi)部類的訪問控制符 | 訪問范圍 |
|---|---|
| private | 同一個類 |
| default | 同一個類,同一包中 |
| protected | 同一個類,子類,同一包中 |
| public | 任何位置 |
1.3 非靜態(tài)內(nèi)部類訪問外部類元素
非靜態(tài)內(nèi)部類是可以訪問外部類的任何實例變量和方法,包括 private 修飾的成員變量?,F(xiàn)在用一個例子來說明一下,代碼如下:
public class Outer {
private int a = 10;
private void innerCall() {
System.out.println("Inner Call");
}
private class Inner {
public void printInfo() {
System.out.println("a = " + a);
innerCall();
}
}
public void test() {
Inner inner = new Inner();
inner.printInfo();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.test();
}
}
輸出結(jié)果:
a = 10
Inner Call
從輸出的結(jié)果就可以看到,非靜態(tài)內(nèi)部類是可以訪問到外部類的任何實例變量和方法的。
那為什么內(nèi)部類可以直接訪問外部類的實例變量和方法呢?因為內(nèi)部類里面是持有外部類的實例引用,一個非靜態(tài)內(nèi)部類創(chuàng)建時必然會有其外部類實例的引用。也就是說上面的 printInfo() 方法也可以寫成如下:
public void printInfo() {
System.out.println("a = " + Outer.this.a);
Outer.this.innerCall();
}
上述代碼中的 Outer.this 就是非靜態(tài)內(nèi)部類持有外部類的實例引用。
1.3.1 非靜態(tài)內(nèi)部類方法訪問某個變量的檢查順序
當非靜態(tài)內(nèi)部類的方法訪問某個變量是按一定順序來查找的,順序如下
- 在該方法的局部變量找
- 方法所在的內(nèi)部類找
- 內(nèi)部類所在的外部類找
- 如果都沒有則編譯報錯
舉個例子,代碼如下:
public class Outer {
private int a = 10;
private class Inner {
private int a = 9;
public void printInfo(int a) {
System.out.println("a = " + a);
}
}
public void test() {
Inner inner = new Inner();
inner.printInfo(8);
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.test();
}
}
以上代碼的輸出結(jié)果就是 a = 8。
如果把 printInfo(int a) 的形參 a 去掉,則輸出的結(jié)果為 a = 9。
再把 Inner 類中的成員變量 a 去掉,則輸出的結(jié)果為 a = 10。
各位可以自己嘗試一下,這里就不再講解。
1.4 外部類訪問非靜態(tài)內(nèi)部類的元素
外部類不能直接訪問非靜態(tài)內(nèi)部類的實例變量和方法,如果想要訪問的話,必須要創(chuàng)建非靜態(tài)內(nèi)部類的實例進而調(diào)用該實例的成員。如下代碼所示:
public class Outer {
private int a = 10;
private class Inner {
public void printInfo() {
System.out.println("a = " + a);
}
}
public void test() {
//這句會編譯錯誤
printInfo();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.test();
}
}
以上代碼會編譯錯誤,如果想要訪問的話只能創(chuàng)建 Inner 的實例訪問該方法,這里有兩種方式,一種就是在 test() 方法里面創(chuàng)建 Inner 對象,另一種就是在 main 方法創(chuàng)建,以下兩種方法都用代碼試一下:
public class Outer {
private int a = 10;
private class Inner {
public void printInfo() {
System.out.println("a = " + a);
}
}
public void test() {
new Inner().printInfo();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.new Inner().printInfo();
outer.test();
}
}
從以上代碼可以看到,test() 方法里面直接 new Inner() 即可調(diào)用 Inner 方法。不過在 main 方法里要先創(chuàng)建 Outer 對象才可以再調(diào)用 new Inner()。
1.4.1 外部類的靜態(tài)方法不可以創(chuàng)建非靜態(tài)內(nèi)部類
根據(jù) Java 的規(guī)則,靜態(tài)成員是不可以訪問非靜態(tài)成員的。非靜態(tài)內(nèi)部類其實就是類實例中的一個成員,所以在外部類的靜態(tài)方法不可以創(chuàng)建非靜態(tài)內(nèi)部類,現(xiàn)用代碼舉例:
public class Outer {
private int a = 10;
private class Inner {
public void printInfo() {
System.out.println("a = " + a);
}
}
public static void main(String[] args) {
//這句代碼會編譯報錯
Inner inner = new Inner();
}
}
1.4.2 非靜態(tài)內(nèi)部類不可以創(chuàng)建靜態(tài)成員
同樣的,非靜態(tài)內(nèi)部類不可以創(chuàng)建靜態(tài)代碼塊,靜態(tài)方法和靜態(tài)成員變量。代碼舉例如下:
public class Outer {
private int a = 10;
private class Inner {
//以下三個靜態(tài)聲明都會編譯報錯
static {
}
public static int a = 0;
public static void printInfo() {
System.out.println("a = " + a);
}
}
public static void main(String[] args) {
}
}
1.5 非靜態(tài)內(nèi)部類子類的規(guī)則
非靜態(tài)內(nèi)部類的子類必須要存在外部類的實例引用。代碼舉例如下:
public class SubInner extends Outer.Inner {
public SubInner(Outer outer) {
outer.super();
}
}
這里解釋一下為什么非靜態(tài)內(nèi)部類的子類需要外部類的實例引用,因為由以上就可以知道非靜態(tài)內(nèi)部類一定會有外部類的實例引用,所以該子類也應該要有外部類的實例引用,只是這個外部類的引用是創(chuàng)建該子類的時候傳入的。
2. 靜態(tài)內(nèi)部類
2.1 定義
靜態(tài)內(nèi)部類就是在一個類的內(nèi)部里面定義了一個使用用 static 修飾的類。
2.2 靜態(tài)內(nèi)部類訪問外部類
靜態(tài)內(nèi)部類不可以訪問外部類的非靜態(tài)成員,但是可以訪問外部類的靜態(tài)成員。代碼如下:
public class Outer {
private int a = 10;
private static int b = 10;
private static class Inner {
public void printInfo() {
//這句會編譯錯誤
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
public static void main(String[] args) {
}
}
可以看到當靜態(tài)內(nèi)部類 Inner 訪問外部類的非靜態(tài)成員變量 a 時,代碼會編譯錯誤,但訪問外部類的靜態(tài)成員變量 b 時,則不會報錯。
2.3 外部類訪問靜態(tài)內(nèi)部類
外部類可以使用靜態(tài)內(nèi)部類的類名調(diào)用靜態(tài)內(nèi)部類的類成員,也可以使用靜態(tài)內(nèi)部類的實例調(diào)用內(nèi)部類的的實例成員。代碼舉例如下:
public class Outer {
private int a = 10;
private static int b = 10;
private static class Inner {
public static void staticPrintInfo() {
System.out.println("靜態(tài)方法");
}
public void printInfo() {
System.out.println("實例方法");
}
}
private static void test() {
Inner.staticPrintInfo();
new Inner().printInfo();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.test();
}
}
要注意的是,Inner 實例不可以調(diào)用 staticPrintInfo() 方法。
其實這里的難點就是要理解內(nèi)部類與外部類成員之間的訪問,現(xiàn)在把這些關(guān)系總結(jié)為一張思維導圖來幫助理解。

上圖的非就是非靜態(tài)的意思。
3. 局部內(nèi)部類
3.1 定義
局部內(nèi)部類就是把一個類放在方法內(nèi)定義。
因為局部內(nèi)部類的上一級程序單元是方法,所以不能使用 static 修飾。
并且不能使用任何訪問控制符修飾,因為它的作用域只能在方法內(nèi)。
這里就不再細說這個概念,因為在實際開發(fā)中很少使用。
4. 匿名內(nèi)部類
4.1 定義
匿名內(nèi)部類就是一種沒有名字的內(nèi)部類,它只會使用一次。
匿名內(nèi)部類必須必須是繼承一個父類或者實現(xiàn)一個接口,但最多只能繼承一個父類或?qū)崿F(xiàn)一個接口。
4.2 實現(xiàn)接口的匿名內(nèi)部類
代碼如下:
interface Person {
void eat();
}
public class AnonymousInnerClass {
public void test(Person p) {
p.eat();
}
public static void main(String[] args) {
AnonymousInnerClass a = new AnonymousInnerClass();
a.test(new Person() {
@Override
public void eat() {
System.out.println("eat someting");
}
});
}
}
可以看到 test() 方法需要一個 Person 的對象,但是可以使用匿名內(nèi)部類直接實現(xiàn) Person 接口的方法即可使用。
4.3 繼承抽象類的匿名內(nèi)部類
通過繼承抽象類來創(chuàng)建的匿名內(nèi)部類,可以使用抽象類當中的任何構(gòu)造器,并且也可以重寫抽象父類當中的普通方法。代碼如下:
abstract class Person {
private String name = "Zede";
public Person() {
}
public Person(String name) {
this.name = name;
}
public abstract void eat();
public String getName() {
return name;
}
}
public class AnonymousInnerClass {
public void test(Person p) {
p.eat();
System.out.println(p.getName());
}
public static void main(String[] args) {
AnonymousInnerClass a = new AnonymousInnerClass();
a.test(new Person() {
@Override
public void eat() {
System.out.println("eat someting");
}
});
a.test(new Person("Zede") {
@Override
public void eat() {
System.out.println("eat someting");
}
@Override
public String getName() {
return "xiaoming";
}
});
}
}
輸入結(jié)果為:
eat someting
Zede
eat someting
xiaoming
可以看到的是,繼承抽象類的匿名內(nèi)部類可以使用任何該抽象類的任何方法并且可以重寫該抽象類的普通方法。
4.4 繼承普通類的匿名內(nèi)部類
class Person {
}
public class AnonymousInnerClass {
public static void main(String[] args) {
new Person() {
public void eat() {
System.out.println("eat someting");
}
}.eat();
}
}
上述代碼可以看到,在 main() 方法中直接創(chuàng)建一個 Person 類型 的匿名內(nèi)部類,這個匿名內(nèi)部類其實就是繼承了一個 Person 類,然后在這個內(nèi)部類當中創(chuàng)建 eat() 方法。運行結(jié)果如下:
eat someting
4.5 匿名內(nèi)部類訪問局部變量
當匿名內(nèi)部類訪問局部變量的時候,這個局部變量必須使用 final 修飾。
但是在 Java 8 開始會自動幫這個局部變量使用 final 修飾。舉例代碼如下:
abstract class Person {
public abstract void getAge();
}
public class AnonymousInnerClass {
public void test(Person p) {
p.getAge();
}
public static void main(String[] args) {
int age = 18;
//這段代碼會編譯報錯
age = 20;
AnonymousInnerClass a = new AnonymousInnerClass();
a.test(new Person() {
@Override
public void getAge() {
System.out.println("age: " + age);
}
});
}
}
可以看到上面那段代碼因為 age 已經(jīng)被匿名內(nèi)部類使用了,所以會被自動使用 final 修飾,如果改變了 age 的值就會編譯報錯。
為什么匿名內(nèi)部類訪問的局部變量要用 final 修飾呢?在解答這個問題之前,首先看一下上面的代碼編譯出來的 class 文件:
AnonymousInnerClass.class:
public class AnonymousInnerClass
{
public void test(Person p)
{
p.getAge();
}
public static void main(String[] args)
{
int age = 18;
AnonymousInnerClass a = new AnonymousInnerClass();
a.test(new Person()
{
public void getAge()
{
System.out.println("age: " + this.val$age);
}
});
}
}
AnonymousInnerClass$1.class:
class AnonymousInnerClass$1
extends Person
{
AnonymousInnerClass$1(int paramInt) {}
public void getAge()
{
System.out.println("age: " + this.val$age);
}
}
可以看到 AnonymousInnerClass$1 的生命周期是對象級別的,而變量 age 的生命周期是方法級別的,也就是說局部變量 age 在方法結(jié)束后就會被銷毀,而 AnonymousInnerClass$1 還可能繼續(xù)存在。這時候 AnonymousInnerClass$1 訪問的 age 變量可能已經(jīng)被銷毀了。所以在匿名內(nèi)部類中會將引用的局部變量復制一個副本到自己的內(nèi)部當中,所以匿名內(nèi)部類訪問的其實就是這個局部變量的副本。而且為了副本和原始變量一致所以要用 final 修飾這個局部變量。
5. 內(nèi)部類一些問題
這節(jié)會介紹一些注意事項。
5.1 如果內(nèi)部類不需要訪問外部類的實例,最好把這個內(nèi)部類變成靜態(tài)內(nèi)部類
如果一個內(nèi)部類不需要訪問外部類的任何實例,這個內(nèi)部類最好是靜態(tài)內(nèi)部類。為什么呢?因為非靜態(tài)內(nèi)部類會持有外部類的對象引用,所以每次創(chuàng)建非靜態(tài)內(nèi)部類的時候,就會與外部類實例進行關(guān)聯(lián),這種操作是需要耗費時間的。
Android 的一些 Adapter 就會使用 ViewHolder 的內(nèi)部類。ViewHolder 中并沒有引用到外部類的任何實例,所以 ViewHolder 類最好使用 static 修飾。