淺談「Anonymous Inner Class」

Programing Language:Java Basics

RICK
7 min readAug 14, 2021

概要

記得若干年前,筆者還在學習「Java」時,當時的老師在教到「匿名內部類別」這個課題的時候是這樣說的:「在你們未來開發網站的時候,『匿名內部類別』是幾乎不會使用到的。」。

暫且不論這句話的正確性,但是⋯,筆者後來投入的是「Android」 的開發,而在「Android」開發中,「匿名內部類別」的使用無疑是相當重要的課題。

所以請容筆者向當時的老師說聲:「幹」。

正文

Anonymous Inner Class」, 中譯為「匿名內部類別」,廢話不多說,直接奉上官方文件,如下:

事實上,「匿名內部類別」也不過是類別的其中一種而已,特性與一般類別也都類似,但其最大的特點就是「匿名」,就如同文件中所描述的:「They are like local classes except that they do not have a name.」。

那為什麼「匿名」呢?因為「匿名內部類別」的目的就是為了承載那些僅需要使用一次的功能實作,所以它不需要名字,我們來看下面這個例子。

假設我們想要撰寫一段下載影片的執行緒代碼,那麼在一般情況下,我們通常會先撰寫一個下載影片的執行緒類別,如下:

public class DownloadVideoThread  extends Thread{

@Override
public void run() {
// Todo Implement Download Code
}
}

然後在 Main 方法下去執行,如下:

public static void main(String[] args) {    DownloadVideoThread thread = new DownloadVideoThread();
thread.start();
}

但若「DownloadVideoThread」這個類別是很高耦合的類別,在整個專案中僅會使用這麼一次,其它地方不會再覆用了,那麼上述的撰寫方式就顯得有些累贅了,此時,我們就可以用「匿名內部類別」來替代了,如下:

public static void main(String[] args) {    new Thread() {
@Override
public void run() {
// Todo Implement Download Code
}
}.start();
}

是不是相對簡潔許多?也因為不會再重覆使用,所以並不需要命名。

題外話, 由於「匿名內部類別」也是一種類別,所以它在編譯的過程中一樣會產生「.class」,命名規則是根據數量以「$」字號加上流水編號。

另外,關於「匿名內部類別」還有一點是值得一提的,例子如下:

public class Main {

public static void main(String[] args) {
int x = 10;

new Thread() {

@Override
public void run() {
System.out.println("PrintX: " + x);
}

}.start();

}
}

上述例子中,若將「Java JDK」的版本調整為「7」之前,就會出現錯誤提示,內容如下:

IDE 會提示我們必須在變數「x」前面加上「final」修飾,這是因為「x」屬於區域變數,而區域變數無法直接在匿名內部類別中被使用。

事實上,匿名內部類別中的「x」只是將原本變數「x」的值複製一份放到匿名內部類別中作為欄位成員來使用,但由於只是副本,所以我們不能直接在匿名內部類別中重新指定「x」的值,故此,編譯器才會要求我們必須在變數「x」前加上「final」 修飾。

但在「JDK 8」引入了「Lambda」以後,爲了撰寫的流暢性而有所放寬,所以便不在強制要求加上「final」,如下:

但這並不代表著我們就可以在「匿名內部類別」中隨意更改「外部類別」中的區域變數,一旦我們這樣做,IDE 仍會給我們一個瘟腥的錯誤提示,如下:

關於「匿名內部類別」的介紹,更詳細的資訊可以參考良葛格的「匿名內部類別」。

最後說個題外話,筆者曾經看過一份關於「Java」的面試考題,其中有個題目是這樣的:「『Anonymous Inner Class』是否可以『extends』其它類別或『implements』其它介面呢?」。

這個問題的答案是「不能」;作為一個流傳許久的面試,這樣的回答雖然沒錯,但應該是無法滿足面試官的;更完整的回答應該是:「我們不能夠『直接』的透過「extends」和「implement」來達到繼承或實作的效果,但是我們可以透過『接口』來達到類似的目的。」。

舉個例子,我們同樣以下載影片功能為例,但這次要求是我們不能直接實作s內容在「run()」方法內,而是必須根據其規格實作其方法,如下:

一般情況下,我們會這麼處理,如下:

public class Main {

public static void main(String[] args) {

DownloadVideoThread thread = new DownloadVideoThread();
thread.start();
}

static class DownloadVideoThread
extends Thread implements IDownloadVideo {

@Override
public void run() {
doDownloadVideo();
}

@Override
public void doDownloadVideo() {
// Todo Implement Download Code
}
}
}

如果換成匿名類別版本,以直接的方式撰寫結果如下:

滿江紅,故此才會說,「匿名內部類別」不能直接「繼承」或「實作」其它類別與介面,但我們可以「藉由接口」的方式來達到類似的目的,程式碼如下:

如果覺得上述有點不明所以,換成下列的撰寫方式應該會比較清楚:

簡言之,就是我們既然無法直接在「Thread()」後面加入「繼承」或「實作」關鍵字,那我們就以「接口」的形式,即包一層的方式來達到該目的。

那麼這樣做的目的是什麼呢?倘若我們直接將實作程式碼撰寫在「run()」 方法中,以當前例子其實差異不大,但如果該介面多個方法需要實作,或是要同時實作多個介面時,就會讓程式碼變得雜亂不堪。

事實上,在實際開發中,我們鮮少會把實作直接放到「run()」方法中,這違反著設計原則,因為「run()」方法是「執行『執行功能方法』」的地方,而不是「執行功能方法」,呃,有點繞口。

總之,以上述例子而言,「run()」方法是「放『執行下載影片方法』的地方,而不是「實作下載影片功能」的地方」,如果還是不理解的話,就多咀嚼幾次吧。

--

--

RICK

當遇到重開機無法解決的 BUG 時,那就試試關機吧。