Java 內部類綜述
摘要:
多重繼承指的是一個類可以同時從多於一個的父類那裏繼承行為和特征,然而我們知道Java為了保證數據安全,它只允許單繼承。但有時候,我們確實是需要實現多重繼承,而且現實生活中也真正地存在這樣的情況,比如遺傳:我們即繼承了父親的行為和特征也繼承了母親的行為和特征。可幸的是,Java 提供了兩種方式讓我們曲折地來實現多重繼承:接口和內部類。事實上,實現多重繼承是內部類的一個極其重要的應用。除此之外,內部類還可以很好的實現隱藏(例如,私有成員內部類)。內部類共有四種類型,即成員內部類、靜態內部類、局部內部類和匿名內部類。特別地,
-
成員內部類:成員內部類是外圍類的一個成員,是 依附於外圍類的
-
靜態內部類:靜態內部類,就是修飾為 static 的內部類,該內部類對象 不依賴於外部類對象,就是說我們可以直接創建內部類對象,但其只可以直接訪問外部類的所有靜態成員和靜態方法;
-
局部內部類:局部內部類和成員內部類一樣被編譯,只是它的 作用域發生了改變,它只能在該方法和屬性中被使用,出了該方法和屬性就會失效;
-
匿名內部類:定義匿名內部類的前提是,內部類必須要繼承一個類或者實現接口,格式為 new 父類或者接口(){定義子類的內容(如函數等)}。也就是說,匿名內部類最終提供給我們的是一個匿名子類的對象。
一. 內部類概述
1、 內部類基礎
內部類指的是在一個類的內部所定義的類,類名不需要和源文件名相同。內部類是一個編譯時的概念,一旦編譯成功,內部類和外部類就會成為兩個完全不同的類。例如,對於一個名為 Outer 的外部類和在其內部定義的名為 Inner 的內部類,在編譯完成後,會出現 Outer.class 和 Outer$inner.class 兩個類。因此,內部類的成員變量/方法名可以和外部類的相同。內部類可以是靜態static的,也可用 public,default,protected 和 private 修飾。 特別地,關於 Java源文件名與類名的關系( java源文件名的命名與內部類無關,以下3條規則中所涉及的類和接口均指的是外部類/接口),需要符合下面三條規則:
-
如果java源文件包含public類(public接口),則源文件名必須與public類名(public接口名)相同。
一個java源文件中,如果有public類或public接口,那麽就只能有一個public類或一個public接口,不能有多個public的類或接口。當然,一個java源文件中可以有多個包可見的類或接口,即默認訪問權限修飾符(類名前沒有訪問權限修飾符)。public類(接口) 與 包可見的類(接口)在文件中的順序可以隨意,即public類(接口)可以不在第一個的位置。 -
如果java源文件不包含public類(public接口),則java源文件名沒有限制。
只要符合文件名的命名規範就可以,可以不與文件中任一個類或接口同名,當然,也可以與其中之一同名。 -
類和接口的命名不能沖突。
同一個包中的任何一個類或接口的命名都不能相同。不同包中的類或接口的命名可以相同,因為通過包可以把它們區分開來。
2、 內部類的作用
使用內部類可以給我們帶來以下優點:
-
內部類可以很好的實現隱藏(一般的非內部類,是不允許有 private 與 protected 權限的,但內部類可以);
-
內部類擁有外圍類的所有元素的訪問權限;
-
可以實現多重繼承;
-
可以避免修改接口而實現同一個類中兩種同名方法的調用。
1)內部類可以很好的實現隱藏
平時我們對類的訪問權限,都是通過類前面的訪問修飾符來限制的,一般的非內部類,是不允許有 private 與 protected 權限的,但內部類可以,所以我們能通過內部類來隱藏我們的信息。可以看下面的例子:
//測試接口
public interface InterfaceTest {
public void test();
}
//外部類
public class Example {
//內部類
private class InnerClass implements InterfaceTest{
@Override
public void test() {
System.out.println("I am Rico.");
}
}
//外部類方法
public InterfaceTest getInnerInstance(){
return new InnerClass();
}
}
//客戶端
public class Client {
public static void main(String[] args) {
Example ex = new Example();
InterfaceTest test = ex.getInnerInstance();
test.test();
}
}/* Output:
I am Rico.
*///:~
對客戶端而言,我們可以通過 Example 的getInnerInstance()方法得到一個InterfaceTest 實例,但我們並不知道這個實例是如何實現的,也感受不到對應的具體實現類的存在。由於 InnerClass 是 private 的,所以,我們如果不看源代碼的話,連實現這個接口的具體類的名字都看不到,所以說內部類可以很好的實現隱藏。
2)內部類擁有外圍類的所有元素的訪問權限
//外部類
public class Example {
private String name = "example";
//內部類
private class Inner{
public Inner(){
System.out.println(name); // 訪問外部類的私有屬性
}
}
//外部類方法
public Inner getInnerInstance() {
return new Inner();
}
}
//客戶端
public class Client {
public static void main(String[] args) {
Example ex = new Example();
ex.getInnerInstance();
}
}/* Output:
example
*///:~
name 這個成員變量是在Example裏面定義的私有變量,這個變量在內部類中可以被無條件地訪問。
3)可以實現多重繼承
對多重繼承而言,可以這樣說,接口只是解決了部分問題,而內部類使得多重繼承的解決方案變得更加完整。內部類使得Java的繼承機制更加完善,是內部類存在的最大理由。Java中的類只能繼承一個類,它的多重繼承在我們沒有學習內部類之前是用接口來實現的。但使用接口有時候有很多不方便的地方,比如,我們實現一個接口就必須實現它裏面的所有方法;而內部類可以使我們的類繼承多個具體類或抽象類,規避接口的限制性。看下面的例子:
//父類Example1
public class Example1 {
public String name() {
return "rico";
}
}
//父類Example2
public class Example2 {
public int age() {
return 25;
}
}
//實現多重繼承的效果
public class MainExample {
//內部類Test1繼承類Example1
private class Test1 extends Example1 {
public String name() {
return super.name();
}
}
//內部類Test2繼承類Example2
private class Test2 extends Example2 {
public int age() {
return super.age();
}
}
public String name() {
return new Test1().name();
}
public int age() {
return new Test2().age();
}
public static void main(String args[]) {
MainExample mexam = new MainExample();
System.out.println("姓名:" + mexam.name());
System.out.println("年齡:" + mexam.age());
}
}/* Output:
姓名:rico
年齡:25
*///:~
註意到類 MainExample,在這個類中,包含兩個內部類 Test1 和 Test2。其中,類Test1繼承了類Example1,類Test2繼承了類Example2。這樣,類MainExample 就擁有了 類Example1 和 類Example2 的方法,也就間接地實現了多繼承。
4) 避免修改接口而實現同一個類中兩種同名方法的調用
考慮這樣一種情形,一個類要繼承一個類,還要實現一個接口,可是它所繼承的類和接口裏面有兩個相同的方法(方法簽名一致),那麽我們該怎麽區分它們呢?這就需要使用內部類了。例如,
//Test 所實現的接口
public interface InterfaceTest {
public void test();
}
//Test 所實現的類
public class MyTest {
public void test(){
System.out.println("MyTest");
}
}
//不使用內部類的情形
public class Test extends MyTest implements InterfaceTest{
public void test(){
System.out.println("Test");
}
}
此時,Test中的 test() 方法是屬於覆蓋 MyTest 的 test() 方法呢,還是實現 InterfaceTest 中的 test() 方法呢?我們怎麽能調到 MyTest 這裏的方法?顯然這是不好區分的。而我們如果用內部類就很好解決這一問題了。看下面代碼:
//Test 所實現的接口
public interface InterfaceTest {
public void test();
}
//Test 所實現的類
public class MyTest {
public void test(){
System.out.println("MyTest");
}
}
//使用內部類的情形
public class AnotherTest extends MyTest {
private class InnerTest implements InterfaceTest {
@Override
public void test() {
System.out.println("InterfaceTest");
}
}
public InterfaceTest getCallbackReference() {
return new InnerTest();
}
public static void main(String[] args) {
AnotherTest aTest = new AnotherTest();
aTest.test(); // 調用類MyTest 的 test() 方法
aTest.getCallbackReference().test(); // 調用InterfaceTest接口中的 test() 方法
}
}
通過使用內部類來實現接口,就不會與外圍類所繼承的同名方法沖突了。
3、 內部類的種類
在Java中,內部類的使用共有兩種情況:
(1) 在類中定義一個類(成員內部類,靜態內部類);
(2) 在方法中定義一個類(局部內部類,匿名內部類)。
二. 成員內部類
1、定義與原理
成員內部類是最普通的內部類,它是外圍類的一個成員,在實際使用中,一般將其可見性設為 private。成員內部類是依附於外圍類的,所以,只有先創建了外圍類對象才能夠創建內部類對象。也正是由於這個原因,成員內部類也不能含有 static 的變量和方法,看下面例子:
public class Outter {
private class Inner {
private final static int x=1; // OK
/* compile errors for below declaration
* "The field x cannot be declared static in a non-static inner type,
* unless initialized with a constant expression" */
final static Inner a = new Inner(); // Error
static Inner a1=new Inner(); // Error
static int y; // Error
}
}
如果上面的代碼編譯無誤, 那麽我們就可以直接通過 Outter.Inner.a 拿到內部類Inner的實例。 由於內部類的實例一定要綁定到一個外部類的實例的,所以矛盾。因此,成員內部類不能含有 static 變量/方法。此外,成員內部類與 static 的關系還包括:
- 包含 static final 域,但該域的初始化必須是一個常量表達式;
- 內部類可以繼承含有static成員的類。
2、交互
成員內部類與外部類的交互關系為:
- 成員內部類可以直接訪問外部類的所有成員和方法,即使是 private 的;
- 外部類需要通過內部類的對象訪問內部類的所有成員變量/方法。
//外部類
class Out {
private int age = 12;
private String name = "rico";
//內部類
class In {
private String name = "livia";
public void print() {
String name = "tom";
System.out.println(age);
System.out.println(Out.this.name);
System.out.println(this.name);
System.out.println(name);
}
}
// 推薦使用getxxx()來獲取成員內部類的對象
public In getInnerClass(){
return new In();
}
}
public class Demo {
public static void main(String[] args) {
Out.In in = new Out().new In(); // 片段 1
in.print();
//或者采用註釋內兩種方式訪問
/*
* 片段 2
Out out = new Out();
out.getInnerClass().print(); // 推薦使用外部類getxxx()獲取成員內部類對象
Out.In in = out.new In();
in.print();
*/
}
}/* Output:
12
rico
livia
tom
*///:~
對於代碼片段 1和2,可以用來生成內部類的對象,這種方法存在兩個小知識點需要註意:
1) 開頭的 Out 是為了標明需要生成的內部類對象在哪個外部類當中;
2) 必須先有外部類的對象才能生成內部類的對象。
因此,成員內部類,外部類和客戶端之間的交互關系為:
- 在成員內部類使用外部類對象時,使用 outer.this 來表示外部類對象;
- 在外部類中使用內部類對象時,需要先進行創建內部類對象;
- 在客戶端創建內部類對象時,需要先創建外部類對象。
特別地,對於成員內部類對象的獲取,外部類一般應提供相應的 getxxx() 方法。
3、私有成員內部類
如果一個成員內部類只希望被外部類操作,那麽可以使用 private 將其聲明私有內部類。例如,
class Out {
private int age = 12;
private class In {
public void print() {
System.out.println(age);
}
}
public void outPrint() {
new In().print();
}
}
public class Demo {
public static void main(String[] args) {
/*
* 此方法無效
Out.In in = new Out().new In();
in.print();
*/
Out out = new Out();
out.outPrint();
}
}/* Output:
12
*///:~
在上面的代碼中,我們必須在Out類裏面生成In類的對象進行操作,而無法再使用Out.In in = new Out().new In() 生成內部類的對象。也就是說,此時的內部類只對外部類是可見的,其他類根本不知道該內部類的存在。
三. 靜態內部類
1、定義與原理
靜態內部類,就是修飾為 static 的內部類,該內部類對象不依賴於外部類對象,就是說我們可以直接創建內部類對象。看下面例子:
2、交互
靜態內部類與外部類的交互關系為:
- 靜態內部類可以直接訪問外部類的所有靜態成員和靜態方法,即使是 private 的;
- 外部類可以通過內部類對象訪問內部類的實例成員變量/方法;對於內部類的靜態域/方法,外部類可以通過內部類類名訪問。
3、成員內部類和靜態內部類的區別
成員內部類和靜態內部類之間的不同點包括:
-
靜態內部類對象的創建不依賴外部類的實例,但成員內部類對象的創建需要依賴外部類的實例;
-
成員內部類能夠訪問外部類的靜態和非靜態成員,靜態內部類不能訪問外部類的非靜態成員;
四. 局部內部類
1、定義與原理
有這樣一種內部類,它是嵌套在方法和作用域內的,對於這個類的使用主要是應用與解決比較復雜的問題,想創建一個類來輔助我們的解決方案,但又不希望這個類是公共可用的,所以就產生了局部內部類。局部內部類和成員內部類一樣被編譯,只是它的作用域發生了改變,它只能在該方法和屬性中被使用,出了該方法和屬性就會失效。
// 例 1:定義於方法內部
public class Parcel4 {
public Destination destination(String s) {
class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() {
return label;
}
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Destination d = p.destination("Tasmania");
}
}
// 例 2:定義於作用域內部
public class Parcel5 {
private void internalTracking(boolean b) {
if (b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() {
return id;
}
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
}
public void track() {
internalTracking(true);
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
p.track();
}
}
2、final 參數
對於final參數,若是將引用類型參數聲明為final,我們無法在方法中更改參數引用所指向的對象;若是將基本類型參數聲明為final,我們可以讀參數,但卻無法修改參數(這一特性主要用來向局部內部類和匿名內部類傳遞數據)。
如果定義一個局部內部類,並且希望它的方法可以直接使用外部定義的數據,那麽我們必須將這些數據設為是 final 的;特別地,如果只是局部內部類的構造器需要使用外部參數,那麽這些外部參數就沒必要設置為 final,例如:
五. 匿名內部類
有時候我為了免去給內部類命名,便傾向於使用匿名內部類,因為它沒有名字。匿名內部類的使用需要註意以下幾個地方:
-
匿名內部類是沒有訪問修飾符的;
-
匿名內部類是沒有構造方法的 (因為匿名內部類連名字都沒有);
-
定義匿名內部類的前提是,內部類必須是繼承一個類或者實現接口,格式為 new 父類或者接口(){子類的內容(如函數等)}。也就是說,匿名內部類最終提供給我們的是一個匿名子類的對象,例如:
// 例 1
abstract class AbsDemo
{
abstract void show();
}
public class Outer
{
int x=3;
public void function()//可調用函數
{
new AbsDwmo()//匿名內部類
{
void show()
{
System.out.println("x==="+x);
}
void abc()
{
System.out.println("haha");
}
}.abc(); //匿名內部類調用函數,匿名內部類方法只能調用一次
}
}
// 例 2
interface Inner { //註釋後,編譯時提示類Inner找不到
String getName();
}
public class Outer {
public Inner getInner(final String name, String city) {
return new Inner() {
private String nameStr = name;
public String getName() {
return nameStr;
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.getInner("Inner", "gz");
System.out.println(inner.getName());
System.out.println(inner instanceof Inner); //匿名內部類實質上是一個匿名子類的對象
} /* Output:
Inner
true
*///:~
}
- 若匿名內部類 (匿名內部類沒有構造方法) 需要直接使用其所在的外部類方法的參數時,該形參必須為 final 的;如果匿名內部類沒有直接使用其所在的外部類方法的參數時,那麽該參數就不必為final 的,例如:
// 情形 1:匿名內部類直接使用其所在的外部類方法的參數 name
public class Outer {
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.getInner("Inner", "gz");
System.out.println(inner.getName());
}
public Inner getInner(final String name, String city) { // 形參 name 被設為 final
return new Inner() {
private String nameStr = name; // OK
private String cityStr = city; // Error: 形參 city 未被設為 final
public String getName() {
return nameStr;
}
};
}
}
// 情形 2:匿名內部類沒有直接使用其所在的外部類方法的參數
public class Outer {
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.getInner("Inner", "gz");
System.out.println(inner.getName());
}
//註意這裏的形參city,由於它沒有被匿名內部類直接使用,而是被抽象類Inner的構造函數所使用,所以不必定義為final
public Inner getInner(String name, String city) {
return new Inner(name, city) { // OK,形參 name 和 city 沒有被匿名內部類直接使用
private String nameStr = name;
public String getName() {
return nameStr;
}
};
}
}
abstract class Inner {
Inner(String name, String city) {
System.out.println(city);
}
abstract String getName();
}
從上述代碼中可以看到,當匿名內部類直接使用其所在的外部類方法的參數時,那麽這些參數必須被設為 final的。為什麽呢?本文所引用到的一篇文章是這樣解釋的:
“這是一個編譯器設計的問題,如果你了解java的編譯原理的話很容易理解。首先,內部類被編譯的時候會生成一個單獨的內部類的.class文件,這個文件並不與外部類在同一class文件中。當外部類傳的參數被內部類調用時,從java程序的角度來看是直接的調用,例如:
public void dosome(final String a,final int b){
class Dosome{
public void dosome(){
System.out.println(a+b)
}
};
Dosome some=new Dosome();
some.dosome();
}
從代碼來看,好像是內部類直接調用的a參數和b參數,但是實際上不是,在java編譯器編譯以後實際的操作代碼是:
class Outer$Dosome{
public Dosome(final String a,final int b){
this.Dosome$a=a;
this.Dosome$b=b;
}
public void dosome(){
System.out.println(this.Dosome$a+this.Dosome$b);
}
}
從以上代碼來看,內部類並不是直接調用方法傳進來的參數,而是內部類將傳進來的參數通過自己的構造器備份到了自己的內部,自己內部的方法調用的實際是自己的屬性而不是外部類方法的參數。這樣就很容易理解為什麽要用final了,因為兩者從外表看起來是同一個東西,實際上卻不是這樣,如果內部類改掉了這些參數的值也不可能影響到原參數,然而這樣卻失去了參數的一致性,因為從編程人員的角度來看他們是同一個東西,如果編程人員在程序設計的時候在內部類中改掉參數的值,但是外部調用的時候又發現值其實沒有被改掉,這就讓人非常的難以理解和接受,為了避免這種尷尬的問題存在,所以編譯器設計人員把內部類能夠使用的參數設定為必須是final來規避這種莫名其妙錯誤的存在。”
以上關於匿名內部類的每個例子使用的都是默認無參構造函數,下面我們介紹 帶參數構造函數的匿名內部類:
public class Outer {
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.getInner("Inner", "gz");
System.out.println(inner.getName());
}
public Inner getInner(final String name, String city) {
return new Inner(name, city) { //匿名內部類
private String nameStr = name;
public String getName() {
return nameStr;
}
};
}
}
abstract class Inner {
Inner(String name, String city) { // 帶有參數的構造函數
System.out.println(city);
}
abstract String getName();
}
特別地,匿名內部類通過實例初始化 (實例語句塊主要用於匿名內部類中),可以達到類似構造器的效果,如下:
public class Outer {
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.getInner("Inner", "gz");
System.out.println(inner.getName());
System.out.println(inner.getProvince());
}
public Inner getInner(final String name, final String city) {
return new Inner() {
private String nameStr = name;
private String province;
// 實例初始化
{
if (city.equals("gz")) {
province = "gd";
}else {
province = "";
}
}
public String getName() {
return nameStr;
}
public String getProvince() {
return province;
}
};
}
}
六. 內部類的繼承
內部類的繼承,是指內部類被繼承,普通類 extents 內部類。而這時候代碼上要有點特別處理,具體看以下例子:
可以看到,子類的構造函數裏面要使用父類的外部類對象.super() [成員內部類對象的創建依賴於外部類對象];而這個外部類對象需要從外面創建並傳給形參。
引用
談談Java的匿名內部類
java源文件名與類名的關系
Java內部類的作用
Java內部類的使用小結
Java中普通內部類為何不能有static數據和static字段,也不能包含嵌套類。
java提高篇(八)—-詳解內部類
【解惑】領略Java內部類的“內部”
java提高篇(九)—–實現多重繼承
Java 內部類綜述