在撰寫Java程式時,經常會有臨時繼承某個類別或實作某個介面並建立實例的需求,由於這類子類別或介面實作類別只使用一次,不需要為這些類別定義名稱,這時可以使用匿名內部類別(Anonymous inner class)來解決這個需求。匿名內部類別的語法為:
new 父類別() | 介面() {
// 類別本體實作
};
在談 內部類別 時略談過匿名內部類別,那時以繼承Object
重新定義toString()
方法為例:
Object o = new Object() { // 繼承Object重新定義toString()並直接產生實例
@Override
public String toString() {
return "無聊的語法示範而已";
}
};
如果是實作某個介面,例如若Some
介面定義了doService()
方法,要建立匿名類別實例,可以如下:
Some some = new Some() { // 實作Some介面並直接產生實例
public void doService() {
System.out.println("作一些事");
}
};
實際上從JDK8開始,若介面僅定義一個抽象方法,可以使用Lambda來簡化這個程式的撰寫,例如:
Some some = () -> {
System.out.println("作一些事");
};
有關Lambda語法的細節,之後會有專區說明。
先來舉個介面應用的例子。假設你打算開發多人連線程式,對每個連線客戶端,都會建立Client
物件封裝相關資訊:
package cc.openhome;
public class Client {
public final String ip;
public final String name;
public Client(String ip, String name) {
this.ip = ip;
this.name = name;
}
}
程式中建立的Client
物件,都會加入ClientQueue
集中管理,若程式中其它部份希望在ClientQueue
的Client
加入或移除時可以收到通知,以便作一些處理(例如進行日誌記錄),那麼可以將Client
加入或移除的資訊包裝為ClentEvent
:
package cc.openhome;
public class ClientEvent {
private Client client;
public ClientEvent(Client client) {
this.client = client;
}
public String getName() {
return client.name;
}
public String getIp() {
return client.ip;
}
}
你可以定義ClientListener
介面,如果有物件對Client
加入ClientQueue
有興趣,可以實作這個介面:
package cc.openhome;
public interface ClientListener {
void clientAdded(ClientEvent event); // 新增Client會呼叫這個方法
void clientRemoved(ClientEvent event); // 移除Client會呼叫這個方法
}
如何在ClientQueue
新增或移除Client
時予以通知呢?直接來看程式碼:
package cc.openhome;
import java.util.ArrayList;
public class ClientQueue {
private ArrayList clients = new ArrayList();
private ArrayList listeners = new ArrayList();
public void addClientListener(ClientListener listener) {
listeners.add(listener);
}
public void add(Client client) {
clients.add(client);
ClientEvent event = new ClientEvent(client);
for(int i = 0; i < listeners.size(); i++) {
ClientListener listener = (ClientListener) listeners.get(i);
listener.clientAdded(event);
}
}
public void remove(Client client) {
clients.remove(client);
ClientEvent event = new ClientEvent(client);
for(int i = 0; i < listeners.size(); i++) {
ClientListener listener = (ClientListener) listeners.get(i);
listener.clientRemoved(event);
}
}
}
ClientQueue
會收集連線後的Client
物件,Java SE API就提供了java.util.ArrayList
,可以讓你進行物件收集,範例中使用了java.util.ArrayList
來收集Client
以及對ClientQueue
感興趣的ClientListener
。
如果有物件對Client
加入ClientQueue
有興趣,可以實作ClientListener
,並透過addClientListner()
註冊。當每個Client
透過ClientQueue
的add()
收集時,會用ArrayList
收集Client
,接著使用ClientEvent
封裝Client
相關資訊,接著使用for
迴圈將註冊的ClientListener
逐一取出,並呼叫clientAdded()
方法進行通知。如果有物件被移除,流程也是類似,這可以在ClientQueue
的remove()
方法中看到相關程式碼。
作為測試,可以使用以下的程式碼,其中使用匿名內部類別,直接建立實作ClientListener
的物件:
package cc.openhome;
public class MultiChat {
public static void main(String[] args) {
Client c1 = new Client("127.0.0.1", "Caterpillar");
Client c2 = new Client("192.168.0.2", "Justin");
ClientQueue queue = new ClientQueue();
queue.addClientListener(new ClientListener() {
@Override
public void clientAdded(ClientEvent event) {
System.out.printf("%s 從 %s 連線%n",
event.getName(), event.getIp());
}
@Override
public void clientRemoved(ClientEvent event) {
System.out.printf("%s 從 %s 離線%n",
event.getName(), event.getIp());
}
});
queue.add(c1);
queue.add(c2);
queue.remove(c1);
queue.remove(c2);
}
}
執行的結果如下所示:
justin 從 192.168.0.2 連線
caterpillar 從 127.0.0.1 離線
justin 從 192.168.0.2 離線
在JDK8之前,如果要在匿名內部類別中存取區域變數,則該區域變數必須是
final
,否則會發生編譯錯誤:必須宣告
arrs
為final
才可以通過編譯:final int[] x = {10, 20};
Object o = new Object() {
public String toString() {
return "example: " + x[0];
}
};
要瞭解為什麼,必須涉及一些底層機制。區域變數的生命週期往往比物件短,像是方法呼叫後傳回物件,區域變數生命週期就結束了,此時再透過物件嘗試存取區域變數會發生錯誤,Java的作法是採用傳值,以上例而言,實際上會在匿名內部類別的實例中,建立新的變數參考原本的物件:
int ai[] = {10, 20};
Object obj = new Object(ai) {
public String toString() {
return (new StringBuilder())
.append("example: ").append(x[0]).toString();
}
final int x[];
{
x = ai;
}
};
所以實際上你也不能改變
x
的參考,為此,編譯器才會強制你在區域變數加上final
。這個限制在JDK8中,為了配合Lambda的引進有了放寬,如果變數本身等效於final
區域變數,也就是說,如果變數不會在匿名類別中有重新指定的動作,就可以不用加上final
關鍵字。