Swing のスレッドポリシー

Swing を使うときは、これに従えといわれます

Swing アプリケーションの構築時と表示時にイベントディスパッチスレッド(以下EDT)上でおこなうことを指示しています。具体的にはinvokeLater メソッドを使って起動するという話です。

これに従ってプログラムがどう変わるかを見てみます

まずは元のプログラム。よくあるJFrameを1つ表示するプログラムです。スレッドポリシーには従っていません。

Frame00.java

package swingEDT;
import javax.swing.*;

public class Frame00{
    public static void main(String[] args){
    	JFrame myframe = new JFrame();
    	myframe.setSize(400, 300);
    	myframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    	myframe.setVisible(true);
    }
}

現在JavaAPI「Swing のスレッドポリシー」に起動例が 2つ示されています。それに従って書き換えるとこうなります。

一つ目

Frame01.java

package swingEDT;
import javax.swing.*;

public class Frame01 implements Runnable {
    public void run() {
    	JFrame myframe = new JFrame();
    	myframe.setSize(400, 300);
    	myframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    	myframe.setVisible(true);
    }
    public static void main(String[] args){
        SwingUtilities.invokeLater(new Frame01());
    }
}

implements Runnable を強制され、run()から始めることになります。

2つ目

Frame02.java

package swingEDT;
import javax.swing.*;

public class Frame02{
    JFrame myframe;
    Frame02(String[] args) {
    	myframe = new JFrame();
    	myframe.setSize(400, 300);
    	myframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    public void show() {
    	myframe.setVisible(true);
    }
    public static void main(final String[] args){
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new Frame02(args).show();
            }
        });
    }
}

無名クラスがでてくるのは初心者にとってはハードルが高い。

私の記憶違いでなければ、この例には前のバージョンがあります。Tutorialのsampleにはこれが使われています。

3つ目ということになりますね

Frame03.java

package swingEDT;
import javax.swing.*;

public class Frame03{
    private static void createAndShowGUI() {
        JFrame myframe = new JFrame();
        myframe.setSize(400, 300);
        myframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        myframe.setVisible(true);
    }
    public static void main(String[] args){
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
}

3つ目の方法だと、機械的に出来ます。

1. いままでmain()だったメソッドをそっくりstaticなcreateAndShowGUI()にしてしまいます。

2. main()にはRunnableな無名クラスを作ってcreateAndShowGUI()を実行させるようにします。

今回使用したのは3つ目

その理由は、次のようにmain()の下に7行を加えるだけで、同じことができるからです

Frame03.java

package swingEDT;
import javax.swing.*;

public class Frame03{
    public static void main(String[] args){
        javax.swing.SwingUtilities.invokeLater(new Runnable(){   
            public void run() {                                  
                createAndShowGUI();                              
            }                                                    
        });                                                      
    }                                                            
    public static void createAndShowGUI() {                      
        JFrame myframe = new JFrame();
        myframe.setSize(400, 300);
        myframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        myframe.setVisible(true);
    }
}

TsuuchiPrev.javaは説明のためのプログラムなので、main()が肥大していきました。そしてJDialogを使うようになり、swingのポリシーに引っかかるわけです。そうすると説明の流れから、できるだけ変更が大きくないほうが良いわけです。

Frame03.javaには、Frame03のインスタンスを作る部分もないので、その便利さがいまいち出ませんが、もともとmain()しかないプログラムでしたので、staticメソッドしかないFrame03.javaは元のプログラムに近いと言えます。

TsuuchiPrev.javaに適用すると、その便利さが出ます。createAndShowGUI()以下も含めてmain()と考えれば、今まで通りに見えます。現在のTsuuchiPrev.javaはcreateAndShowGUI()にたくさんの要素が入っていますが、データの準備がファイルからの読み込みとして独立したり、プレビューが画面の切り替え機能をもって独立したりと、独立した機能をもつクラスに分けてそれを組み合わせるように発展します。そこを考えるとこの方法は柔軟に対応できる方法と言えると思います。

TsuuchiPrev.javaの骨格

public class TsuuchiPrev extends JPanel implements Printable {
    //フィールド変数
    /*コンストラクタ*/
    public TsuuchiPrev(String[] kams, int[] tans, int[] tens){
        //.......
    }
    public void paintComponent(Graphics g){
        Graphics2D g2 = (Graphics2D)g;
        drawPage(g2);
    }
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex != 0) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D)g;
        drawPage(g2);
        return PAGE_EXISTS;
    }
    /*ページ描画本体*/
    public void drawPage(Graphics2D g2){
        //.......
    }

     public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable(){   
            public void run() {                                  
                createAndShowGUI();                              
            }                                                    
        });                                                      
    }                                                            
    public static void createAndShowGUI() {                      
        //データ準備
        //用紙設定
        //プレビュー表示
        //印刷
    }
}

どうしてこんなことになったの?

プログラムの初心者にJavaを教えていたことがあります。始めたのは2003年からで、当時はサンプルもFrame00.javaのようになっていました。ひととおりCUIでクラスとかインスタンスの生成とかを扱って、GUIでも当然同様の扱いです。GUIの場合はnewで生成したインスタンスが作った数だけ目に見えるわけですから、とてもわかり易い。

ところが、いつのまにか invokeLater メソッドを使って起動せよと書き換わっていました。スレッドまで教える予定もないし無名クラスもやる予定がありません。Frame00.javaに比べて、Frame01.java以降の書き方は初心者が理解しなければならないことが多過ぎます。しかも理由が「Swing はスレッドに対して安全ではないから」というところが気に入りません。

JavaはC言語で危険とされるポインタの操作をユーザーから隠し、try-catchの記述を強制し、やってはいけないことをやれないようにするというのがポリシーだったはずです。EDTで実行しなければならないなら自動的にそうなるようにしておけばいい話です。

しかも、示された対策が汚い。プログラム記法としての美しさがないと感じます。

もちろんswingがawtを拡張してあとから増設したものであることも知っています。あとからわかった不具合なのでしょう。でもJavaのポリシーから、後々のバージョンアップで直すべきことであると私は思います。

いろいろ考えた結果、invokeLater メソッドを使って起動する方法は教えませんでした。いまでも日本の初心者向けの本やウェブの記述で invokeLater メソッドを使って説明しているものは少ないと感じています。初心者が書くレベルのものでは問題が起こらないことや、Eclipseなどの統合開発環境が自動で書いてくれるからかも知れません。中級者向けでは扱っているものはありますが、Javaのシステムの方にも責任があるという意見はほとんど出会ったことがありません。

1つだけあったJavaの方が悪いという意見

最初に引用します。

Sunは、ほとんど誰も知らない間に、GUIを始動するときの推奨手続きを変えました。 SunのGUIのチュートリアルのプログラム例は、2004年の初めに書き換えられ、短い説明が付きました。その説明を要約すると“Swingのどこかにスレッド関連のバグがあるので、GUIの構築と始動はつねにEDTから行う必要がある”。Sunはそのバグの根本原因を知っているのか、それを直す気があるのか、などの情報は、まだ(2004/10月上旬現在)どこにもありません。

これは、Thomas Weidenfeller さんがまとめた"comp.lang.java.gui FAQ"を岩谷宏さんが日本語に訳したもに含まれる「Q4.3 SwingのGUIを起動する正しいやり方は?」の答えです。現在(2017-4-24)はhttp://alga.no.coocan.jp/JavaGUIFaq19j.htmlに日本語訳はありますが、英文へのリンクは切れています。私がいつこれを見つけたかは覚えが無いですが、ウェブの情報が消えてはまずいと記録したのは2012-10-12となっています。

ゴミは透明な袋に入れて

「Swing のスレッドポリシー」に示された対策をみるとゴミの出し方の笑い話を思い出します

ある地域ではゴミは透明な袋に入れて出すことになっています。ある日、黒い袋に入れられたゴミが集積場に置いてありました。「透明な袋にいれること」と書いた紙が貼られて残されていました。次のごみ収集の日、この黒い袋はそのまま透明な袋に入れられていました。今度は持って行ってもらえました。

参考リンク

Java8のSwingのスレッドポリシー (http://docs.oracle.com/javase/8/docs/api/javax/swing/package-summary.html)

Java7のSwingのスレッドポリシー (http://docs.oracle.com/javase/jp/7/api/javax/swing/package-summary.html)

"comp.lang.java.gui FAQ"の岩谷宏さんによる日本語訳(再掲)