「Javaによるテキスト処理入門」のページへ
updated: 08 Oct. 2005
「Javaの哲学」の恥かき
★2006年より、Javaプログラミング関連記事の新規掲載場所を[Javaの手帖]に統一しました。
0. What's new on this update
●マウスのダブルクリック“だけ”を検出するには?
051008: MouseListenerのメソッドの中で、マウスのクリック数を判定するためには、MouseEventのgetClickCount()を呼び出します。このメソッドが2を返したら、ユーザがダブルクリックをしたことが分かります。
ところが、ここに思わぬ落とし穴が。実は、ユーザがマウスをダブルクリックしたときでも、その最初のクリックでやはりマウスイベントが発生し、そしてもちろんそれはgetClickCount()の返し値が1、すなわちシングルクリックとして判定されてしまいます。
それでは、ダブルクリックをしたときの最初のシングルクリックが判定されないようにするためには、どうしたらいいでしょうか?。言い換えると、ユーザがダブルクリックをしたときには、マウスリスナの中のシングルクリック用の処理が実行されないようにするには、どうしたらいいのでしょうか?。
[この例題プログラム]をいじくりながら、みなさまも解を模索してみてください。
(なお、一般的には、ダブルクリックに対してはその最初のシングルクリックもそれなりに活用するような、おだやかなプログラミングスタイルが推奨されています(例:[指定(セレクト)]→[開く(オープン/実行)])。しかしここでは、ダブルクリックと通常のシングルクリックを明確に区別できる方法を考えてください。)
●requestFocus()は事実上非推奨、requestFocusInWindow()を使いましょう
050714: J2SE 1.4からのこの変更によって、本ページの以前の記述と逆の状況が発生しました。詳しくは[ここ]に。報告がたいへん遅まきで恐縮ですが、私はついきのうまで平気でrequestFocus()を使い続けていました!。
●クラスのフィールドを返すメソッド
050216: ひとの書いたプログラムのデバッグは、自分のものより手こずることがあります。
そのプログラムの、あるメソッドは、呼び出すたびに、新しい値を持つオブジェクトを返します。
私のアプリケーションは、forループの中でそのメソッドを何度も呼び出し、返されるオブジェクトをArrayListに保存しました。
しかし、毎回新しい値のオブジェクトが確かに返されているにもかかわらず、最後にはArrayListの中身がすべて同じ値のオブジェクト(ループの最後の呼び出しで返されたオブジェクト)です。
そのメソッドは、毎回、同一のフィールド(クラスのインスタンスフィールド)を、中の値だけを変えて、返していました。ですから、ArrayListに収まったのは、同一のオブジェクトの参照です。結果が、ぜーんぶ同じ値のオブジェクトになるのも、当然ですね。
教訓は、「自分でその都度新しいオブジェクトを作って返すメソッドはクラスのフィールドを返してはいけない」です。いわゆるゲッターメソッド、getXxx()は、典型的な例外ですが。
そのプログラムは、メソッドの結果を毎回System.out.println()してるだけなので、同一オブジェクトであることがばれないのでした。というか、同一オブジェクトでもよいのでした。
以上のお話は、この例題プログラムで擬似体験できます。これぐらい小さなプログラムでは、問題も最初からみえみえですが、ちょっと複雑なプログラムになると、今回の私のようにデバッグで苦戦するかもしれません。
●ArrayListのadd()とput()
050125: ArrayListやVectorなど、List系のデータ構造クラスには、インデクス位置を指定して新しいデータ項目を加えるためのメソッドにadd()とset()があります。add()はその位置への「挿入」です。そのうしろのデータは1つうしろにずれて、Listのサイズが1増えます。set()は「置換」です。その位置のデータ項目を新しいデータ項目に入れ替えます。
最初、この二つのメソッドの存在を明確に自覚していなかったために、2つのHTMLドキュメントをマージするアルゴリズムが完成するまで、足かけ三日、七転八倒しました。
●DBに依存しないサーブレット/JSP
041004: JavaサーブレットやJSPを使うアプリケーションのコード例は、本格的な関係データベースを使うものばかりで、かえって、この簡便な技術の敷居を高くしています。JavaのProperties形式など、ふつうの単純なデータファイルの読み出し/更新を行うポイントをまとめました。
●コンパイラが沈黙するエラー
041003: (私)MouseAdapterをextendsしたクラスに、public void mouseClickedメソッドを書くべきところを、public void MouseClickedメソッドを書いてしまった。(私ではない人)GUIではないふつうのプログラムのタイマ制御にjavax.swing.Timerクラスを使ってしまった。…コンパイラもランタイムも、静かに黙(だま)ーっているから(つまり一応は正しいプログラムだから)、この種のエラーに気づくまでものすごい長時間の悪戦苦闘をして、消耗することがありますね。
なお、前者のようなオーバライドメソッドのエラーは、JDK 1.5より、@Overrideアノテーションである程度は防げます。
●requestFocus()を呼ぶタイミング
040408: requestFocus()の、ちょっとした罠。
●Swingプログラミングの安全運転
040301: 以下は、サンプルプログラムを書く時間的余裕が生まれたときに、もっと正式に掲載します。今回は予告編です:
(1)javax.swing.InputVerifierクラスのverify()メソッドやshouldYieldFocus()メソッドの中で、GUIを使ったユーザとの対話など、時間のかかる処理をしようとすると、うまくいきません。時間のかかる処理を別スレッド化すればなんとかOKですが、しかしその処理は、フォーカスがInputVerifierが属するコンポーネントから別のコンポーネントに移動しようとしたときにやっと実行されるので、利用価値も限られます。
(2)C言語の危険な凶器だったポインタがJavaにはないため、Javaはずいぶん安全な言語だといえます。ところがJavaのGUI系(AWT/Swing)には、イベントキューをプログラマが操作できるという別の凶器があります。私からのアドバイスは、なにかを実現するためにイベントキューを操作したくなったら、それをせずに、それをしなくてもすむ再設計(リファクタリング)に着手してください。イベントキューをいじくっているプログラムのデバッグは、泥沼地獄になりがちです。
●正規表現の置換文字列中の'$'
java.util.regexパッケージのMatcher#appendReplacement()メソッドなどを使って、元の文字列の、正規表現にマッチした部分を別の文字列に変えようとするとき、置換文字列の中に$の文字(ドルマーク)や\
(バックスラッシュ)があると、それらをエスケープする必要があります。置換文字列が、既存のファイルやネットワークなどから読み込まれる文字列である場合に、この注意をついつい忘れがちになります。String#replaceAll/First()の置換文字列に関しても、同じ事が言えます。
●正規表現の'|'のワナ
Javaの正規表現パターンでは、|の文字(バー)をORの意味で使えます。ところがこのORを使ったパターンによるマッチは、Xa|Xb|Xcというパターンに対し、対象が最初のXaにマッチしたら、そのあとのXbやXcに対するマッチは試みません。
ですからたとえば、"1234567890"という文字列に対して、\d{5}|\d{10,}
というパターンを適用すると、マッチ文字列としては"12345"が取り出されます。
なるべく長いほうとマッチさせたかったら、複数のパターンを長い順にORでつながなければなりません。上の例では、\d{10,}|\d{5}
とすれば、マッチ文字列として"1234567890"が取り出されます。
040125: 初心者のワナに、重要な項目を二つ加えました。
●超簡単レイアウトマネージャ
040109: GUIのレイアウトというものは、コンテナの中の各コンポーネントの“位置(x/y)”と“サイズ(width/height)”を指定するだけでよいではないか?。GridBagLayoutのマスターにまたまた挫折したことをきっかけに、この長年の理想の実現に取り組みました。
●JARファイル化するアプリケーション中のリソース指定
040101: そのままアプリケーションとして実行可能なjarファイル(executable jar file)は、main()メソッドのあるクラスをマニフェストファイルの中で指定すれば作れます。しかし、通常のドキュメントにちゃんと書かれていない大きな落とし穴が…。それは、そのプログラムが使用する画像ファイルやテキストファイルなど、広義のリソースファイルです。jar化するつもりのなかった簡単なプログラムをたまたまうっかりjar化したりすると、この落とし穴にはまりがちです。新年早々の恥かき経験からの教訓を、お読みください。
●Swingにおけるインクリメンタルpaint
031219: プロッターやお絵かきプログラムに代表される連続描画は、java.awtのコンポーネントの上では簡単にできますが、Swing(javax.swing)のコンポーネントの上では「それまで描かれたものの保存と再描画」と「背景色の保存再生」の両立が意外と難物なので、awtの場合とは違う連続描画方法を採らなければなりません。そのことの結論と解説。
031214: 初心者が書いたプログラムの中で、文字列の比較に==演算子を使っていました(正しくはequals()メソッドを使わなければならない)。しかしそのプログラムは、正常に動作しました。その不思議な事件の[解説]。
031213: Linuxの上では、キーボード上のキーをずうっと押し続けると、aaaaaaaaaaaaaaaaみたいにkey-pressedとkey-releasedイベントが交代で急速に生成されます。ですから、そのようなアプリケーションのKeyListenerは、keyReleased()メソッドを空実装にしたほうが無難です(解説)。連続押し下げのときのkey-releasedイベントは無視せよ、というユーザの意見もあります:[例1], [例2]。
031205: 自作のクラスには、equals()とhashCode()の両方を--Objectクラスのメソッドのシグネチャ(形式)どおりに--正しく実装したほうがよい場合があります。そのことの解説。
031125: 初心者のとまどい:パッケージに属するクラスのクラスパスは?。体験的解説を設けました。
031116: 3.情報源案内、に、最新サイトをひとつ追加。
031109: コンパイラjavacのエラーメッセージの一覧を作りました。【解説】は、まだほんのちょっとしか書いていません(今後にご期待を!)。日本語よりも英語のほうが、意味が明確に分かるものが多いです。
031102: Javaの浮動小数点数の怪と、gccのinclude <math.h>の七転八倒。
031028: 恥かきバグ…サイコロを3回(または3つのサイコロ)を振った結果をJavaの乱数でシミュレーションする。
031024: java.util.Observableオブジェクトの正しい使い方。
031024: あるクラスのインナークラスのオブジェクトを、ほかのクラスから使う方法。
030824: ひとつのJTabbedPaneの中の複数のタブページが、同じコンポーネントを“共用”することは、ご法度!(解説)。
030814: JOptionPane.showInputDialog()の怪と対策。
030809: クラスのどのメソッドを使うべきか?。ページ内リンクはこれ。
030722: Java World(*)の初心者向けフォーラムが、6月下旬に新装再開店しました。Java Beginnerと、Theory & Practiceを訪ねてみましょう。質問者の多くがインド人または中国人、日本人は皆無、というところがおもしろいですねー。日本の未来を、暗示しているようですねー。
p149, 4行目の:
MyClass[] a = (MyClass[])(c.toArray());
は間違いです(ClassCastExceptionが投げられてしまいます)。正しくは、toArray()から得られるObject[]の個々の成分をMyClassオブジェクトにキャストしなければなりません。この簡単なプログラム例を見てください。以上、大津市の舩木さんより、メールでご指摘いただきました。多謝!!
ぶつブツぶつブツ
おい、Javaランタイムさんよ。Objectのキャストを認めるっちゅう
ことは、そのObjectがMyClassオブジェクトであることを、あんた、事
前に知ってるわけや。でなきゃ、instanceof演算子なんか無意味やも
んね。しかし、それなら、そのObject[]が、実質、MyClass[]であるこ
とも、当然、分かっとるわけやん。なのに、キャストを認めんのは、
ちょっと、いや、相当、おかしいで。ブツぶつブツぶつ…。
p.s. 無引数のtoArray()メソッドではなく、Objectの配列を引数に取るtoArray()は、引数のタイプと同タイプの配列を返します。これまた、おかしなメソッドでありますが…。
JNI(Java Native Interface)は、ネイティブコード側のメモリエラーに対して無難な優雅な対応ができませんから、重要なアプリケーションでは使うべきではありません。いわゆる古参のベテランのソフトウェア技術者(既存コード資産の多い人)は、安易にJNIを使いがちですから、気を付けましょう。
こことここを、推奨情報サイトとして追加します。玉石混交のページはこんなのやこんなの。
最近出現したここは、多様なテーマのフォーラム(BBS, 掲示板)の集合体です(なにしろ、ものすごく数が多い!)。このサイトはCGIを使っていますが、その理由は、「Javaサーブレットの登場以前に作ったサイトだから」ということです。
【Javaプログラミング超初心者のかたは】
超初心者が、どんな質問でも、恥ずかしがらずにポストできるのが、ここです。このようなレベルのフォーラムは珍しく、稀少価値があります!。
そして、伝統的な巨大フォーラムが、[ここ]と[ここ]。これらは、比叡山延暦寺と高野山金剛峰寺か、それとも西本願寺と東本願寺か?(ばかなムダ口)。
●2003年後半、最新のJava情報サイトが[ここ]。
△■●★☆◇ここまでくると、Javaトリビアかっ??, Javaカルトかっ???、Javaフェチかっ????。とにかく、おもしろさに脱帽:[JavaTips], [Marco Schmidt氏], [Java Glossary]。
Setオブジェクトに与えるComparatorのcompare()メソッドや、Setに収める成分のオブジェクト(そのクラスがimplements Comparable)のcompareTo()メソッドは、“二つの成分が同じ”(メソッドの返し値が0)という結果を返してはいけません。とくにそれを意図した場合を除き…。
BufferedWriterは明示的にclose()ないしflush()しないと、プログラム終了時にバッファのフラッシュ(バッファの全内容の書き出し)が保証されません。以上、私の大失敗からの報告でした!。
漢字の文字コードの大小順は、JIS, EUC-JP, Shift_JIS(この三者は同じ)とUnicodeではじぇんじぇん違いますので、プラットホーム上でsortした漢字文字列の配列をJavaプログラムの上でArrays.binarySearch()などで使ってはいけません。事前にJavaの上でArrys.sort()等してから使いませう。
また、JIS, EUC-JP, Shift_JISの漢字コードの並びもおかしいので、"第三の", "第四の","第一の", "第二の", "第五の", といった文字列をソートしても、第一、二、三、四、五、という順にはなりません。
文字コードの規格を決めている人たちは、一体、何を想定して決めているのやら…
ブロードバンドの普及とともに、自前でサーバを動かせるかたが増えているでしょう。うらやましいですね。自前でサーバを動かせる人は、古くさいプロバイダに依存している人と違って、Javaを使えます。CGIという古い技術と悪戦苦闘せずにすみます。この短い入門記事をまず読んでみましょう。
Fileオブジェクトの背後の実体がディレクトリであるとき、そのディレクトリ内のファイルやサブディレクトリ〜サブサブディレクトリ内のすべてのファイルを一括的に処理したいことがあります。そのためには、ディレクトリであるFileのlist()を取得するところまでは分かりますが、そのリスト内の個々の項目に対する正しい処理の書き方は、いろいろ模索してみてやっと分かりました。
正しい結論は、「リスト内の項目から新たなFileを作るときは、親の(つまり現ディレクトリの)パス名もコンストラクタに渡す」であります(※)。この実験用プログラムを動かしてみてください。
〔※:JDK 1.2からは、Fileの配列が得られるlistFiles()
メソッドがFileクラスに加わったので、この話題もやや
古い話題になってしまいました。〕
ついでに分かったことは、FileクラスにgetAbsolutePath()メソッドがあるのに対し、現在のgetPath()メソッドは、むしろgetRelativePath()という名前だったほうが、ユーザにとって分かりやすかったでしょう。getPath()は、カレントディレクトリをベースとする、まさしく相対パス(最後のファイル名〜ディレクトリ名付き)を返します。対してgetName()は、単独のファイル名〜ディレクトリ名だけを返します。そして、本当に絶対パスがほしいときは、getAbsolutePath()よりもgetCanonicalPath()を使ったほうが良さそうです("."や".."が後者では省略されます)。
この学習結果に基づいて、ディレクトリ内のすべてのファイルの相対パス名のリストをIteratorとして提供するクラスを作りました。そしてこのクラスを利用するアプリケーションの試作例も、ついでに見てください。
intの変数等をオブジェクトとして扱いたい/扱わなくてはならない(たとえばjava.util.Collection系のデータ構造を使いたい)ときは、intのラッパータイプであるIntegerを使います。このほか、floatのラッパータイプはFloat、等々であります。
メソッド、とくに、別オブジェクトのメソッドに、引数として渡したint変数の値を変えてほしいときも、intそのものを渡したのでは呼び出し側の値を変えてもらうことはできません。メソッドには、そのintのコピーが渡されるからです(それを“値渡し”と呼ぶ)。
Integerの参照をメソッドに渡せば、(メソッドに渡される参照のコピーは呼び出し側と同じオブジェクトを指していますから)、メソッド側で呼び出し側のオブジェクトそのものをいじれます。ただしもちろん、呼び出し側・メソッド側ともに、その参照に新たに別のオブジェクトを代入したりしてはいけません。それをすると、“同じオブジェクトを指している”という状況が崩れます(参照変数へのfinal修飾子が、安全のために有効でしょう---JLS 2nd ed. 4.5.4)。
さて、Java歴8年になろうとする私がごく最近気づいたのは、「Integer等のラッパータイプには、それがラップしているint等の値をset, increment, decrement等するメソッドが全然ない」という事実であります。ですから、メソッドにIntegerの参照を渡しても、呼び出し側のintの値をそのメソッド様に変えてもらうことができません。
でありますから、GUIのイベントハンドラメソッドなどに、何らかのintの値などをどんどん自動的に変えてもらいたいようなときは、setValue(), incValue(), decValue()メソッド等を備えた新たなラッパークラスを、自作して使わざるをえないのであります。たとえばこれ、試作品(このクラスのオブジェクトを使うアプリケーションクラスは、オブジェクトの値を自分の中にキャッシュしないほうが安全です…ソース冒頭のコメントを見てください)。
このことは、あまりに原始的な話題のせいか、市販のJavaプログラミングテキストの、どれにも載っていないようです。
(1)配列の宣言にはサイズを書けません
/*正しくはこう*/
int[] ary;
...
ary = new int[3];
-------------------------------------
/*これはエラー*/
int[3] ary;
...
ary = new int[3];
(2)JLabelはデフォルトでは背景が透明です
そこで、JLabelオブジェクトにsetBackground()するためには、その前にsetOpaque(true)をコールする必要があります。このページを読んでみましょう。
(3)先にaddしたリスナが先に実行されるとは限らない!
一つのGUIコンポーネントに複数のリスナ(たとえばActionListener(s))をaddしたとき、それらの実行順は不定です。ですから、サブクラスがaddするListenerが、スーパークラスがaddしているListnerが先に実行されることに依存していると、裏切られることがあります。順序性が重要な処理は、単一のListener内に書くのが無難です。JDKのdocのawt/designspec/events.htmlを見てください。
(4)ボタンのラベルテキストと, Actionの“名前”
Actionは、いろんなところに持ち運びしやすい便利なオブジェクトです。一方、ボタンを作るときは通常、ラベルテキストを指定します。たとえば:
JButton stopButton = new JButton("STOP"); //"STOP"というラベルテキスト
しかし、このボタンにActionを結びつけると、どうなるでしょうか?:
Action act = new MyAction();
stopButton.setAction(act);
これでは、表示されたボタンの表面にはSTOPもなにも、テキストがありません。のっぺらぼーのボタンが表示されます。
結論としては、Actionを使うときにはボタンの表面の文字列もActionの名前として指定しないとだめです。
stopButton.setAction(new MyAction2("STOP")); //Actionのname
このことは、同じアクションでも名前はいろいろあっていい、という意外な結論に結びつきます。そうすると、actionPerformed()の中では、名前で条件分岐できます。
(comp.lang.java.guiのみなさま、idiotな質問をしてゴメーナサイ!....This is ワタクシゴト!!)
(5)ローカル変数に隠されるクラスグローバル変数
初心者のうっかりミスで多いのが、これです:
public class MyClass{
JButton aButton;
public MyClass(){
JButton aButton = new JButton("Ahaha");
....
}
メソッドの中(上の場合はコンストラクタのメソッド)の文は、書いた当人の気持ちとしては、クラスグローバル変数のaButtonを初期化しているつもりなのですが、アタマにタイプ指定(JButton)があることによって、ひとつの新しい変数(メソッド内のローカル変数)の宣言になってしまっています。言い換えると、これでは、クラスグローバルのほうのaButtonはnullのままです。
このコードは、クラスグローバルのaButtonにアクセスしているところで、NullPointerExceptionを投げるでしょう。しかも、文の形そのものは正しいので、なかなか自分のミスに気づきにくいのです。「aButtonは、コンストラクタの中でちゃんと初期化しとるやんけ!、なにがNullPointerや?!アホ」、と初心者はJavaに対して怒ったりします?!?。
変数名のアタマにタイプ指定があると、それは、新たな変数宣言です。このことを。頭にたたきこんでおきましょう。
(6)オブジェクトのhash値を利用しているデータ構造のワナ
java.util.Mapインタフェイスのドキュメンテーションの冒頭部分に、初心者には少々分かりにくい文章で書いてあります。結論だけを言うと、Map系のデータ構造や、それを利用して実装されているデータ構造(HashSetなど)にオブジェクトをkeyとしてput()または単独でadd()したあとには、そのオブジェクトのhash値(hashCode()メソッドの返し値)が変わるような変更…たとえばデータメンバの値を変えるなど…をオブジェクトに対して行なってはなりません。なぜならば、そのオブジェクトをデータ構造内に収めたときには、その時点のhash値が使われている==その位置等は最初のhash値で決まっているからです。ですから、同じオブジェクトでも、そのhash値が変わるような変更をしてしまうと、まさにその新しいhash値の位置にはなにもまだありません(※)から、「そのオブジェクトはここにはないよ」という意外な返事をデータ構造からもらってしまいます。〔※:危険!!、なにもまだないのは、まだデータ数が少ないときのみ!〕
逆に言うと、メンバの値をあとから変えようとどうしようと、同じオブジェクトは同じオブジェクトとして認知してほしいのなら、hashCode()とequals()をオーバライドせずに、Objectクラスのままを使うべきです。Objectクラスの場合は、オブジェクトのメンバの値等ではなく、オブジェクトそのものの同一性に基づいて、これらのメソッドを実装しているからです。
髪の色や着ている服が変わっても、AさんはAさんと認めるのか?、それとも、茶髪に染める前のAさんと染めた後のAさんは別人と見なすのか?。前者の論理を採りたければ、Objectのメソッドをそのまま使いましょう。
コーディングスタイルから見て、Microsoft世界の人びとと思われる連中が、あるフォーラムで騒いでいました。(ご覧いただくコーディングスタイルは私がJavaふうに書き直してあります)まずFoo.javaをコンパイルし、次にTestFooExample.javaをコンパイルしてください。そして、TestFooExampleプログラムを実行してください。彼らは、メソッドfooFound()<-HashSet#contains()が前にadd()した同じオブジェクトに関してfalseを返す、変だ変だ、どうしたんだ、なにごとだ!、と騒いでいるのです。
こんな程度の人びとが集まってるんじゃ、Microsoftのソフトはいつまでたっても相変わらず粗悪なままだろうね。
(7)privateやpublicはコンパイラに対する指示だ!
なにか適当に、publicなデータメンバのあるクラスAを書いてコンパイルしてください。次に、そのクラスAのオブジェクトのpublicデータにアクセスするメソッドのある、別のクラスBを、実行クラス(main()メソッドのあるクラス)として書き、コンパイルし、実行してみてください。Bのメソッドには、クラスAのpublicデータをSystem.out.println()する文などがあるといいでしょう。
そして今度は、クラスAのデータのpublic修飾子をprivateに書き換えて、Aを再コンパイルしてください。それから、さっきのBを再び実行してみてください。privateに変えたにもかかわらず、Bは正常にAのデータにアクセスできちゃいます。
こらっ、そいつは今じゃprivateだぞ!、と叱ってもムダです。では、今度は、Bを再コンパイルしてみてください。…コンパイルできません!。
private, publicなどのアクセス制御子は、あくまでもコンパイル時に機能する指定です。実行時には関係ありません。Java Language Specificationの6.6や6.6.1を読んでください。…これも、あるフォーラムで騒がれていた話題です。
そもそも、プログラミング言語の仕様は、実行時エラーよりもコンパイル時エラーが充実しているほうが優秀なのです。できるだけ多くのエラーを、コンパイル時にチェックしてしまう、これがJava言語の偉大なる設計哲学と言えます。そういう基本理解のないMicrosoft派の連中が、Javaのchecked exceptions(コンパイル時に対応の有無をチェックされる例外)について、邪魔だ、要らない、と文句を言っています。Googleで探すと、そういうページがいっぱい見つかりますが、読むのは無駄です。
(8)requestFocus()は直後のsetVisible(true)によってリセットされます
ですから、この順はだめです:
textField3.requestFocus();
frame.setVisible(true);
この順で書いてください:
frame.setVisible(true);
textField3.requestFocus();
(050714追記)
J2SE 1.4より、requestFocus()は事実上、非推奨になりました(How to Use the Focus Subsystem, AWT Focus Model Specification)。そして、requestFocus()ではなくrequestFocusInWindow()メソッドを、フレームのsetVisible(true)の前に呼び出します。以下は、How to ...文書からの引用です:
frame.pack(); //コンポーネントを実現する
//このボタンが最初にフォーカスを取得する
button.requestFocusInWindow();
frame.setVisible(true); //ウィンドウを表示する
この項の本文は、こちらをご覧ください。
Javaアプリケーションを起動するとき、通常はコマンドラインで:
java MyApp
とタイプします。この'java'が余計です。
Linuxには、binfmtという便利な仕組みがありますし、また'java'を含むコマンド行をスクリプト化して、それをGUIのランチャ(プログラム起動アイコン)に結びつけるのも簡単です。
で、Windowsのばやいは、デスクトップの上にショートカットを作ります。その[リンク先]の欄に、javaのWindowsだけ用の変種であるjavawを起動するコマンドラインを書きます。アプリケーションのクラスがMyApp.classで、それがc:\
MyJavaAppディレクトリにあるならば
javaw -cp c:\MyJavaApp MyApp
そして、ショートカットアイコンの[作業フォルダ]の欄には、やはりc:\
MyJavaAppを書きます。これで暗黙のクラスパスの'.'(カレントディレクトリ)が生きます。
しかし、この簡単な例を見ても、Windowsとつきあっていては、本当のコンピュータの勉強&利用は、とうてい進みませんことが分かります。
こちらをお読みください。
J2SE 1.4から、物理的なグラフィクス環境を表すクラスがいくつか、java.awtパッケージに導入されました。とくに私がありがたいと思ったのは、ウィンドウサイズを各環境の相対値で指定できるようになったことです。
GraphicsEnvironment ge
= GraphicsEnvironment.getLocalGraphicsEnvironment();
Rectangle rec = ge.getMaximumWindowBounds();
int height = rec.height;
int width = rec.width;
そしてウィンドウサイズをheightやwidthの相対値で指定すれば、どの解像度の画面でも各GUIがきれいに画面内に収まるようになるでしょう。この簡単なテストプログラムを、動かして見てください。
UNIX/Linuxの上のbashシェルなどでは、ファイルを指定するコマンド行の中で、*.*は「ファイル名の途中にドット文字がある」を指定し、*は「あらゆるファイル名」を指定します。では、「途中にもどこにもドット文字がないファイル名」は、どうやって指定するのでしょうか?。
MS-DOS/Windowsでは、*.*は「あらゆるファイル」を指定し、*は「途中にドット文字がないファイル名」を指定します。では、「途中にドット文字があるファイル名だけ」は、どうやって指定するのでしょうか?。
途中の議論を省いて結論を申し上げると、Java Swingの JFIleChooserと自作のアクセサリコンポーネントを使うと(関連ドキュメント)、正規表現を利用するファイル管理プログラムを作れます。正規表現が使えれば、上の問題が解決するだけでなく、ファイル指定機能がよりパワフルになります。
java.util.Randomクラスの、無引数のコンストラクタは、乱数のタネとして、ミリ秒で表した現在時刻を使います。そこで、二つのコンストラクタ呼び出しが時間的に接近しすぎていると、タネの値が同じになり、同じ乱数系列が得られてしまうことがあります。
対策としては、コンストラクタの呼び出しは一回だけにして、同一のRandomオブジェクトをずっと使い回すとよいでしょう。
ごめんね、ライル(わたくしごと!)。
java.util.Listオブジェクトをモディファイ(部分改変)するための素材として、その同じListオブジェクトやその部分を使うと、おかしな結果になります。
対策は、そのListオブジェクトの現状のコピーを作って、それを素材として使うことです。
クラスのメソッドには、そのクラスで宣言されているメソッド(d)と、スーパークラスから継承しているメソッド(i)があります。結論としては、同じ仕事をするためには、できるかぎり、dのほうのメソッドを使うべきです。
その理由は、dのほうにはユーザのための配慮や便宜が盛り込まれているからです。たとえばBufferedOutputStreamを使ってバイナリデータを出力するときには、
write(byte[] b)
ではなく
write(byte[] b, int off, int len)
を使うと、同期化やデータ長の指定を忘れずにすみます。
staticメソッドJOptionPane.showInputDialog()は、簡単な入力GUIとして便利に使えますが、入力確定用の[OK]ボタン(日本語環境では[了解]ボタン)のほかに、やや意味不明の[Cancel]ボタン(日本語環境では[取り消し]ボタン)があります。このボタンをうっかり押すと、メソッドの返し値のStringがnullになってしまいます。
厄介なNullPointerExceptionなどを避けるためには、このnull対策が必要です。
String s = null;
while (s == null){
s = JOptionPane.showInputDialog(parent, "入力プロンプトメッセージ", "title",
messageType);
}
なお、入力確定には、[OK]ボタンのほかに<Enter>キーも使えます。
nullの帰値は、「帰値sがnullだったらsをデフォルトの(or元の)文字列にする」といった使い道もありますね。
JTabbedPaneの各タブつきページは、add(), addTab(), insertTab()などのメソッドにタブのタイトルと(アイコン)と、ページの中身となるGUIコンポーネントを渡すことによって実現します。
しかしこのとき、複数のページの中身、または中身の一部が、同じコンポーネントだと、各タブの期待した正しい表示は行われません。これがプログラマのうっかりミスだったら、デバッグに苦労するでしょう。
その悩ましい状況を、この例示プログラムで体験してみてください。
Sunは、「これはバグではない」と主張しています。「だって、われわれは、“同じコンポーネントを複数のタブが共用してよい”とは言ってないから」だって。なんだか、ものすごい、ものの言い方ですねっ!。
これはドキュメントにはっきり書かれていないので、つい犯してしまうエラーです。ObservableオブジェクトのnotifyObservers()メソッドを呼び出す前には、直前にどこかで、setChanged()メソッドが呼び出されていないとあきません。それを忘れていると、Observerに全然notifyが行きません。
なお、Observableオブジェクトのデータ等の変化が、当のオブジェクト以外の不特定の場所で行われてしまうときには、そのデータ等を監視するための別スレッドを、そのObservableオブジェクト内で動かさざるをえません。(変えられる場所が事前に特定できる場合には、その場所で、そのObservableオブジェクトoの、o.setChanged()とo.notifyObservers()を呼び出しておけばいいのですが…。)
22. インナークラスをほかのクラスから使う
public class Outer{
...
class Inner{
...
}
...
}
こんなインナークラスを、ほかのクラスから使うには:
public class Other{
Outer.Inner oi;
...
public Other{
oi = new Outer().new Inner();
...
}
...
...
}
このように、2つのnew文を書く書き方をします。こんな簡単なことでも、分かるまでがたいへん!
サイコロ3つだから、これでいい?
import java.util.*;
...
Random threeDice = new Random();
int n = threeDice.nextInt(18) + 1;
(負け惜しみ)コンピュータの上には、こんなサイコロがあってもいいんじゃない?
Javaは、浮動小数点数として正しく表現できない数に関して、C言語のような実用的な最適化をやってくれません。そこでたとえば次のような文は、正しい結果を作り出しません:
System.out.println(12.9 % 1.0);
System.out.println(1.0 + Math.IEEEremainder(12.9, 1.0));
System.out.println(1.0 + StrictMath.IEEEremainder(12.9, 1.0));
ほぼ同じことをCでやってみるために、こんなプログラムを書きました:
#include <stdio.h>
#include <math.h>
/* ただし、書式指定のfの前に.16などと書いて“精度”を上げると、Javaと似た結果になります. */
int main(void){
printf("%f\n", fmod(12.9, 1.0));
printf("%f\n", 1.0 + drem(12.9, 1.0));
return 0;
}
ところが、Linuxの上のgcc(ccコマンドを介して使用)は、リンカldが、fmodやdremがない、と文句を言います。結局:
cc -o Rem Rem.c
ではだめで、-lmというオプションで数学ライブラリの使用を明確に指定してやる必要があるのです:
cc -lm -o Rem Rem.c
このことが分かるまでに、ほぼまる一日、七転八倒しました。
だって、#include <math.h>と書いてあるんだから、数学ライブラリを使うに決まってんじゃん!。いや、書いてなくたってさぁ、fmod()やdrem()を現に呼び出してんだからさぁ…。するとgcc/ldは、「#includeはリンカのための指示ではない。fmod()だけではどこのfmod()か分からん」と官僚的なことを言うのだろうね。C言語と縁のない世代は、このお話が分からなくてもかまいません。
なお、今のところ、Javaで正しい実数演算をしようと思ったら、"12.9"のようなStringをコンストラクタに与えてBigDecimalオブジェクトを作る、という奇妙な方法しかありません。doubleを与えたんじゃー、だめですよー。
HashSetはSet(集合)ですから、同じオブジェクト(そのオブジェクトのクラスのequals()メソッドがtrueを返すオブジェクト同士)を入れない約束になっています。しかしHashSetのドキュメントはequals()だけを強調していて、HasSet#add() -> HasMap#put()の中でわざわざチェックしているhashCode()の、ハの字にも言及していません。そこで、hashCode()のことを忘れてpublic boolean equals(Object 0)だけをを正しいシグネチャ(Objectクラスのとおりの形で)で正しく実装しても、add()の動作などが期待と違う結果になることがあります。
Object#hashCode()メソッドのドキュメントを、読んでください。なにしろ、equals()メソッドを書かなければならないようなクラスは、hashCode()も、equals()なオブジェクト同士は同値のhash値を返すように実装する…これを常識とすべきようです。
このことは、かなり昔に雑誌に書いたことがありますが、久しぶりに同じ問題に出会うと、すっかり忘れていました。元はと言えば、HashSetのドキュメントがequals()とhashCode()を併記して強調していれば、落ちずにすむワナなんですけどね。
(Javaの初期のデータ構造クラスであるHashtableのドキュメントは、ちゃんと両者を併記して強調しています。)
26.KeyEvent#keyReleased()メソッドの怪
Linuxの上ではこんなゲーム制御が正しく動きません:
/*KeyListenerの中*/
public void keyPressed(KeyEvent e){
if (e.getKeyCode() == KeyEvent.VK_RIGHT){ // [→]キー
animationStart(); //キャラクターを動かす
}
}
public void keyReleased(KeyEvent e){
animationStop(); //キャラクターの動きを停止する
}
上のコードでは、右矢印キーをずっと押し続けている間、キャラクターが動き、キーから指を放すと動きが止まる、という方針です。ところがパソコンのキーボードのキーを押し続けると、aaaaaaaaaaaaaaaaaのような連続文字入力が可能です。ということは、キーを押し続けている間、key-pressedとkey-releasedの両イベントが高速で交代々々に連続生成されているのです。Linuxの上のJava(1.4.2)では、このことを忠実に反映するので、上の構想はパーです。Windowsの上のJavaは、構想どおりの動作をします。
SunのBugParadeには、Linux/Solarisユーザからの不平が寄せられています:[例1], [例2]。
27.Stringを==演算子で正しく比較できる??
Javaの二つのStringが同じ文字内容であることは、equals()メソッドを使って調べます。Objectのequals()メソッドは二つのオブジェクトの同一性を調べます(美空ひばりと加藤和枝は同一人物だ!)が、Stringのequals()メソッドは二つの異なるStringオブジェクトの同値性を調べます。そこで、二つのStringの“同一性”を調べるニーズがもしもあったら、==演算子を使わなければなりません。
ところが、==演算子を使って二つのStringオブジェクトの同値性(?)を正しく調べられるケースがあります。それは、プログラムの最初のほうにたまたま、同じ文字列が登場している場合です:
JButton button1 = new JButton("BUY");
...
...
/* ActionListenerの中 */
...
if (button.getText() == "BUY"){ //equals()を使わなきゃダメ!
...
Javaは、プログラム中の複数の同じ文字列は一つにしてしまう、そしてそのアドレス(参照)を使い回す、というメモリ節約行為を舞台裏でやります。そこで、上では、JButtonが持っている"BUY"とif節に書かれている"BUY"はメモリの上で物理的に同一物です。ですから、equals()を使わずに==を使っている上のコードは、たまたま正しく動作するのです。
ただしもちろん、こんな偶然に依存するプログラムを本番で書いてはいけません。文字列(JavaのStringオブジェクト)を比較するときは、必ずequals()メソッドを使いましょう。
28.プロット(連続描画)のjava.awt.Panelとjavax.swing.JPanelの違い
ソフトバンクから4年前に出版された「速習Java Swingプログラミング」をお手元にお持ちのかたはすぐに確認できますが、15章の簡単なお絵かきプログラム、リスト15.3のTJColorChooserクラスは、同書がSwingのプログラミング教本でありながら、描画用のコンポーネントとしてJPanelではなくjava.awt.Panelを使っています。なぜでしょう?。その答えは、JPanelを使ってお絵かきやプロッターアプリケーションのような連続描画をやろうとすると、Panelの場合のような簡単なコードではできない(=入門書向きではない)からです。
java.awt.Panel(をextendsした自作クラス)を使うなら、update()メソッドをオーバライドして、背景の塗り直し(=前に描いたものの消去)を防いでやるだけでOKです:
public class PlotPanel extends Panel{
int oldX, oldY, newX, newY;
public PlotPanel(){ //コンストラクタ
...
setBackground(Color.white);
...
}
public void update(Graphics g){ //背景色の再描画を抑止する
g.setColor(Color.blue); //インクの色は青
paint(g);
}
public void paint(Graphis g){ //repaint()が呼び出されるたびに
g.drawLine(oldX, oldY, newX, newY); //連続的に線を描いていく
}
.....
}
ところがSwingのコンポーネントは、update()メソッドなどの仕様がawtと違うため、上のような簡単なコードでは連続描画ができません。
まる二日間悪戦苦闘した結果、たどり着いた結論がこれです:
public void paintComponent(Graphics g){
g.drawImage(bufImg, 0, 0, this);
}
ごらんのように、paintComponent()メソッド(awtコンポーネントではpaint()に相当)はGraphics#drawImage()を呼び出しているだけです。そして、repaint()を呼び出すアプリケーションメソッドの中で、ImageオブジェクトbufImgの中へdrawLine()していきます。それはたとえば、こんなコードです:
public void doPlot(){
Graphics g = null;
while (! stop){
if (begin){ //初回
bufImg = createImage(getSize().width, getSize().height);
g = bufImg.getGraphics();
g.setColor(Color.white); //白いキャンバスを作る
g.fillRect(0, 0, getSize().width, getSize().height);
begin = false;
}
else{ //初回以降
prevX = newX; prevY = newY;
newX = getNewx(); //次の座標値を得る
newY = getNewy();
g.setColor(Color.blue);
//Imageオブジェクトの中へ描画していく
g.drawLine(prevX, prevY, newX, newY);
}
repaint();
.....
}
.....
}
この方法が有効なのは、Imageオブジェクトへの複数回の描画の仕様が、「新規更新描画(renewal)」ではなく、ありがたいことに、「描き足し(描き加え)描画(append)」であるためです。なお、ImageではなくBufferedImageを使うと、javax.imageioを使って描画結果をファイルに保存することもできます。
連続描画ではなく、アニメーション(=前の絵を消してから今回の絵を描かなければならない)は、JPanelの上でも苦労なく、標準のpaintComponent()の実装方法で可能です。
[関連クイズ]このプログラムは、現状ではJPanelをScrollPaneの中でスクロールさせると画面がゴミになります。原因とオーソドックスな正しい対策(最も単純すなおなバグフィックス)を考えてください。
29.jarファイルの中のリソースファイル
jarファイルの中には通常、プログラムが使用するリソースファイル(テキストファイルや画像ファイル、狭義のリソースファイルなど)も収めます。しかし、jarファイル化するJavaプログラムの中で、単純にそれらのファイルを読み込んでいると、
java -jar MyApp.jar
でプログラムを起動したとき、javaはファイルがないとか見つからないとか文句を言います。ちゃんとあるだろっ、jarファイルの中に!。
つまり、こんなふつうのファイルアクセスではだめなんです:
/*だめ*/
Image img = Toolkit.getDefaultToolkit().getImage("myicon.gif");
BufferedReader br = new BufferedReader(new FileReader("help.txt"));
典型的な正しいやり方は、こうです:
/* OK */
URL url = MyApp.class.getResource("/myicon.gif");
Image img = Toolkit.getDefaultToolkit().getImage(url);
InputStream is = this.getClass().getResourceAsStream("/help.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// MyApp.classもthis.getClass()も意味はおんなじです
つまりですな、ふつうのファイルアクセスの文が書かれているとjavaは、jarファイルの中のファイルではなくてカレントディレクトリ内にそのファイル(やディレクトリ)を探すんですな。だから、(たまたまカレントディレクトリ内にhelp.txtなどがあった場合を除き)ファイルがないと文句を言うんです。
では、上のコードの、先頭の'/'はいったい何なのか?。これらの約束事(プロトコル)については、JDKのAPIドキュメンテーションを読んでください。
[参考ページ抜粋]
なお、ClassクラスのgetResource...()メソッドはClassLoaderクラスのメソッドに委譲されますが、私たちは後者のメソッドを直接呼び出さないほうが無難です。同じ引数に対して動作が異なることがあり、しかもそのことが詳しくドキュメントされていません。もっぱらClassクラスのメソッドだけ使うのが、安全です(こちらは引数に関する説明が具体的で詳しい)。(061020追記)
OSのファイルシステムとJavaのファイル認識のギャップ。それは、パッケージのクラスパス問題でも悩まされますね。
30.超簡単・手抜きレイアウトマネージャ
Java APIが提供している各種のレイアウトマネージャは、簡単なものは小回りが利かず、細かい指定ができるものは覚えるべき要素が多すぎて複雑難解、という欠点(?)があります。
そこで、各コンポーネントの位置やサイズを自由に指定できて、しかも使い方は超簡単、という“理想の”レイアウトマネージャを作ってみました。マネージャ本体のクラスがGridAlocLayout、それ用の制約(constraints)オブジェクトのためのクラスがGridAlocConstraintsです。
こういうものは、ゼロから自作するか、既存のクラスを利用するかのどちらかですが、今回は作り方も楽なほう(後者)を選びました。それは、GridBagLayoutのpublicな配列メンバを利用して、コンテナ内のグリッド分割を強制的に決めてしまうという方法です。そしてGridLayoutなどと違って、各コンポーネントを配置するグリッド位置と、それらが占めるタテヨコのグリッド数もプログラマが指定できます。
つまり、コンテナのグリッド分割数を多くすればするほど、よりきめ細かいレイアウト指定が可能になります。
--------
もちろん、この簡単なGridAlocLayoutも、今後いくらでも、凝った要素を加えていくことが可能です。そして、凝りすぎると、今のGridBagLayoutと変わらないぐらい、複雑難解なものになってしまうでしょう!!。
31.シンプルなデータファイルを使う動的Webページ
TomcatやResin Web Serverなどのwebappsディレクトリに、あなたのWebアプリケーションの、WAR化しないファイル群を収めたサブディレクトリがあるとしましょう。そしてそのルート(いわゆるコンテキストルート)に、java.util.Properties形式などの簡単なデータファイルがあるとします。その名前をここでは仮に、users.propertiesとします。
このデータファイルをJSPページやサーブレットから読み出し/更新するためには、こんなコードを書きます:
// ... リクエストデータなどの取り出し
.....
.....
String fileNameFromContextRoot = "/users.properties";
//JSPの場合:
String path = application.getRealPath(path); //物理的なパスを取得
//サーブレットの場合:
getServletContext().getRealPath(path); //物理的なパスを取得
..... // このpathを使ってFileInputStreamなど入力ストリームやReaderをnewする
/* いろんな読み出し処理 */
.....
..... // 必ずclose()すること
.....
..... // pathを使ってFileOutputStreamなど出力ストリームやWriterをnewする
/* いろんな更新/書き込み処理 */
.....
..... // 必ずclose()すること
.....
この記事のポイントは:入力ストリームは、コンテキスト相対名"/users.properties"と、ClassのgetResourceAsStream()を使っても得られますが、これと対照性のある、出力ストリームを得るメソッドがなく、getRealPath()を使って作る出力ストリーム(等)と、ClassのgetResourceAsStream()を使って得られる入力ストリームは異様に相性が悪いので、上のような入力/出力の対照性を使わなければならない、という点です。
以上は、まる一日の悪戦苦闘から得られた、ささやかな技術情報であります。本格的なデータベースの運用管理で苦労せずに、単純にWebアプリケーションの作成と運用を楽しみましょう。
====end of contents====