AWTとSwingのペイント(paint)の仕組み Java API javadoc改善運動! Javaの手帖 Javaの手帖--GUI分冊 /ひとつ上をゆくJavaの教科書 「JavaServer Faces完全ガイド」補遺
javacのエラーメッセージ解説 「Javaの哲学」の恥かき 「Javaによるテキスト処理」の恥かき 「モア・サーブレット」の恥かき 恥かきのホームページ フォーラム「JavaBeginner」
UIDefaultsのkeyとデフォルト値「コア・サーブレット」の恥かき comp.lang.java.guiのFAQ日本語訳 「入門 正規表現」 「Javaネットワークプログラミングの真髄」 「Java並行処理プログラミング」

Javaの手帖



  私のこのWebサイトの現状では、Javaプログラミング関連の
記事が複数のページに分散してしまって、読者にとって見づら
く、作る側にとってメンテナンスしづらいものになっています。
そこで本年2006年からは、一般的なJavaプログラミング記
事をこの「Javaの手帖」ページに一本化したいと思います。

  過去の各ページの記事ではなく最新の追加記事を見たいかた
は、今後はもっぱら、このページだけをちらっと見てくだされ
ばOKです。

  それでは、みなさま、これからもよろしく!。

  なお、GUIは、それだけで細かいプログラミングの話題が豊富
ですから、別のページ:[Javaの手帖-GUI分冊]を設けました。


0002(060219)

java.io.FileオブジェクトのequalsとgetParent

【概要】Fileクラスのequals()メソッドやgetParent(), getParentFile()
メソッドなどは、そのFileオブジェクトが表しているファイルではなくて
Fileオブジェクトを作ったときのパス名文字列を参照します。ファイルそ
のものを見てくれると期待していると、思わぬバグを導入してしまいます。
解決策はgetCanonicalFile()を使うことです。

  ファイル(やディレクトリ)のパス名の書き方は、いろいろありますね。たとえば自分が今、C:\java\javalessonディレクトリにいて、このディレクトリ(つまりカレントディレクトリ)を書き表したいときには、こんな書き方がありえます:
"."
"C:\java\javalesson"
"../javalesson"
などなど。
  そして言うまでもなく、どんな書き方をしても、それらのパス名が指しているファイルやディレクトリは、ひとつの同じファイルやディレクトリを表しています。
  しかし、java.io.Fileクラスの落とし穴は、このクラスのequals()メソッドが、ファイル(やディレクトリ)そのものを見てくれない、という点です。まったく同じファイル(やディレクトリ)を指す二つのFileオブジェクトがあっても、それらを作ったときのパス名文字列が違っていたら、equals()による比較の結果はtrueになりません。
  また、FileクラスのgetParent()メソッドやgetParentFile()メソッドも、ファイル(やディレクトリ)そのものを見てくれません。パス名文字列に親ディレクトリの記載--部分文字列--があれば、その部分文字列をもとに親を表すFileオブジェクトを作るだけです。ファイル名やディレクトリ名だけからFileオブジェクトを作ると、これらのメソッドはそれらのファイルやディレクトリの親ディレクトリを見つけてくれません。超不親切!!。
  このことを、下に示すFileCompareプログラムを動かして確認してください。このプログラムには、問題の解決方法--Fileを正規化すること--も書かれていますから、その結果も確認してください。

/**** FileCompare.java ************************/ 
/* 読者は、ご自分の実行環境に合わせて、プログラム中のパス名を適宜書き換えてから
 * コンパイルしてください.
 */
import java.io.*;

public class FileCompare{

  public static void main(String[] args) throws Exception{
    File f1, f2, f3, fx, fy;
   
    f1 = new File("html"); // カレントディレクトリの直下の'html'ディレクトリ
    f2 = new File("/root/html"); // その絶対パス名
    f3 = new File("./html"); // カレントディレクトリをドットで表す

    System.out.println(" vs.    : " + f1.equals(f2));
    System.out.println(" vs. <./html> : " + f2.equals(f3));
    System.out.println("<./html> vs.        : " + f3.equals(f1));
    System.out.println("...これらの親ディレクトリ...");
    System.out.println(f1.getParentFile());
    System.out.println(fx = f2.getParentFile());
    System.out.println(fy = f3.getParentFile());
    System.out.println("二つの親は等しいか? : " + fx.equals(fy));

    System.out.println("\n--正規化後の結果--");

    f1 = f1.getCanonicalFile();
    f2 = f2.getCanonicalFile();
    f3 = f3.getCanonicalFile();
    
    System.out.println(" vs.    : " + f1.equals(f2));
    System.out.println(" vs. <./html> : " + f2.equals(f3));
    System.out.println("<./html> vs.        : " + f3.equals(f1));
    System.out.println("...これらの親ディレクトリ...");
    System.out.println(f1.getParentFile());
    System.out.println(fx = f2.getParentFile());
    System.out.println(fy = f3.getParentFile());
    System.out.println("二つの親は等しいか? : " + fx.equals(fy));
  }
}
/*******************************************/
  Fileクラスのequals()メソッドやgetParent()メソッドなどは、なぜ、ファイル本体ではなくパス名文字列を見て判断をするのでしょうか?。その理由はたぶん、同じパス名文字列が指すファイル(やディレクトリ)本体が、プログラムの実行環境(たとえばカレントディレクトリの違い)によって変わってしまうから、と思われます。言い換えると、単純に特定の環境でのファイル本体の同じ/同じでないに依存したプログラムを書いてしまうことへの、予防策でしょう。


0001(060127)

正規表現:捕捉グループと欲張りマッチの罠

【概要】欲張りなマッチ(greedy match,最長マッチ)による全文字マッチ式
.*や.+のうしろに、オプションマッチX?(Xがゼロ個以上)や、任意数マッチ
X+(Xが一つ以上)があり、それらが捕捉グループに指定されていると、欲張
りマッチは、それらの捕捉グループのためのマッチの分け前として必要最小
限のものしか与えないので、プログラマの期待と異なる結果になることがあ
ります。解決策は全文字マッチ式として控えめなマッチ(reluctant match)
.*?や.+?を使うことです。

  正規表現の全体マッチの中の部分マッチを取得したいときは、かっこ(.....)を使用する捕捉グループ(capturing group)という仕組みを使います。捕捉グループとのマッチ部分は、Matcherオブジェクトのgroup(1)、group(2)などのメソッドで入手します。
  しかしここに、正規表現マッチの“欲張りなマッチ(greedy match)”と“控えめなマッチ(reluctant match)”の違いに起因する意外な落とし穴があります。…というか、日常的一般的には欲張りなマッチだけを使ってしまうという、私たちの正規表現の使い方のワンパターン化に、この問題の原因はありそうです。

  ここでは、メールアドレスのタイプ、すなわちドット以降の部分(.com, .org, co.jpなど)を取り出すプログラムを考えてみましょう。
  下のプログラムMaddType.javaの現状では、メソッドaddrType()の中で、メールアドレスを表す(と思われる)文字列をチェックするための正規表現文字列として、次のものを使っています:
"^.+@.+(\\..+)$"
  これは、最初に行頭を表す^、最後に行末を表す$があるので、文字列まるまる全体とのマッチを調べる正規表現文字列です。最初の.+@は、「なにかの文字が一つ以上あってそのあとに@がある」を意味しています。つまりこれは、メールアドレスの前半の@までの部分を指定していますね。
  その次の.+は、「なにかの文字が一つ以上ある」を指定しています。それは、@の直後から以降の部分ですね。しかしここではさらに、最後の.comなどを捕捉グループとして取り出したいので、\\..+をかっこ内に入れています。最初の\\.はドット文字そのものを指定します。そしてその次の.+は「なにかの文字が一つ以上」を意味します。.comなどは、まさにこの指定とマッチするはずです。
  そしてこのメソッドをテストするmain()メソッドの中では、ご覧のように4種類の文字列をテストしています。s1はドット以降部分のないメールアドレス、s2はメールアドレスではない文字列、s3とs4はそれぞれドット以降部分のあるメールアドレスです。
    String s1 = "marian@furabaloo";
    String s2 = "georgewashington";
    String s3 = "dinky@woops.com";
    String s4 = "ester@dreams.co.jp";
  この4つの文字列をテストして、次のような結果を得たいのですが…

ドット部分のないメアドです
不正なメールアドレスです
.com
.co.jp

しかし結果はこうなります:

不正なメールアドレスです
不正なメールアドレスです
.com
.jp

  最初の正しいメールアドレスが「不正」と判断されてしまいました。また、.co.jpではなく、.jpしか取り出せません。
  正しいメールアドレスが不正と判断されたのは、現状の正規表現文字列ではかっこ内の部分…ドット以降を求める部分…が、「あってもなくてもいいよ」という指定(オプション指定)になってなくて、必須になってるからですね。
  オプション指定のためには、「ゼロまたは一つ」を意味する?か、または、「ゼロまたはいくつでも」を意味する*を使います。ここでは複数のマッチを求めませんから?を使ってみましょう。そこで、こんな正規表現文字列を使ってみます。ご覧のように、捕捉グループの部分が末尾の?によってオプション指定になってます:
"^.+@.+(\\..+)?$"
ところが、結果はこうなります:

ドット部分のないメアドです
不正なメールアドレスです
ドット部分のないメアドです
ドット部分のないメアドです

  ご覧のようにこの結果は、正しいのはs1に対するチェックだけですね。この点にかぎり、最初の問題はクリアされました。s2に対するチェックは、前回も今回もOKです。新たな問題は、s3とs4に対する結果です。どちらも「ドット部分がない」という間違った判断をされています。なんでやー?。なんじゃこりゃー?。

  この奇怪な現象と疑問に対する答えは、以下のとおりです:

  正規表現でいちばんふつうに使う“欲張りなマッチ”では、.+や.*などのパターンがまず最大限のマッチ(最長マッチ)を求めます。ですから@以降にある.+は、文字列の最後までマッチしてしまいます。そして欲張りなマッチは次に、バックトラック(backtrack,後戻り)という行為を行って、.+以降に書かれている正規表現とのマッチを求めます。
  そして、ここでいちばん重要なのは、バックトラックによって求められる、ほかの正規表現とのマッチ部分は、必要最少限しか拾われないという、まさに欲張りなルールです。欲張りなマッチは(\\..+)?の部分に対してこう言います「俺が全部呑み込んでしまった中から、ちょっとだけ吐き出して、お前のぶんを分けてやってもいいが、ただし必要最少限だけしかやんないよ」。

  プログラムの現状の正規表現、つまりオプション指定のない捕捉グループ指定で、s4に対して.co.jpではなく.jpしかくれなかったのは、それがまさに(\\..+)に対する“必要最少限のマッチ”だからです。
  それでは、オプション指定はどうでしょうか?。オプション指定をした正規表現に対する、“必要最少限のマッチ”とは、何でしょうか?。オプション指定とは、「ゼロまたは一つ」または「ゼロまたはいくつか」でしたね。このマッチ条件に対する必要最少限とは?。…そうです!、ゼロ、すなわちノーマッチであります!。オプション指定とは「マッチするものが、あってもいいが、なくてもいい」というマッチ指定ですから、必要最少限主義の欲張りなマッチは、オプション指定に対して「お前には何も分けてやんなくてもよいのだ」、と正しく判断するのです。
  今回の課題では、ドット部分のないメールアドレスを正しくメールアドレスと判定させるために、ドット部分を表す捕捉グループをオプションにする必要がありました。しかしそうすると、欲張りなマッチではそのグループがもらう分け前として「ゼロマッチ=ノーマッチ」が選ばれてしまい、その結果、ドット部分のあるメールアドレスが「ドット部分がない」と判断されてしまいました。
  もうすでにお分かりと思いますが、この問題の解決は、以降の文字列全部を呑み込むような正規表現、.*や.+に対して、欲張りなマッチではなく控えめなマッチを使うことです。控えめなマッチは、部分的な正規表現に対して、バックトラック(後戻り)ではなく、前から順に調べるという動作をしますから、\\.に対しては最初に出会ったドットが拾われ、そこで、.jpではなく.co.jpが期待どおりに取り出されます。
  控えめなマッチを使う正規表現文字列は、こんな文字列です:
"^.+@.+?(\\..+)?$"
そしてその結果は、みごとに、こうなります:

ドット部分のないメアドです
不正なメールアドレスです
.com
.co.jp

  なお、この、メールアドレスの構成要素を取り出す課題は、控えめなマッチなどの話題に立ち入らない、“ふつうの”(=欲張りマッチによる)正規表現による解法もありますが、今回はあえてそっちに対しては目をつむっています。

/**** MaddType.java ************************/ 
import java.util.regex.*;

public class MaddType{

  public static String addrType(String maddr){
    String returnValue;
    String regex = "^.+@.+(\\..+)$"; // 最後の'.com'などを捕捉したい

    Pattern pat = Pattern.compile(regex);
    Matcher mat = pat.matcher(maddr);

    if (! mat.matches()){
      returnValue = "不正なメールアドレスです";
    }
    else{
      returnValue = mat.group(1); // (.....)のマッチ部分を得る
      if (returnValue == null){
        returnValue = "ドット部分のないメアドです";
      }
    }
    return returnValue;
  }

  public static void main(String[] args){
    String s1 = "marian@furabaloo";
    String s2 = "georgewashington";
    String s3 = "dinky@woops.com";
    String s4 = "ester@dreams.co.jp";

    System.out.println(addrType(s1));
    System.out.println(addrType(s2));
    System.out.println(addrType(s3));
    System.out.println(addrType(s4));
  }
}
/*******************************************/

AWTとSwingのペイント(paint)の仕組み Java API javadoc改善運動! Javaの手帖 Javaの手帖--GUI分冊 /ひとつ上をゆくJavaの教科書 「JavaServer Faces完全ガイド」補遺
javacのエラーメッセージ解説 「Javaの哲学」の恥かき 「Javaによるテキスト処理」の恥かき 「モア・サーブレット」の恥かき 恥かきのホームページ フォーラム「JavaBeginner」
UIDefaultsのkeyとデフォルト値「コア・サーブレット」の恥かき comp.lang.java.guiのFAQ日本語訳 「入門 正規表現」 「Javaネットワークプログラミングの真髄」 「Java並行処理プログラミング」