為什麼要有內部類?都有哪些內部類?它們都適合用在什麼場景?內部類最後都會轉換為獨立的類,它們是如何轉換的?為什麼內部類可以訪問外部類的私有變數和方法?為什麼方法內部類可以訪問方法參數?但參數又為什麼必須要聲明為final? ... ...
內部類
之前我們所說的類都對應於一個獨立的Java源文件,但一個類還可以放在另一個類的內部,稱之為內部類,相對而言,包含它的類稱之為外部類。
為什麼要放到別的類內部呢?一般而言,內部類與包含它的外部類有比較密切的關係,而與其他類關係不大,定義在類內部,可以實現對外部完全隱藏,可以有更好的封裝性,代碼實現上也往往更為簡潔。
不過,內部類只是Java編譯器的概念,對於Java虛擬機而言,它是不知道內部類這回事的, 每個內部類最後都會被編譯為一個獨立的類,生成一個獨立的位元組碼文件。
也就是說,每個內部類其實都可以被替換為一個獨立的類。當然,這是單純就技術實現而言,內部類可以方便的訪問外部類的私有變數,可以聲明為private從而實現對外完全隱藏,相關代碼寫在一起,寫法也更為簡潔,這些都是內部類的好處。
在Java中,根據定義的位置和方式不同,主要有四種內部類:
- 靜態內部類
- 成員內部類
- 方法內部類
- 匿名內部類
方法內部類是在一個方法內定義和使用的,匿名內部類使用範圍更小,它們都不能在外部使用,成員內部類和靜態內部類可以被外部使用,不過它們都可以被聲明為private,這樣,外部就使用不了了。
接下來,我們逐個介紹這些內部類的語法、實現原理以及使用場景。
靜態內部類
語法
靜態內部類與靜態變數和靜態方法定義的位置一樣,也帶有static關鍵字,只是它定義的是類,示例代碼如下:
public class Outer { private static int shared = 100; public static class StaticInner { public void innerMethod(){ System.out.println("inner " + shared); } } public void test(){ StaticInner si = new StaticInner(); si.innerMethod(); } }
外部類為Outer,靜態內部類為StaticInner,帶有static修飾符。語法上,靜態內部類除了位置放在別的類內部外,它與一個獨立的類差別不大,可以有靜態變數、靜態方法、成員方法、成員變數、構造方法等。
靜態內部類與外部類的聯繫也不大(與後面其他內部類相比)。它可以訪問外部類的靜態變數和方法,如innerMethod直接訪問shared變數,但不可以訪問實例變數和方法。在類內部,可以直接使用內部靜態類,如test()方法所示。
public靜態內部類可以被外部使用,只是需要通過"外部類.靜態內部類"的方式使用,如下所示:
Outer.StaticInner si = new Outer.StaticInner(); si.innerMethod();
實現原理
以上代碼實際上會生成兩個類,一個是Outer,另一個是Outer$StaticInner,它們的代碼大概如下所示:
public class Outer { private static int shared = 100; public void test(){ Outer$StaticInner si = new Outer$StaticInner(); si.innerMethod(); } static int access$0(){ return shared; } }
public class Outer$StaticInner { public void innerMethod() { System.out.println("inner " + Outer.access$0()); } }
內部類訪問了外部類的一個私有靜態變數shared,而我們知道私有變數是不能被類外部訪問的,Java的解決方法是,自動為Outer生成了一個非私有訪問方法access$0,它返回這個私有靜態變數shared。
使用場景
靜態內部類使用場景是很多的,如果它與外部類關係密切,且不依賴於外部類實例,則可以考慮定義為靜態內部類。
比如說,一個類內部,如果既要計算最大值,也要計算最小值,可以在一次遍歷中將最大值和最小值都計算出來,但怎麼返回呢?可以定義一個類Pair,包括最大值和最小值,但Pair這個名字太普遍,而且它主要是類內部使用的,就可以定義為一個靜態內部類。
我們也可以看一些在Java API中使用靜態內部類的例子:
- Integer類內部有一個私有靜態內部類IntegerCache,用於支持整數的自動裝箱。
- 表示鏈表的LinkedList類內部有一個私有靜態內部類Node,表示鏈表中的每個節點。
- Character類內部有一個public靜態內部類UnicodeBlock,用於表示一個Unicode block。
以上這些類我們在後續文章再介紹。
成員內部類
語法
成員內部類沒有static修飾符,少了一個static修飾符,但含義卻有很大不同,示例代碼如下:
public class Outer { private int a = 100; public class Inner { public void innerMethod(){ System.out.println("outer a " +a); Outer.this.action(); } } private void action(){ System.out.println("action"); } public void test(){ Inner inner = new Inner(); inner.innerMethod(); } }
Inner就是成員內部類,與靜態內部類不同,除了靜態變數和方法,成員內部類還可以直接訪問外部類的實例變數和方法,如innerMethod直接訪問外部類私有實例變數a。成員內部類還可以通過"外部類.this.xxx"的方式引用外部類的實例變數和方法,如Outer.this.action(),這種寫法一般在重名的情況下使用,沒有重名的話,"外部類.this."是多餘的。
在外部類內,使用成員內部類與靜態內部類是一樣的,直接使用即可,如test()方法所示。與靜態內部類不同,成員內部類對象總是與一個外部類對象相連的,在外部使用時,它不能直接通過new Outer.Inner()的方式創建對象,而是要先將創建一個Outer類對象,代碼如下所示:
Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.innerMethod();
創建內部類對象的語法是"外部類對象.new 內部類()",如outer.new Inner()。
與靜態內部類不同,成員內部類中不可以定義靜態變數和方法 (final變數例外,它等同於常量),下麵介紹的方法內部類和匿名內部類也都不可以。Java為什麼要有這個規定呢?具體原因不得而知,個人認為這個規定不是必須的,Java這個規定大概是因為這些內部類是與外部實例相連的,不應獨立使用,而靜態變數和方法作為類型的屬性和方法,一般是獨立使用的,在內部類中意義不大吧,而如果內部類確實需要靜態變數和方法,也可以挪到外部類中。
實現原理
以上代碼也會生成兩個類,一個是Outer,另一個是Outer$Inner,它們的代碼大概如下所示:
public class Outer { private int a = 100; private void action() { System.out.println("action"); } public void test() { Outer$Inner inner = new Outer$Inner(this); inner.innerMethod(); } static int access$0(Outer outer) { return outer.a; } static void access$1(Outer outer) { outer.action(); } }
public class Outer$Inner { final Outer outer; public Outer$Inner(Outer outer){ ths.outer = outer; } public void innerMethod() { System.out.println("outer a " + Outer.access$0(outer)); Outer.access$1(outer); } }
Outer$Inner類有個實例變數outer指向外部類的對象,它在構造方法中被初始化,Outer在新建Outer$Inner對象時傳遞當前對象給它,由於內部類訪問了外部類的私有變數和方法,外部類Outer生成了兩個非私有靜態方法,access$0用於訪問變數a,access$1用於訪問方法action。
使用場景
如果內部類與外部類關係密切,且操作或依賴外部類實例變數和方法,則可以考慮定義為成員內部類。
外部類的一些方法的返回值可能是某個介面,為了返回這個介面,外部類方法可能使用內部類實現這個介面,這個內部類可以被設為private,對外完全隱藏。
比如說,在Java API 類LinkedList中,它的兩個方法listIterator和descendingIterator的返回值都是介面Iterator,調用者可以通過Iterator介面對鏈表遍歷,listIterator和descendingIterator內部分別使用了成員內部類ListItr和DescendingIterator,這兩個內部類都實現了介面Iterator。關於LinkedList,後續文章我們還會介紹。
方法內部類
語法
內部類還可以定義在一個方法體中,示例代碼如下所示:
public class Outer { private int a = 100; public void test(final int param){ final String str = "hello"; class Inner { public void innerMethod(){ System.out.println("outer a " +a); System.out.println("param " +param); System.out.println("local var " +str); } } Inner inner = new Inner(); inner.innerMethod(); } }
類Inner定義在外部類方法test中,方法內部類只能在定義的方法內被使用。如果方法是實例方法,則除了靜態變數和方法,內部類還可以直接訪問外部類的實例變數和方法,如innerMethod直接訪問了外部私有實例變數a。如果方法是靜態方法,則方法內部類只能訪問外部類的靜態變數和方法。
方法內部類還可以直接訪問方法的參數和方法中的局部變數,不過,這些變數必須被聲明為final,如innerMethod直接訪問了方法參數param和局部變數str。
實現原理
系統生成的兩個類代碼大概如下所示:
public class Outer { private int a = 100; public void test(final int param) { final String str = "hello"; OuterInner inner = new OuterInner(this, param); inner.innerMethod(); } static int access$0(Outer outer){ return outer.a; } }
public class OuterInner { Outer outer; int param; OuterInner(Outer outer, int param){ this.outer = outer; this.param = param; } public void innerMethod() { System.out.println("outer a " + Outer.access$0(this.outer)); System.out.println("param " + param); System.out.println("local var " + "hello"); } }
與成員內部類類似,OuterInner類也有一個實例變數outer指向外部對象,在構造方法中被初始化,對外部私有實例變數的訪問也是通過Outer添加的方法access$0來進行的。
方法內部類可以訪問方法中的參數和局部變數,這是通過在構造方法中傳遞參數來實現的,如OuterInner構造方法中有參數int param,在新建OuterInner對象時,Outer類將方法中的參數傳遞給了內部類,如OuterInner inner = new OuterInner(this, param);。在上面代碼中,String str並沒有被作為參數傳遞,這是因為它被定義為了常量,在生成的代碼中,可以直接使用它的值。
這也解釋了,為什麼方法內部類訪問外部方法中的參數和局部變數時,這些變數必須被聲明為final,因為實際上,方法內部類操作的並不是外部的變數,而是它自己的實例變數,只是這些變數的值和外部一樣,對這些變數賦值,並不會改變外部的值,為避免混淆,所以乾脆強制規定必須聲明為final。
如果的確需要修改外部的變數,可以將變數改為只含該變數的數組,修改數組中的值,如下所示:
public class Outer { public void test(){ final String[] str = new String[]{"hello"}; class Inner { public void innerMethod(){ str[0] = "hello world"; } } Inner inner = new Inner(); inner.innerMethod(); System.out.println(str[0]); } }
str是一個只含一個元素的數組。
使用場景
方法內部類都可以用成員內部類代替,至於方法參數,也可以作為參數傳遞給成員內部類。不過,如果類只在某個方法內被使用,使用方法內部類,可以實現更好的封裝。
匿名內部類
語法
匿名內部類沒有名字,在創建對象的同時定義類,語法如下:
new 父類(參數列表) { //匿名內部類實現部分 }
或者
new 父介面() { //匿名內部類實現部分 }
匿名內部類是與new關聯的,在創建對象的時候定義類,new後面是父類或者父介面,然後是圓括弧(),裡面可以是傳遞給父類構造方法的參數,最後是大括弧{},裡面是類的定義。
看個具體的代碼:
public class Outer { public void test(final int x, final int y){ Point p = new Point(2,3){ @Override public double distance() { return distance(new Point(x,y)); } }; System.out.println(p.distance()); } }
創建Point對象的時候,定義了一個匿名內部類,這個類的父類是Point,創建對象的時候,給父類構造方法傳遞了參數2和3,重寫了distance()方法,在方法中訪問了外部方法final參數x和y。
匿名內部類只能被使用一次,用來創建一個對象。它沒有名字,沒有構造方法,但可以根據參數列表,調用對應的父類構造方法。它可以定義實例變數和方法,可以有初始化代碼塊,初始化代碼塊可以起到構造方法的作用,只是構造方法可以有多個,而初始化代碼塊只能有一份。
因為沒有構造方法,它自己無法接受參數,如果必須要參數,則應該使用其他內部類。
與方法內部類一樣,匿名內部類也可以訪問外部類的所有變數和方法,可以訪問方法中的final參數和局部變數
實現原理
每個匿名內部類也都被生成為了一個獨立的類,只是類的名字以外部類加數字編號,沒有有意義的名字。上例中,產生了兩個類Outer和Outer$1,代碼大概如下所示:
public class Outer { public void test(final int x, final int y){ Point p = new Outer$1(this,2,3,x,y); System.out.println(p.distance()); } }
public class Outer$1 extends Point { int x2; int y2; Outer outer; Outer$1(Outer outer, int x1, int y1, int x2, int y2){ super(x1,y1); this.outer = outer; this.x2 = x2; this.y2 = y2; } @Override public double distance() { return distance(new Point(this.x2,y2)); } }
與方法內部類類似,外部實例this,方法參數x和y都作為參數傳遞給了內部類構造方法。此外,new時的參數2和3也傳遞給了構造方法,內部類構造方法又將它們傳遞給了父類構造方法。
使用場景
匿名內部類能做的,方法內部類都能做。但如果對象只會創建一次,且不需要構造方法來接受參數,則可以使用匿名內部類,代碼書寫上更為簡潔。
在調用方法時,很多方法需要一個介面參數,比如說Arrays.sort方法,它可以接受一個數組,以及一個Comparator介面參數,Comparator有一個方法compare用於比較兩個對象。
比如說,我們要對一個字元串數組不區分大小寫排序,可以使用Arrays.sort方法,但需要傳遞一個實現了Comparator介面的對象,這時就可以使用匿名內部類,代碼如下所示:
public void sortIgnoreCase(String[] strs){ Arrays.sort(strs, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } }); }
Comparator後面的<Stirng>與泛型有關,表示比較的對象是字元串類型,後續文章會講解泛型。
匿名內部類還經常用於事件處理程式中,用於響應某個事件,比如說一個Button,處理點擊事件的代碼可能類似如下:
Button bt = new Button(); bt.addActionListener(new ActionListener(){ @Override public void actionPerformed(ActionEvent e) { //處理事件 } });
調用addActionListener將事件處理程式註冊到了Button對象bt中,當事件發生時,會調用actionPerformed方法,並傳遞事件詳情ActionEvent作為參數。
以上Arrays.sort和Button都是上節提到的一種針對介面編程的例子,另外,它們也都是一種回調的例子。所謂回調是相對於一般的正向調用而言,平時一般都是正向調用,但Arrays.sort中傳遞的Comparator對象,它的compare方法並不是在寫代碼的時候被調用的,而是在Arrays.sort的內部某個地方回過頭來調用的。Button中的傳遞的ActionListener對象,它的actionPerformed方法也一樣,是在事件發生的時候回過頭來調用的。
將程式分為保持不變的主體框架,和針對具體情況的可變邏輯,通過回調的方式進行協作,是電腦程式的一種常用實踐。匿名內部類是實現回調介面的一種簡便方式。
小結
本節,我們談了各種內部類的語法、實現原理、以及使用場景,內部類本質上都會被轉換為獨立的類,但一般而言,它們可以實現更好的封裝,代碼上也更為簡潔。
我們一直沒有討論一個重要的問題,類放在哪裡?類文件是如何組織的?本節中,自動生成的方法如access$0沒有可見性修飾符,那可見性是什麼?這些都與包有關,讓我們下節來探討。
----------------
未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心寫作,原創文章,保留所有版權。
-----------
更多相關原創文章