Java 內部類使用詳解

ZedeChan發表於2019-03-03

這個系列是幫助複習 Java 的基礎知識的,但是並不會按照一個特定的順序。現在開始複習下內部類的相關知識。

0. 簡介

內部類的定義很簡單,它其實就是在一個類裡面定義了另一個類,但是這個定義還是有很多細節需要掌握的。

1. 非靜態內部類

1.1 定義

非靜態內部類就是在一個類的內部裡面定義了一個沒有用 static 修飾的類。

1.2 訪問控制符

內部類的訪問控制符 訪問範圍
private 同一個類
default 同一個類,同一包中
protected 同一個類,子類,同一包中
public 任何位置

1.3 非靜態內部類訪問外部類元素

非靜態內部類是可以訪問外部類的任何例項變數和方法,包括 private 修飾的成員變數。現在用一個例子來說明一下,程式碼如下:

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();
		
	}

}
複製程式碼

輸出結果:

a = 10
Inner Call
複製程式碼

從輸出的結果就可以看到,非靜態內部類是可以訪問到外部類的任何例項變數和方法的。
那為什麼內部類可以直接訪問外部類的例項變數和方法呢?因為內部類裡面是持有外部類的例項引用,一個非靜態內部類建立時必然會有其外部類例項的引用。也就是說上面的 printInfo() 方法也可以寫成如下:

public void printInfo() {
    System.out.println("a = " + Outer.this.a);
    Outer.this.innerCall();
}
複製程式碼

上述程式碼中的 Outer.this 就是非靜態內部類持有外部類的例項引用。

1.3.1 非靜態內部類方法訪問某個變數的檢查順序

當非靜態內部類的方法訪問某個變數是按一定順序來查詢的,順序如下

  1. 在該方法的區域性變數找
  2. 方法所在的內部類找
  3. 內部類所在的外部類找
  4. 如果都沒有則編譯報錯

舉個例子,程式碼如下:


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();

	}

}

複製程式碼

以上程式碼的輸出結果就是 a = 8。
如果把 printInfo(int a) 的形參 a 去掉,則輸出的結果為 a = 9。
再把 Inner 類中的成員變數 a 去掉,則輸出的結果為 a = 10。
各位可以自己嘗試一下,這裡就不再講解。

1.4 外部類訪問非靜態內部類的元素

外部類不能直接訪問非靜態內部類的例項變數和方法,如果想要訪問的話,必須要建立非靜態內部類的例項進而呼叫該例項的成員。如下程式碼所示:



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();

	}

}


複製程式碼

以上程式碼會編譯錯誤,如果想要訪問的話只能建立 Inner 的例項訪問該方法,這裡有兩種方式,一種就是在 test() 方法裡面建立 Inner 物件,另一種就是在 main 方法建立,以下兩種方法都用程式碼試一下:

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() 即可呼叫 Inner 方法。不過在 main 方法裡要先建立 Outer 物件才可以再呼叫 new Inner()。

1.4.1 外部類的靜態方法不可以建立非靜態內部類

根據 Java 的規則,靜態成員是不可以訪問非靜態成員的。非靜態內部類其實就是類例項中的一個成員,所以在外部類的靜態方法不可以建立非靜態內部類,現用程式碼舉例:


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 非靜態內部類不可以建立靜態成員

同樣的,非靜態內部類不可以建立靜態程式碼塊,靜態方法和靜態成員變數。程式碼舉例如下:

public class Outer {

	private int a = 10;

	private class Inner {
		
		
		//以下三個靜態宣告都會編譯報錯
		static {
			
		}
		public static int a = 0;
		public static void printInfo() {
			System.out.println("a = " + a);
		}

	}

	public static void main(String[] args) {


	}

}
複製程式碼

1.5 非靜態內部類子類的規則

非靜態內部類的子類必須要存在外部類的例項引用。程式碼舉例如下:

public class SubInner extends Outer.Inner {

	public SubInner(Outer outer) {
		outer.super();
	}	
	
}
複製程式碼

這裡解釋一下為什麼非靜態內部類的子類需要外部類的例項引用,因為由以上就可以知道非靜態內部類一定會有外部類的例項引用,所以該子類也應該要有外部類的例項引用,只是這個外部類的引用是建立該子類的時候傳入的。

2. 靜態內部類

2.1 定義

靜態內部類就是在一個類的內部裡面定義了一個使用用 static 修飾的類。

2.2 靜態內部類訪問外部類

靜態內部類不可以訪問外部類的非靜態成員,但是可以訪問外部類的靜態成員。程式碼如下:

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) {


	}

}
複製程式碼

可以看到當靜態內部類 Inner 訪問外部類的非靜態成員變數 a 時,程式碼會編譯錯誤,但訪問外部類的靜態成員變數 b 時,則不會報錯。

2.3 外部類訪問靜態內部類

外部類可以使用靜態內部類的類名呼叫靜態內部類的類成員,也可以使用靜態內部類的例項呼叫內部類的的例項成員。程式碼舉例如下:


public class Outer {

	private int a = 10;
	private static int b = 10;

	private static class Inner {
		
		
		public static void staticPrintInfo() {
			
			System.out.println("靜態方法");
		
		}
		
		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 例項不可以呼叫 staticPrintInfo() 方法。

其實這裡的難點就是要理解內部類與外部類成員之間的訪問,現在把這些關係總結為一張思維導圖來幫助理解。

內部類與外部類關係.png

上圖的非就是非靜態的意思。

3. 區域性內部類

3.1 定義

區域性內部類就是把一個類放在方法內定義。
因為區域性內部類的上一級程式單元是方法,所以不能使用 static 修飾。
並且不能使用任何訪問控制符修飾,因為它的作用域只能在方法內。
這裡就不再細說這個概念,因為在實際開發中很少使用。

4. 匿名內部類

4.1 定義

匿名內部類就是一種沒有名字的內部類,它只會使用一次。
匿名內部類必須必須是繼承一個父類或者實現一個介面,但最多隻能繼承一個父類或實現一個介面。

4.2 實現介面的匿名內部類

程式碼如下:

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 的物件,但是可以使用匿名內部類直接實現 Person 介面的方法即可使用。

4.3 繼承抽象類的匿名內部類

通過繼承抽象類來建立的匿名內部類,可以使用抽象類當中的任何構造器,並且也可以重寫抽象父類當中的普通方法。程式碼如下:

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";
			}
			
		});

	}

}

複製程式碼

輸入結果為:

eat someting
Zede
eat someting
xiaoming
複製程式碼

可以看到的是,繼承抽象類的匿名內部類可以使用任何該抽象類的任何方法並且可以重寫該抽象類的普通方法。

4.4 繼承普通類的匿名內部類

class Person {
	
}

public class AnonymousInnerClass {

	public static void main(String[] args) {

		new Person() {
			public void eat() {
				System.out.println("eat someting");
			}
		}.eat();

	}

}
複製程式碼

上述程式碼可以看到,在 main() 方法中直接建立一個 Person 型別 的匿名內部類,這個匿名內部類其實就是繼承了一個 Person 類,然後在這個內部類當中建立 eat() 方法。執行結果如下:

eat someting
複製程式碼

4.5 匿名內部類訪問區域性變數

當匿名內部類訪問區域性變數的時候,這個區域性變數必須使用 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 已經被匿名內部類使用了,所以會被自動使用 final 修飾,如果改變了 age 的值就會編譯報錯。

為什麼匿名內部類訪問的區域性變數要用 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 在方法結束後就會被銷燬,而 AnonymousInnerClass$1 還可能繼續存在。這時候 AnonymousInnerClass$1 訪問的 age 變數可能已經被銷燬了。所以在匿名內部類中會將引用的區域性變數複製一個副本到自己的內部當中,所以匿名內部類訪問的其實就是這個區域性變數的副本。而且為了副本和原始變數一致所以要用 final 修飾這個區域性變數。

5. 內部類一些問題

這節會介紹一些注意事項。

5.1 如果內部類不需要訪問外部類的例項,最好把這個內部類變成靜態內部類

如果一個內部類不需要訪問外部類的任何例項,這個內部類最好是靜態內部類。為什麼呢?因為非靜態內部類會持有外部類的物件引用,所以每次建立非靜態內部類的時候,就會與外部類例項進行關聯,這種操作是需要耗費時間的。
Android 的一些 Adapter 就會使用 ViewHolder 的內部類。ViewHolder 中並沒有引用到外部類的任何例項,所以 ViewHolder 類最好使用 static 修飾。

相關文章