文字列を処理する方法

文字列を処理するといえば、検索、抽出、比較、置換、整形ということに集約できそうですが、どういう手段を使うかについてまとめておきます。

検索

  • 正規表現
  • indexOf メソッド
  • lastIndexOf メソッド
  • カスタム Scanner クラスを利用(自作なりDLなり)

抽出

  • substring メソッド
  • split メソッド
  • trim メソッド
  • java.util.StringTokenizer を利用
  • カスタム Scanner クラスを利用(自作なりDLなり)

比較

  • equals メソッド
  • equalsIgnoreCase メソッド
  • startsWith メソッド
  • endsWith メソッド
  • matches メソッド
  • java.text.RuleBasedCollator を利用(大小関係のルールを任意に定義できる)

置換

  • toUpperCase メソッド
  • toLowerCase メソッド
  • replaceFirst メソッド
  • replace メソッド
  • replaceAll メソッド
  • カスタム Scanner クラスを補助的に利用(自作なりDLなり)

整形

  • 単純に + する
  • concat メソッド
  • StringBuilder クラスを利用
  • StringBuffer クラスを利用
  • java.text.MessageFormat を利用
  • カスタムフォーマッタを利用(自作なりDLなり)


とりあえずそれぞれの目的ごとに最もよく使うであろうものから順に並べてみています。手に負えなくなると下の方が使われていくような感じです。

JavaDoc はいつでも読めるようにしておこう。何がどこにあるかの大体の目処を頭に入れておこう

これを何のために挙げておくかというと、以下のような切り口の主張を私が好まないためです。

  • 「全部正規表現で処理します。応用範囲広いし統一できるから」
  • 「全部 StringBuilder 使います。わかりやすいから」
  • 「全部 + でつくります。最適化されて速いから」


「それで何が悪いの?」と思う方には、今何をしようとしているかを表現するにはもっとうまく役割を理解して構成した方がいいんじゃないの?と問いかけてみたいところではありますが、多分、JavaDoc を参照するような環境が揃っていないとか、本を開く余裕がないとか、そういう事情があるんでしょうが、気がかりです。

せめて java.lang.*、java.io.*、java.text.*、java.util.* はひと通り眺めて、いつでも JavaDoc 開いてサンプルコード書いて使えるようにしておくだけで、複雑なコードを書いてしまったり大きな jar をダウンロードしてきて依存しまくったりせずに済むケースが多いのです。

Reader クラスを活用しよう

文字列を1文字ずつ取り出して何かをするような状況で、

String s = "abcdefg";
for (int i = 0; i<s.length(); i++) {
    switch (s.charAt(i)) {
    case 'a':
      …
      …
    default:
      …
      break;
    }
}

というようなコードを見かけることがありますが、文字構成の前後関係を気にして何か処理をするときにはこのやり方は条件分岐を多数記述することになりがちで、困ったことになることが多いです。どうリファクタリングしてもなかなかうまくいかない。

そんなとき、以下のような記述を行うとうまくいくことが結構あります。慣れると応用範囲がとても広いのですが、上のやり方に慣れすぎると無自覚にも「やってみようとすら思わない」というような壁ができていることがあります。

StringReader r = new StringReader("abcdefg");
int ch;
while ((ch = r.read())!=-1) {
    switch (ch) {
        …
    }
}

「IOException を catch する記述が必要になるからインデント深くなって嫌だ」とかあれこれ理由をつけて遠慮されるものですが、結局のところ「恩恵感じるまでやりこまないと聞いたところでわからない」ってこともあるのかもしれません。

というのはさておき、Readerクラスには以下のような特徴があります。

  • 「文字」およびその配列の読み込みに適したクラスである
  • 文字列、ファイル、ストリームなどいろいろなデータソースから文字を読み込んで処理できる
  • FilterReader を派生させることで、あらかじめデータソースの内容を加工できる
  • 文字列を読みすぎた場合に「読まなかったことにし」「やりなおす」ことができる


これのメリットは以下の通り。

  • 文字列中の位置を示す具体的なインデックスがないため、読み出す順番を間違えない。
  • 途中まで読み出した結果、他のメソッドに処理を移して後続の文字列の処理を任せたいというようなときに、文字列だとsubstringなりインデックス渡しなりをする必要があるが、Reader クラスを使っていれば単に Reader を引き渡すだけとなる。
  • 余分な空白はあらかじめ除去する、とか途中の # から LF まではコメントとみなしてなかったことにする、などといったことを Filter で前処理として抜き取ることができる
  • 複数の文字で特別な意味を持つケース(-- でコメント、など)の場合に「読み戻す」のにインデックスを操作することなく、unread メソッドで文字を「押し戻し」、処理を切り替えたり、予めマークしておいた位置に reset メソッドで戻ったあとで処理をやり直すことができる。


こうやって書いたコードは、まさに文字を処理する記述だけで構成され、現在読んでいる位置の情報が雑味として混じることがありません。

Writer クラスを活用しよう

StringBuffer/StringBuilder のように途中を加工する必要がない場合には、StringWriter を使うことで確定した出力文字列はもう操作できないから意図がはっきりする、というような言い回しで個人的には利用を推奨していましたが、実際には中身が StringBuilder を利用していることもあり、同期がかかって若干処理速度が落ちるというのがあって良いことばかりではありません。

しかし、StringWriter はともかくとして、Writer クラスの活用にはやはり以下のようにメリットがあります。

  • 出力先としてファイル、ストリーム、文字列などがあるが利用する側は与えられた Writer オブジェクトを使うだけである。
  • FilterWriter の派生としてクラスを作る(他の Writer クラスでもかまいはしませんが)ことで、ある出力のついでに他の出力(ヘッダやフッタなど)を追加したり、不適切な出力を抑制したり、バッファリングを行うことができる。


要は本来の処理に関係ない出力される側の事情を Writer クラスが担うことでコードの役割の分離を行い、見通しをよくできるということです。