markdown
はじめに (対象読者・この記事でわかること)
本記事は、Javaで GUI アプリケーションを開発しているエンジニア、特に Swing や JavaFX を使っている方を対象としています。
for‑each ループ内でモーダルダイアログ(JOptionPane.showMessageDialog や Stage.showAndWait など)を複数回表示しようとした際に、すべて同じデータが表示されてしまうという典型的なバグに遭遇した経験がある方は必見です。
この記事を読むことで、以下のことが理解・実践できるようになります。
- なぜループ内で同一データしか検出されないのか、言語仕様とスコープの観点から原因を把握できる
- 正しい変数のスコープ管理と、モーダル表示のタイミング調整のベストプラクティスを学べる
- 実際のコード例をもとに、問題の再現と解決策をステップバイステップで実装できる
このバグは「変数の再利用」や「ラムダ式・匿名クラスが外部変数をキャプチャする」ことが原因で起こります。原因を突き止めずに回避策だけを探すと、後々別の不具合を呼び込む可能性がありますので、根本的な対処法をしっかり押さえておきましょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本文法(クラス、メソッド、ループ構文)
- Swing または JavaFX のモーダルダイアログの呼び出し方
- ラムダ式や匿名クラスが外部変数を キャプチャ する仕組みの概要
モーダル表示が同一データになる原因と背景
1. 変数のスコープと参照の共有
for (Item item : itemList) { … } という拡張 for 文では、item はループ毎に新しいローカル変数として生成されます。しかし、ラムダ式や匿名クラスに渡すときに その変数が「実質的に final」な形で捕捉されます。
Javafor (Item item : items) { JButton btn = new JButton("詳細"); btn.addActionListener(e -> showModal(item)); }
上記のように item を直接ラムダ式に渡すと、各ボタンが 同じ item の参照 を保持してしまうケースがあります。実際にはコンパイラが暗黙的に コピー を作りますが、参照型オブジェクト(Item が保持する内部リストやプロパティ)が変わると、全ボタンが最後に処理された item の状態を参照するようになるのです。
2. モーダルウィンドウの表示タイミング
モーダルダイアログは 表示がブロッキング されるため、ループがすべて終了した後にまとめて表示しようとすると、ループ変数は既に最後の要素を指しています。例えば以下のコードは典型的な失敗例です。
JavaList<JDialog> dialogs = new ArrayList<>(); for (Item item : items) { JDialog dlg = new JDialog(); dlg.setTitle(item.getName()); dialogs.add(dlg); } for (JDialog d : dialogs) { d.setModal(true); d.setVisible(true); // ここで全てが最後の item の情報になる }
最初のループで作成した dlg はそれぞれ別オブジェクトですが、setTitle に渡す文字列が 同一の item.getName() を参照しているため、結果としてすべて同じタイトルが表示されます。
3. Java のコンパイル時最適化
Java コンパイラは ループ変数の再利用 を最適化することがあります。特に int i のようなプリミティブ型は、同一スロットが使い回されますが、オブジェクト型でも同様に 再利用可能 なローカル変数が生成されます。その結果、デバッグ時に「変数が毎回同じ」と錯覚しやすく、バグの根源を見逃しがちです。
正しい実装方法と具体的な手順
以下では、上記で指摘した問題点を 確実に回避 できる実装パターンを示します。サンプルは Swing を対象にしていますが、JavaFX でも同様の考え方が適用できます。
ステップ 1️⃣:ループ内でローカル変数を final にする
Javafor (Item item : items) { final Item current = item; // ここで新しい final 変数を作成 JButton btn = new JButton("詳細"); btn.addActionListener(e -> showModal(current)); }
final キーワードを付与することで、ラムダ式は この変数のコピー をキャプチャします。結果として各ボタンはそれぞれ異なる Item インスタンスを保持し、正しいデータがモーダルに渡ります。
ステップ 2️⃣:モーダル表示はループ内で即座に行う
モーダルダイアログは 表示直後にブロック されるため、ループの各イテレーションで個別に表示すれば、変数の上書きは起こりません。
Javafor (Item item : items) { final Item current = item; SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog( null, "商品名: " + current.getName() + "\n価格: " + current.getPrice(), "商品詳細", JOptionPane.INFORMATION_MESSAGE ); }); }
SwingUtilities.invokeLater を使うことで、EDT (Event Dispatch Thread) 上で安全に UI を操作できます。
ステップ 3️⃣:リストを事前に作成せず、遅延評価で生成
もし多数のダイアログを一括で生成したい場合は、遅延評価(Supplier<JDialog>)を利用すると、表示時点で正しいデータが取得できます。
JavaList<Supplier<JDialog>> dialogSuppliers = new ArrayList<>(); for (Item item : items) { final Item current = item; dialogSuppliers.add(() -> { JDialog dlg = new JDialog(); dlg.setModal(true); dlg.setTitle(current.getName()); dlg.add(new JLabel("価格: " + current.getPrice())); dlg.pack(); return dlg; }); } // 必要になったときに実行 for (Supplier<JDialog> supplier : dialogSuppliers) { JDialog dlg = supplier.get(); // ここで初めて current が評価される dlg.setVisible(true); }
Supplier は「呼び出すまでインスタンスを生成しない」ので、各ダイアログが生成される瞬間に最新の current が参照されます。
ステップ 4️⃣:JavaFX での実装例
JavaFX の Alert クラスでも同様に final 変数を作ります。
Javafor (Item item : items) { final Item cur = item; Platform.runLater(() -> { Alert alert = new Alert(AlertType.INFORMATION); alert.setTitle("商品詳細"); alert.setHeaderText(cur.getName()); alert.setContentText("価格: " + cur.getPrice()); alert.showAndWait(); // モーダル表示 }); }
Platform.runLater は UI スレッドでの実行を保証し、cur が正しくキャプチャされる点がポイントです。
ハマった点やエラー解決
| 発生した問題 | 原因 | 解決策 |
|---|---|---|
| すべてのダイアログで同じ商品名が表示された | 変数 item をラムダ式で直接参照し、最後のイテレーションの値が残った |
変数を final にコピー (final Item current = item;) |
| ダイアログが表示されない、または例外が出た | UI スレッド外で JOptionPane / Alert を呼び出した |
SwingUtilities.invokeLater または Platform.runLater を使用 |
| 大量のダイアログを一括生成するとメモリ不足 | ループ内で即座に JDialog インスタンスを作成した |
Supplier<JDialog> で遅延生成し、必要時にだけインスタンス化 |
まとめ
本セクションでは、「モーダルウィンドウを foreach で回すと同じデータしか検出されない」 問題の根本原因と、安全に実装するためのベストプラクティス を体系的に解説しました。ポイントは次の通りです。
- ループ変数は
finalにコピーしてラムダ式に渡す - モーダル表示は ループ内で即時に行う、または遅延評価 (
Supplier) を活用 - UI スレッド(EDT / JavaFX Application Thread)での操作は必ず
invokeLater系メソッドでラップ
これらを守るだけで、同一データが繰り返し表示されるバグは確実に回避できます。
まとめ
本記事では、Java の for‑each ループ内でモーダルダイアログを表示した際に 同一データが繰り返し表示されてしまう 現象の原因を、変数スコープやラムダ式のキャプチャ、表示タイミングの観点から徹底的に解説しました。
- 変数を
finalにして コピーを作る ことで正しい参照を保持 - UI スレッドで安全に表示するために
invokeLater/runLaterを使用 - 必要に応じて
Supplierで 遅延生成 する手法を活用
これらの対策を実装すれば、モーダル表示が期待通りに動作し、デバッグに費やす時間も大幅に削減できます。次回は、モーダルウィンドウのカスタマイズや非同期処理との組み合わせについても紹介する予定です。
参考資料
- Swing の公式ドキュメント – Dialogs
- JavaFX の公式ガイド – Alerts
- Effective Java 第3版 – アイテム 70: オブジェクトの参照を漏らさない
- Stack Overflow: Lambda captures variable in loop
