はじめに (対象読者・この記事でわかること)

この記事は、Java SwingでGUIアプリケーションを開発しているが、テキストフィールドの変更を検知してリアルタイムで処理を実行したいと考えている中級者の方を対象としています。
記事を読むことで、DocumentListenerの正しい実装方法、無限ループを避けるためのフラグ管理、そして実用的なリアルタイム検索フィルタの完成までをマスターできます。サンプルコードはJDK 11以降で動作確認済みです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法(インターフェースの実装、匿名クラスもしくはラムダ式) - Swingコンポーネント(JFrameJTextFieldJTableなど)を使った簡単な画面構築 - イベントリスナーの仕組み(ActionListenerなど)を理解していること

DocumentEventとDocumentListenerの仕組みを理解する

SwingのJTextComponentJTextFieldJTextAreaなど)が保持するDocumentモデルは、内容が変更されるたびにDocumentEventを発行します。このイベントを受け取るためには、DocumentListenerインターフェースを実装したリスナーをDocument#addDocumentListener()で登録する必要があります。

DocumentListenerは次の3つのメソッドを要求されます。 - insertUpdate(DocumentEvent e) … 文字列が挿入されたとき - removeUpdate(DocumentEvent e) … 文字列が削除されたとき - changedUpdate(DocumentEvent e) … 属性(スタイル)が変更されたとき(通常のテキスト入力では発火しない)

ポイントは、これらのメソッドはイベントディスパッチスレッド(EDT)内で同期的に呼ばれるため、重い処理をそのまま書くとUIが固まってしまうことです。リアルタイム検索などでは、SwingWorkerや別スレッドで処理を逃すか、少なくとも「入力が止まってから○秒後」という遅延戦略を取る必要があります。

リアルタイム検索フィルタを実装してみる

ここでは、ユーザーリストをJTableで表示し、上部のJTextFieldに文字を入力するたびにリストを絞り込む「リアルタイム検索フィルタ」を作ります。最終的に下図のような画面を完成させます。

(イメージ:ウィンドウ上部に検索ボックス、下部にフィルタ後のユーザーリスト)

ステップ1:DocumentListenerを登録する

まずは最小構成でDocumentListenerを登録してみましょう。匿名クラス版とラムダ式版を両方示します。

Java
// 匿名クラス版 searchField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { filter(); } @Override public void removeUpdate(DocumentEvent e) { filter(); } @Override public void changedUpdate(DocumentEvent e) {} private void filter() { String text = searchField.getText(); System.out.println("フィルタ文字列 = " + text); // TODO: ここでテーブルをフィルタ } }); // ラムダ式版(defaultメソッドを使って1行で書くテクニック) searchField.getDocument().addDocumentListener((SimpleDocumentListener) e -> filter());

SimpleDocumentListenerは自作の関数型インターフェースです。JDK標準ではDocumentListenerが3メソッドすべてを要求するため、ラムダ式で書くには下記のようなアダプタを用意すると便利です。

Java
@FunctionalInterface public interface SimpleDocumentListener extends DocumentListener { void update(DocumentEvent e); @Override default void insertUpdate(DocumentEvent e) { update(e); } @Override default void removeUpdate(DocumentEvent e) { update(e); } @Override default void changedUpdate(DocumentEvent e) {} }

ステップ2:無限ループを回避するフラグ管理

filter()メソッドの中でsearchField.setText(...)のようにテキストフィールドの内容を書き換えると、再びDocumentEventが発火して無限ループが発生します。これを避けるため、「自分自身で変更した場合は無視する」フラグを使います。

Java
private boolean isAdjusting = false; // 自分で変更中フラグ private void filter() { if (isAdjusting) return; // 無限ループ防止 isAdjusting = true; try { // ここでDocumentを直接操作しても安全 String text = searchField.getText(); TableRowSorter<TableModel> sorter = (TableRowSorter<TableModel>) table.getRowSorter(); if (text.trim().isEmpty()) { sorter.setRowFilter(null); } else { sorter.setRowFilter(RowFilter.regexFilter("(?i)" + text)); } } finally { isAdjusting = false; } }

Document#removeDocumentListener()で一時的にリスナーを外す方法もありますが、フラグのほうがスレッドセーフで簡単です。

ステップ3:入力が止まってから遅延実行する

連打対策として、最後の入力から300 ms経過してからフィルタを実行する「遅延実行」は非常に有用です。Swingのjavax.swing.Timerを使うとEDT上で安全に遅延処理できます。

Java
private final Timer searchTimer = new Timer(300, e -> filter()); // 300 ms後に一度だけ実行 { searchTimer.setRepeats(false); // 1回だけ searchField.getDocument().addDocumentListener((SimpleDocumentListener) ev -> { searchTimer.restart(); // 入力があるたびにタイマーリセット }); }

これで、ユーザーがバシバシタイピングしても300 ms以内に再入力があればタイマーがリセットされ、最後の入力から300 ms後にのみフィルタが走ります。CPU負荷もUXも劇的に改善します。

ハマった点:例外が黙って握りつぶされる

DocumentListenerの各メソッド内で例外がスローされると、EDTのUncaughtExceptionHandlerに渡されるだけで、ログに残らないことがあります。特にRowFilter.regexFilterに無効な正規表現を渡すとPatternSyntaxExceptionが出てフィルタが無効化されるのですが、GUI上は何も起きていないように見えるため、原因特定に時間がかかります。

解決策:catchして適切にログ出力&ユーザー通知

Java
private void filter() { if (isAdjusting) return; isAdjusting = true; try { String text = searchField.getText(); TableRowSorter<?> sorter = (TableRowSorter<?>) table.getRowSorter(); if (text.trim().isEmpty()) { sorter.setRowFilter(null); } else { try { sorter.setRowFilter(RowFilter.regexFilter("(?i)" + text)); } catch (PatternSyntaxException ex) { // 正規表構文エラーを握りつぶさない LOGGER.log(Level.INFO, "不正な正規表現: " + text, ex); statusBar.setText("検索文字列が無効です"); sorter.setRowFilter(null); // フィルタ解除 } } } finally { isAdjusting = false; } }

まとめ

本記事では、SwingのDocumentEventを使ったリアルタイム検索フィルタの実装方法を解説しました。 - DocumentListenerの3メソッドの違いと、ラムダ式で簡潔に書くテクニック - 自分自身でテキストを変更するときの無限ループ回避策(フラグ管理) - タイマーを使った遅延実行でUXとパフォーマンスを両立 - 例外が黙って握りつぶされる問題とログ・ユーザー通知の重要性

この記事を通して、Swingでもモダンな「入力しながら即絞り込み」機能を安全に実装できるようになりました。次回は、JTextComponentInputMap/ActionMapを使った独自キーバインドの作り方を紹介します。

参考資料