最近、JavaでGUIツールを作成している…のだが。
ちょっと謎のエラーが出る時出ない時とあって、気持ち悪いので調べてみた。
その備忘録として本記事を書く。
そもそもの仕組みの理解から必要だったようなので、そこからまとめていこう。
なお、間違っているかもしれないので、その際はご指摘頂けると幸いだ。
そもそもSwingとは
Swingとは、JavaでGUI(Graphical User Interface)のアプリケーションを作成する際に使用できるツールキットである。
今まで本ブログで解説していたJavaは、全てCUI…コンソールに実行結果を表示していた。
そうではなく、新しいウィンドウを開くなどで、見た目を作り込むことができる。
初学者向けに、先に簡単な具体例を見てみよう。
ウィンドウを表示して、その中にHello World!!という文字列を表示してみる。
public class SwingTest {
public static void main(String[] args) {
FrameMaker fm = new FrameMaker();
}
}
import javax.swing.JFrame;
import javax.swing.JLabel;
public class FrameMaker {
private JFrame frame;
private JLabel label;
public FrameMaker() {
frame = new JFrame("はろーわーるど");
label = new JLabel("Hello World!!");
frame.add(label);
frame.setSize(300, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
これらを同ディレクトリに入れて、SwingTest.javaをコンパイル・実行すると、以下の画面が出てくるはずだ。
簡単に、これらの説明をしておこう。
Swingの基本事項
まず、Swingは表示する各項目を、全てJComponentというものとして扱うことができる。
今回は、ウィンドウ自体を表すJFrameクラス、そしてラベルを表すJLabelクラスを使用している。
ウィンドウはそれだけで表示されるが、JLabelは、表示するウィンドウに登録することで初めて表示される。
この二つも、ちょっと詳しく見ていこう。
JFrame
JFrameはその名の通り、フレーム…つまりウィンドウを表している。
上のFrameMakerのコンストラクタでやっていることを説明していこう。
まず9行目、newで新しいJFrameインスタンスを生成している。
このときに渡している文字列は、表示するウィンドウのタイトルになっている。
次、12行目でaddメソッドを呼び出している。
これは、このJFrameインスタンスに表示するコンポーネント(今回の場合はJLabelインスタンス)を追加している。
その次、13行目はフレームのサイズだ。一つ目が横幅、二つ目が縦幅。
14行目、これはこのフレームの×ボタンを押した時の動作を指定している。
JFrameに定数として定義されており、今回渡しているJFrame.EXIT_ON_CLOSEは、×が押されたらそのままプログラムを終了することを意味している。
最後、15行目でフレームを可視化している。
つまり、これがないと表示されないというわけだ。
JLabel
JLabelは、テキストや画像なんかを表示するためのコンポーネントだ。
今回は何を表示するかだけ設定している。
10行目でインスタンス化しているが、コンストラクタの引数が表示する文字列だ。
…このあたりはまだ本当に解説したい箇所ではないので一旦このくらいで。
どんなエラーが出たのか
以下を見て欲しい。
Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: No such child: 49
at java.awt.Container.getComponent(Container.java:334)
at javax.swing.JComponent.rectangleIsObscured(JComponent.java:4390)
at javax.swing.JComponent.paint(JComponent.java:1054)
at javax.swing.JComponent.paintToOffscreen(JComponent.java:5210)
at javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1579)
at javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1502)
at javax.swing.RepaintManager.paint(RepaintManager.java:1272)
at javax.swing.JComponent._paintImmediately(JComponent.java:5158)
at javax.swing.JComponent.paintImmediately(JComponent.java:4969)
at javax.swing.RepaintManager$4.run(RepaintManager.java:831)
at javax.swing.RepaintManager$4.run(RepaintManager.java:814)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:814)
at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:789)
at javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:738)
at javax.swing.RepaintManager.access$1200(RepaintManager.java:64)
at javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1732)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
at java.awt.EventQueue.access$500(EventQueue.java:97)
at java.awt.EventQueue$3.run(EventQueue.java:709)
at java.awt.EventQueue$3.run(EventQueue.java:703)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
このエラーが、10回に1回くらいの割合で出ていた。
まずエラーの種類から見ると、以下のようになっている。
java.lang.ArrayIndexOutOfBoundsException
これは、配列で定義した範囲外の添え字でアクセスしようとした際に起こるものだ。
次に、どこでそのエラーが出ているかだが…今回、自分で書いたものはこのどこにもない。
そして、もう一つ。ここが今回のポイントなのだが、先頭に以下のような内容が書かれている。
Exception in thread "AWT-EventQueue-0"
これは、「AWT-EventQueue-0」という名前のスレッドでエラーが出ているよということを表している。
ちなみに、自分で書いた処理が直接の原因になっている場合、以下のように出る。
public class ErrorTest {
public static void main(String[] args) {
int[] numArray = new int[5];
numArray[5] = 1;
}
}
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at ErrorTest.main(ErrorTest.java:4)
上と同じ見方でいくと、エラーの種類は同じ配列の範囲外へのアクセスだ。
場所について、at ErrorTest.main(ErrorTest.java:4)
と書いてあるので、ErrorTest.javaの4行目だと分かる。
で、ソースの4行目を見ると、確かに0~4の範囲しか定義していない配列に、5でアクセスしている。
こんなふうにエラーの個所を見つけられる…はずだった。
ちなみに、スレッドはmainとあり、これはmain関数を実行した際に作られるスレッドだ。
つまり…上で挙げたAWTなんとかというスレッドは、これとは別ということになる。
このスレッドという用語について、後の解説で必要なので説明しておこう。
スレッド
スレッドは、簡単に言ってしまうと処理の流れだ。
CUIの場合は何も考えないで組むと、基本的にシングルスレッドという形になる。
コマンドで実行した際に一つだけこのスレッドというやつが生成されて、それが順次処理を行っていく。
で、最後まで実行したらそれで終了だ。
なぜこんなことを書くかというと…これ、複数作ることもできるのだ。
その場合、二つ以上の処理が並列して動く…ように見せることができる。
厳密に言うと、ものすごく細かい時間で二つの処理を交互に進めて、並列のように見せている時もある。
で、これをするときに気を付けなければいけないことがある。
あるインスタンスについて、複数のスレッドからアクセスする場合だ。
一旦、同じインスタンスに対して、そのままでは複数スレッドからアクセスできないと思っておいて欲しい。
…このあたりは突っ込むと一本丸々書けてしまうほどの内容があるので、このくらいにしておこう。
「AWT-EventQueue-0」って何?
このあたりから、今回調べた内容になる。
このよく分からないスレッドは何者だ、ということだ。
先に名前だけ出すと、これはイベントディスパッチスレッドと呼ばれるものだ。
なお、末尾の数字は変わることがあるらしい。
イベントディスパッチスレッド
イベントディスパッチスレッドとは、GUIに対する処理や、イベントのリスナーなどを処理しているスレッドだ。
だが、今回ソースコードの中では一切自分でスレッドの作成など行っていない…と思っていた。
実は、画面描画を行い始めた瞬間に、自動で新しいスレッドが生成されるのだ。
それがイベントディスパッチスレッドと呼ばれるスレッドになる。
そして、その後は基本的に、そのイベントディスパッチスレッドから画面操作を行う必要がある、ということらしい。
で、それ以外…つまり、通常のmainスレッドからアクセスすると、同じインスタンスに複数のスレッドからアクセスすることになる。
…これは上に書いた通り、そのままではできなかったはずだ。
というわけで、エラーになる可能性がある、というのが今の私の理解だ。
上のサンプルソースについて、このスレッド観点で処理を見ていこう。
ちょっと距離が空いているので、再掲しておく。
import javax.swing.JFrame;
import javax.swing.JLabel;
public class FrameMaker {
private JFrame frame;
private JLabel label;
public FrameMaker() {
frame = new JFrame("はろーわーるど");
label = new JLabel("Hello World!!");
frame.add(label);
frame.setSize(300, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
まず、このFrameMakerクラスが、mainスレッド内でインスタンス化される。
つまり、9~15行目の処理はmainスレッド内で1回のみ行われる。
で、15行目が行われた時点で、イベントディスパッチスレッドが生成される。
ここで、mainスレッドの処理は終了、後はイベントディスパッチスレッド側になる。
ここからは、ウィンドウに対するイベントを監視している。
×ボタンが押されたらウィンドウを閉じ、アプリケーションを終了するという処理を、このイベントディスパッチスレッドが担う。
そんなふうに解釈している。
また、今回エラーが起こっていた原因は、例えばインスタンス化が終わってmain関数に処理が戻った後、そのままウィンドウサイズを変更したり、中のJLabelを書き換えたりすることによって、mainスレッドから処理をかけてしまっていたこと…だろうか。
上のサンプルで言えば、FrameMakerインスタンスのコンストラクタの処理が終わった後…厳密には、frame.setVisible(true);を実行した後に、その流れのままJLabelの中身を書き換えたら起こる可能性がある、と認識している。
以下のような例だ。
import javax.swing.JFrame;
import javax.swing.JLabel;
public class FrameMaker {
private JFrame frame;
private JLabel label;
public FrameMaker() {
frame = new JFrame("はろーわーるど");
label = new JLabel("Hello World!!");
frame.add(label);
frame.setSize(300, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
label.setText("Hello World!!!!");
}
}
この17行目が、イベントディスパッチスレッドの外(mainスレッド)から、GUIコンポーネントを操作していることになる。
これが、エラーの原因だろう。
どうすりゃいいの?
まず、mainスレッドでの設定は表示前に全て終わらせて、その後表示する。
そして、その後はイベントが発生しない限り触らない、というのが一つ目だろう。
イベント発生時はこのイベントディスパッチスレッドが処理してくれているはずなので、問題ないはず。
で、最初の画面描画でこのスレッドが作成されるなら、その前に全て終わらせればいい、という考え方。
と書いたが、別でスレッドをまた生成してしまうとNGだ。
どんな状況でも対応できるものが欲しい。
それが、あった。
使用するのは、SwingUtilitiesというクラス。
以下のように書くらしい。
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// 処理
}
});
こうすることで、イベントディスパッチスレッドに「この処理おねがい」と通知して、このスレッド自体は先に進むことができる。
で、イベントディスパッチスレッドは準備ができたら、受け取った処理を実行する。
こうすることでイベントディスパッチスレッドからの処理にできるので、問題なくなる…はず。
実際、これを使って30回くらい実行しているが、今のところ最初のエラーは出ていない。
おわりに
ちょっと今回は自分用の備忘録としての役割が強い記事になってしまった。
内容も私なりの理解を確認するためという意味も含めて書いている。
とはいえ、まだ理解不足なのも事実だ。
ここまでの内容も合っていればいいが、もし参考にする場合は他のサイトも参考にしていただきたい。
最後に参考文献リストを記載して、本記事は終わりにする。
また、今後何かわかったら追記、あるいは別記事として投稿しよう。
更新情報はTwitterでも告知する。
ページ下部から私のプロフィールを見れると思うので、気になる方はフォローして欲しい。
参考文献
・AWTとSwingのペイント(paint)の仕組み
・SwingとEDT(イベントディスパッチスレッド) -多くのサイトを見て、色々- Java | 教えて!goo
・Swing とスレッド
コメント