1. 独自イベントが必要な場合
ボタンのクリック、マウスの移動、テキスト変更といった Java の標準イベントは、氷山の一角にすぎません。実際のアプリケーションでは、その枠に収まらない状況が数多く発生します。たとえば、プログラムがインターネットからデータを読み込み、完了時にアプリの他の部分へ通知する必要があるかもしれません。ゲームではプレイヤーが新しい実績を獲得したとき、UI やログシステムなど複数のコンポーネントに同時に知らせたいことがあります。業務アプリでは、注文状態の変更が経理、倉庫、ユーザーへの通知を同時にトリガーすべき場合があります。
このような場合には、独自のイベントとリスナーを用意するのが有効です。コンポーネント間の固有のやり取りを表現でき、コードをより柔軟かつ構造化できます。
ユーザー定義イベントの構成
独自イベントを作るには、通常次の三つを用意します:
- イベントクラス — 通常は java.util.EventObject のサブクラス。イベントの情報(発生元、イベントに関連するデータ)を保持します。
- リスナーインターフェース — 例えば MyEventListener。java.util.EventListener を拡張し、イベント処理用のメソッドを定義します。
- 購読/購読解除の仕組み — イベント発行元に add...Listener/remove...Listener を用意し、イベント発生時にリスナーを呼び出す(fire...)。
これを、ファイルやネットワークからデータを読み込み、完了時に他へ通知するアプリを例に段階的に見ていきます。
イベントクラス
読み込み完了の情報を持つイベントクラスを作成します。
import java.util.EventObject;
// イベントクラス: EventObject を継承する
public class DataLoadedEvent extends EventObject {
private final String data; // イベントの付加情報
public DataLoadedEvent(Object source, String data) {
super(source); // source: イベントを発生させたオブジェクト
this.data = data;
}
public String getData() {
return data;
}
}
ここで source はイベントの発生元(例: データローダー)、data は読み込まれたデータの文字列(ファイルパス、JSON、結果など)です。
リスナーインターフェース
リスナーのインターフェースを定義します。通常は EventListener(型付けのためのマーカーインターフェース)を拡張します。
import java.util.EventListener;
// このイベントのリスナーインターフェース
public interface DataLoadedListener extends EventListener {
void dataLoaded(DataLoadedEvent event);
}
メソッド dataLoaded はイベント発生時に呼び出されます。
イベントの発行元
リスナーの一覧を保持し、登録/削除でき、イベント発生時に通知する役割が必要です。
import java.util.ArrayList;
import java.util.List;
public class DataLoader {
private final List<DataLoadedListener> listeners = new ArrayList<>();
// リスナーの登録
public void addDataLoadedListener(DataLoadedListener listener) {
listeners.add(listener);
}
// リスナーの削除
public void removeDataLoadedListener(DataLoadedListener listener) {
listeners.remove(listener);
}
// イベントを発火するメソッド(例: データ読み込み後)
private void fireDataLoaded(String data) {
DataLoadedEvent event = new DataLoadedEvent(this, data);
// すべてのリスナーへ通知
for (DataLoadedListener listener : listeners) {
listener.dataLoaded(event);
}
}
// データを「読み込む」メソッドの例
public void loadData() {
// ファイルやネットワークからの読み込みを擬似的にシミュレート
String loadedData = "これは読み込まれたデータです!";
System.out.println("データが読み込まれました: " + loadedData);
// すべてのリスナーに通知
fireDataLoaded(loadedData);
}
}
実際にはメソッド loadData() は非同期になったり、ファイルを読んだり、サーバーへアクセスするなどしますが、ここでは単に読み込みを擬似的に行っています。
2. ユーザー定義イベントの利用
データ読み込み完了を知りたいコンポーネントがあるとします。
public class DataLoadedHandler implements DataLoadedListener {
@Override
public void dataLoaded(DataLoadedEvent event) {
System.out.println("ハンドラーがイベントを受け取りました: " + event.getData());
}
}
アプリのメインクラスで全体をつなげます。
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
DataLoadedHandler handler = new DataLoadedHandler();
// リスナーを登録
loader.addDataLoadedListener(handler);
// データの読み込みを開始
loader.loadData();
}
}
何が起こるか?
- DataLoader がデータを読み込みます(擬似処理)。
- 完了後、fireDataLoaded が呼ばれ、イベントオブジェクトを生成して全リスナーに通知します。
- ハンドラー(DataLoadedHandler)がイベントを受け取り、メッセージを出力します。
出力例:
データが読み込まれました: これは読み込まれたデータです!
ハンドラーがイベントを受け取りました: これは読み込まれたデータです!
匿名クラスとラムダ式の使用
リスナーごとに別クラスを増やさないために、しばしば匿名クラスやラムダ式(Java 8 以降)が使われます。
public class Main {
public static void main(String[] args) {
DataLoader loader = new DataLoader();
// ラムダ式によるリスナー
loader.addDataLoadedListener(event ->
System.out.println("ラムダハンドラー: " + event.getData())
);
loader.loadData();
}
}
リスナーの登録と削除
リスナーは追加・削除できます。これはメモリ管理やリーク防止の観点で重要です(特にリスナーが「重い」オブジェクト、または不要になった場合)。
DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);
// 後で、ハンドラーが不要になったら:
loader.removeDataLoadedListener(handler);
リスナーを削除しないままイベント発行元が長寿命だと、リスナーは一覧に残り、ガーベジコレクタに回収されません。これがメモリリークにつながることがあります。
3. 実践: ミニ例 — クリックカウンター
学習用アプリとして発展できる小さな例を作りましょう。値を増加させるカウンタークラスがあり、増加のたびに新しい値をリスナーに通知します。
イベントクラス
import java.util.EventObject;
public class CounterChangedEvent extends EventObject {
private final int newValue;
public CounterChangedEvent(Object source, int newValue) {
super(source);
this.newValue = newValue;
}
public int getNewValue() {
return newValue;
}
}
リスナーインターフェース
import java.util.EventListener;
public interface CounterChangedListener extends EventListener {
void counterChanged(CounterChangedEvent event);
}
カウンタークラス
import java.util.ArrayList;
import java.util.List;
public class Counter {
private int value = 0;
private final List<CounterChangedListener> listeners = new ArrayList<>();
public void addCounterChangedListener(CounterChangedListener listener) {
listeners.add(listener);
}
public void removeCounterChangedListener(CounterChangedListener listener) {
listeners.remove(listener);
}
public void increment() {
value++;
fireCounterChanged();
}
private void fireCounterChanged() {
CounterChangedEvent event = new CounterChangedEvent(this, value);
for (CounterChangedListener listener : listeners) {
listener.counterChanged(event);
}
}
}
使用例
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// ラムダでリスナーを登録
counter.addCounterChangedListener(event ->
System.out.println("カウンターが変わりました: " + event.getNewValue())
);
counter.increment(); // カウンターが変わりました: 1
counter.increment(); // カウンターが変わりました: 2
}
}
4. 独自イベントの設計・処理でよくあるミス
ミス1: リスナー通知メソッドの呼び出し忘れ。 イベントは作られているのに、リスナーへ通知するメソッド(fireDataLoaded、fireCounterChanged)を呼んでいない。結果としてリスナーは反応しません。
ミス2: リスナーのハンドラー内での例外。 あるリスナーが例外を投げると、以降のリスナーが通知を受け取れないことがあります。1 つの「悪い」リスナーが他に影響しないよう、リスナー呼び出しは try–catch で囲むのがよい実践です。
ミス3: リスナーを削除しない。 不要になったのに削除されていないと、引き続きイベントを受け取り、メモリからも消えません。とくに発行元が長寿命でリスナーが多い場合、メモリリークにつながります。
ミス4: 走査中にリスナー一覧を変更する。 イベント処理中に誰かがリスナーを追加/削除すると、ConcurrentModificationException を引き起こすことがあります。安全な方法は、まず一覧を別の配列にコピーし、そのコピーを反復することです。
ミス5: ハンドラーで時間のかかる処理を行う。 ハンドラーが長時間の処理(ファイル読み込みやネットワーク待ち)を行うと、UI が固まることがあります。重い処理は別スレッドへ移すか、非同期の仕組みを利用しましょう。
GO TO FULL VERSION