Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第149回「文字列の処理は難しい」

← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

「コンピュータって何をする機械ですか?」 という質問に対して的確に答えることは難しい。

計算する機械? ある意味正解である。 確かに昔はコンピュータのことを電子計算機と呼んだ時代もあった。 しかし、皆さんがもしパソコンをお持ちなら、 それを使って「計算」したことがあったかどうか、 考えてみてはどうだろう。 アクセサリの「電卓」のような機能を使って計算したことは多分あるかもしれない。 しかし、 デスクトップに表示されている電卓を滅多に使わない人は多いと思う。 私もそうだ。 電卓を使うような計算をしたい場合は、 本物の電卓を使った方が手っ取り早いからだ。

しかも、Windows の場合は、電卓をマウスで操作して素早く結果を得るには大変な熟練を必要とする。 いや、もしかして、少し慌てると、 ボタンをクリックし損ねて計算を間違えることが非常に多くなるというのは、 私だけなのだろうか?

少しパソコンに慣れた人だと、 表計算を使って計算するとか、 家計簿のようなソフトを使っていたり、 その種の計算をしたことのある人もいるだろう。 私の場合は、確定申告の計算に perl を使っている。 そこまでくると、コンピュータで計算しているという実感が沸いてくる。 ただ単に、入出金の情報をテキストファイルにしておいて、 それを perl で読み込んで足したり引いたりするだけの処理なのだが。

§

パソコンが実際、何に使われているかというと、 私の周囲ではメールのやりとりに使うケースが圧倒的に多い。 次いで、Web から情報を get するのに使うような場合。 となると、 コンピュータは情報を処理する機械だ、という答え方もカッコいい。

では情報とは何ですか。 これまた難問だが、 テキストとか画像とか音楽とか、そういったものだと思えばいい。 今時だと XML で書かれたモノ、なんてのもいいかもしれない。 特に、文字という所に注目すれば、 コンピュータは文字データを処理する機械、 というような説明ができる。 実際、プログラミングの入門段階で、 文字を表示したり、入力したり、といった処理の書き方を必ず習う。 現実に、そのような処理が重要であることの証拠である。

§

Java の世界では、 基本的に String クラスを使って文字列を扱う。 だから、マニュアルなんかいちいち見なくても String クラスは使える、 という人が多いかもしれない。 余談だが、 String クラスは JDK のバージョンアップにつれてかなりグレードアップしている。 例えば、JDK 1.4 からは、 match というメソッドが追加された。 これを使えば、 正規表現を指定してマッチするかどうかを簡単に調べることができるのだが、 このメソッドがあることを知らないと、 ややこしい手間をかけることに無駄な時間を費やしてしまう。 かといって、後から追加されたメソッドを知るには、それなりの時間がかかる。 世の中厳しいものだ。

Java の開発では、 リファレンスマニュアルなどのドキュメントがインターネットで割と簡単にダウンロードできるし、 しかも、それがブラウザで表示できるので、かなり便利である。 しかし、日本語に翻訳されるには少し時間がかかる。 最新のものは英語で読むしかない。

  

もっとも、日本語になっていればいいというものでもない。 分からなくなったら原文のドキュメントにあたれ、 という格言がある位である。 もっとも、 Java の場合は本家の Sun が翻訳したドキュメントが公開されているのだから、 誤訳はあまりない、と考えても殆ど問題ない。 全くない訳ではない所がミソではあるのだが。

 

※ ある程度の規模になったら、ミスがあるのは仕方ない。 問題は、誤訳がプログラミングに影響するほど重大なのか、 それとも、単に表現の問題で済むのか、ということだ。

§

文字列の中に部分文字列が含まれるかどうかを調べるにはどうすればいいか。 C言語だと、strstr という関数があるが、 Java なら、 String.indexOf というメソッドを使えばいい。 使い方は簡単なのだが、 Java2 SE 1.4.0 のリファレンスマニュアルには、 次のように説明されている。

public int indexOf(String str, int fromIndex)

指定されたインデックス以降で、指定された部分文字列がこの文字列内で最初に出現する位置のインデックスを返します。返される整数は、次の最小値 k になります。

k >= Math.min(fromIndex, str.length()) && this.startsWith(str, k)

このような k の値が存在しない場合、-1 が返されます。

引用は、Java2 SE 1.4.0, String.indexOf の説明より

最初の文を読む。 インデックスというのは、文字列の何個目の文字かということだ。 例えば、"abc" という文字列があるとする。 最初の文字の 'a' に対応するインデックスは 0 である。 C言語でもそうなのだが、 順番を1ではなく0から始めるというのは、 プログラムを書くときに、 その方が都合のいいことがしばしばあるためである。 同様に、文字 'b' に対するインデックスは 1、文字 'c' に対するインデックスは 2、 ということになる。

途中をスキップして最後の文を読む。 何がkなのか分からないにしても、「存在しない場合、-1が返されます」というのだ。 これを直感で考えると、 指定した部分文字列が見つからない場合に -1 が返されるのだろう、 と想像できるはずだ。 実際そうなのである。 インデックスが文字列の中の位置を表現するならば、 それは0以上の整数値となるはずだから、 インデックスの世界においては、-1 という値は、 「文字列の中には無い」という意味に使うことができるのだ。

実はこれで indexOf というメソッドの振る舞いは殆ど理解できているのであるが、 非常に細かいことが、一つだけ抜けている。 簡単だという人は、最後の壁も乗り越えて欲しい。 そのためには、いま無視した真ん中にある文を解釈する必要がある。 このメソッドの戻り値は、次の条件を満たすような最小の k だというのだ。

k >= Math.min(fromIndex, str.length()) && this.startsWith(str, k)

あなたはこの意味を理解できるか? というか、分かる人はいるのか?

まずこの条件を List 1 のように、前半と後半に分けて考える。

---- List 1 ----

    k >= Math.min(fromIndex, str.length()) // 条件1
    &&
    this.startsWith(str, k) // 条件2

条件2は簡単である。 この文字列の k文字目から指定された部分文字列 str が現れる、 という意味である。

問題は条件1だ。 fromIndex と、str.length() の小さい方の値よりも k が大きい、 ということは、 fromIndex <= str.length() なら、k がfromIndex よりも大きい、 fromIndex > str.length() なら k がstr.length() よりも大きい、 という条件を満たせというのである。

単純に考えてみる。 indexOf を呼び出して、部分文字列が見つかった場合、 戻り値は fromIndex 以上であるという条件があるはずだから、 0以上の値が戻るのであれば、 k >= fromIndex でなければおかしい。

例えば、 "my beautiful dream".indexOf("ea", 10) がどうなるかを考えてみる。 つまり、検査対象の String が "my beautiful dream"、 strは"ea"、 fromIndex は 10としたのである。 この場合、str.length() は 2だから、 条件1は、 k >= Math.min(10, 2) となる。 これは、 k >= 2 ということである。

この文字列の中には "ea" が2箇所に現れる。 条件2を満たすようなkは、 4と15の2つある。 従って、結局、条件1と条件2を同時に満たす最小のkは、4だ。 つまり、リファレンスマニュアルの通りの処理が行われたなら、 "my beautiful dream".indexOf("ea", 10) の返す値は4になるはずだ。

理解できただろうか? ちなみに、 原文である英語のリファレンスマニュアルの記述も同様であるから、 稀にあるような、 「日本語訳の時に奇天烈な解釈をした」という類の話ではない。 しかし、 "my beautiful dream".indexOf("ea", 10) の返り値は15になって欲しいだろう。 実際にJava でプログラムを書けば一目瞭然、 当たり前のようにこのコードは15を返す。 どこで間違ったのだろうか?

解説の最初の文の通りなら、

    k >= fromIndex && this.startsWith(str, k)

でもよいのではないか? なぜ str.length() がここで出てくるのだろうか? ちなみに、lastIndexOf も難解である。 こちらのメソッドは次のような条件を満たす最大のkを戻すという。

     k <= Math.min(fromIndex, str.length()) && this.startsWith(str, k)

またもや str.length() が出てくる。

§

このあたりでどうもギブアップということで、 詳しい人に質問してみることにした。 Webで探せば、Java に関するQ&Aの掲示板がいくらでもある。 プログラマーズフォーラムで質問するという手もある。 今回は、一番確実だろうと思われる、 Sun の Java Technology Forum で質問してみたら、 結局、こういう回答になったのである。

  

「str.lentgth() は誤りで、 this.length() が正しい」

 

※ もちろん、回答の原文は英語である。

なんとー。str.length() と this.length() とでは話が全然違う。 Java の誕生から10年というのだが、 多分 JDK 1.0 の頃からあったようなメソッドの公式マニュアルに、 そんな単純な誤記が今まで残っているとは想像できなかったのだ。 今まで誰もそのことを指摘しなかったのだろうか? あるいは、マニュアルなんて熟読する人はいないのか、 それとも、熟読するまでもなく、ここは this が正しいに決まっているということで、 str と書いてあるにも関わらず、誰でも持っている大脳の自己訂正機能が働いて、 頭は勝手に this.length() と書いてあると解釈してしまうのが正解なのだろうか。

ともあれ、条件1は、

     k >= Math.min(fromIndex, this.length())

となる。 これは言い換えると、 k は fromIndex 以上、ただし、文字列の長さよりも fromIndex が大きい場合は、文字列の長さ以上、という条件である。 "my beautiful dream" の長さは18だから、 これに対して indexOf(str, 30) を実行すると、 前半の条件は、「k が 30以上」ではなく「k が 18以上」ということになる。 では、なぜそのような条件が必要になるのか? 実は私はそのことに気付かなかったのだが、 Java Technology Forum ではある人が指摘してくれた。 皆さんも考えてみてください。

Java に詳しくない人のためのヒントを一つだけ紹介しておく。

startsWith(String prefix, int offset) というメソッドは、 指定した文字列が指定したインデックスから始まっているかどうかを boolean で返す。 offset に文字列の長さ「よりも大きな値」を指定すると、 このメソッドは 常に false を返す。 つまり、長さ18 の文字列に対して、 this.startsWith(str, 30) を実行すると、 str の内容とは無関係に、結果は false になる。

参考: Java Technology Forum

  

Note

長さ20の文字列に対しては、 前半の条件は k >= 30 ではなく、 k >= 20 ということになる。 fromIndex よりも、this.length() の 20 という値が小さいからだ。 しかし、this.length() が 20 なら、 20文字目というのは、文字列は既に終わった所を指すはずである。 this.startsWith(str, 20) が true になるようなことがあるのか?
それがあるのだ。 Java の仕様では、 this.startsWith("", 20) は true なのである。 つまり、 長さが20 の文字列に対して、 indexOf("", 30) は true を返すのである。 これがどういう意味を持つのか、 具体的には難しいのだが、 ある種のアルゴリズムを実現するために都合がよいというご指摘をいただいた。

(C MAGAZINE 2004年11月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-03-01 裏ページに転載。

(C) Phinloda 2004-2006, All rights reserved.