文字列の Tips and Ticks

Revised: Feb./23rd/2003

システムにおいて文字列の処理は重要な機能です。Javaでも文字列操作の処理は重要であり、基本機能、正規表現、国際化などの役割を持つ標準クラスが多数用意されています。ここでは、文字列処理に関わるパフォーマンス上の問題を解決する、基本的なクラスの使い方を紹介します。

Q1. StringとStringBufferの違いは?

String型オブジェクトは内容の文字列を変更できませんが、StringBuffer型オブジェクトでは可能です。

String型オブジェクトのように、内容/状態を変更できないものを不変オブジェクト(immutable object)と呼びます。文字列は頻繁に使われるオブジェクトなので、Java実行環境は特別な取り扱いをします。基本データ型の振る舞いに似せるために、リテラル "~" の記述により、オブジェクト生成が行われます。更に、記述した文字列リテラルが、既に生成された文字列と同じであれば、既存の文字列への参照が代入されます。

例えば、

String str = "Hello";

の場合、"Hello"という文字列が既に存在しないか調べます。存在すれば、strには作成済みの同じ文字列への参照を代入します。存在しなければ、新たにプールして、その参照を代入します。

オブジェクトの生成はコストが高い処理なので、オブジェクトをプールして再利用するこの方法は非常に効果的です。

StringBuffer型オブジェクトのように、内容が変更可能なものを可変オブジェクトと呼んで不変オブジェクトと区別することがあります。変更用の余分なメモリ領域(つまりバッファ)を持っているので、String型オブジェクトに比べると、メモリ領域を余分に消費することになります。

しかし、StringBuffer型オブジェクトは、バッファ内で不定長の文字列を操作できるために、Stringオブジェクトのように、変更の都度、一時オブジェクトを生成する無駄がなくなります。

一般に、参照型変数に代入された不変オブジェクトを変更するには、別のオブジェクトの生成、新しい参照の代入、古いオブジェクトの破棄という手順が必要になります。一方、可変オブジェクトの変更は、既存のオブジェクト用のメモリ領域内での作業です。このため、不変オブジェクトの変更はコストが高くなると言えます。

変更しない文字列の代入にはString型オブジェクトが簡単です。変更する文字列にはStringBuffer型オブジェクトを使うことで、一時的に生成されるオブジェクトの数を減らすことができます。不変オブジェクトの変更コストの高さは、Stringオブジェクトに限った話ではなく、Javaのコア・パッケージ定義されている多数の不変オブジェクトに共通することです。

Q2. 文字列の連結で遅くなる?

+演算子で文字列の連結を繰り返していると、パフォーマンスが悪化することがあります。これを避けるためにStringBufferが有効です。

Javaの二項演算子の一つである+演算子は、文字列を演算対象としても機能するようにオーバーロードされています。二項のうち一方でも文字列であれば、連結子として機能します。例えば、

String str = "Java" + "World" + 2003;

となっていれば、strには"JavaWorld2003"が代入されます。文字列の連結では、メモリ消費量を抑制するために、コンパイル時にStringBufferのappend()を使うステートメントに変換されます。今の場合は、次のステートメントと等価になります。

String str = new StringBuffer().append("Java").append("World").append(2003).toString();

バッファを使うことで、一時文字列の生成を抑えています。自動的に行われる便利な仕組みなのですが、次の例を考えてみましょう。

String str = "Hello";
str = str + "World";
str = str + 2003;

先ほどの例とまったく同じことをしているように見えますが、コンパイル結果は次のステートメントと等価なバイト・コードとして生成されます。

String str = "Hello";
str = new StringBuffer().append(str).append("World").toString();
str = new StringBuffer().append(str).append(2003).toString();

都合三つのオブジェクトが作られています。オブジェクトの生成はコストの高い操作であり、パフォーマンス上好ましくありません。

文字列の連結操作は、一つの行内で行うべきです。ステートメントを分けてはいけません。止むを得ずステートメントを分けなければならないこともあるでしょう。例えば、次のようにブロック内で連結する場合です。

String str = "count";
for (int i =0; i < 100; i++) {
    str = str + i;
}

このようなときは、事前にStringBufferを生成して、ブロックの外でStringに変換します。

String str = "count";
StringBuffer sbuf = new StringBuffer(str);
for (int i =0; i < 100; i++) {
    sbuf.append(i);
}
str = sbuf.toString();

文字列の連結は一つのステートメント内(行内)で行い、複数ステートメントに分割される場合は、StringBufferを使います。

Q3. 文字列バッファのサイズは?

文字列バッファのサイズは自動的に拡張されますが、パフォーマンスの悪化を招くコストの高さを認識しておく必要があります。

StringBuffer型オブジェクトのサイズの省略時値は16です。足りなくなると、倍のサイズの別のオブジェクトが生成され、内容をコピーし、古いオブジェクトは廃棄対象になります。ここにもオブジェクトの生成と廃棄のプロセスが潜んでおり、パフォーマンスを悪化させます。

ほとんどの場合、必要なサイズが見積もれるはずなので、最初からバッファ領域の追加が発生しないように、オブジェクト生成時に指定しておいた方が良いでしょう。次の例は、必要となる最大サイズを500未満と見積もった場合です。

StringBuffer sbuf = new StringBuffer(500);

文字列バッファのサイズについて注意すべき局面があります。一般に、パフォーマンス向上の原則は、オブジェクトを生成しないことであり、そのためにオブジェクトの再利用が推奨されます。しかし、StringBuffer型オブジェクトは再利用しない方が賢明です。StringBufferの文字列バッファの容量は、一度増えたら減らないため、長期間にわたって再利用を繰り返すと、予想以上にバッファ・サイズが拡張されており、メモリ使用率をむやみに圧迫する可能性があるためです。

StringBufferを使うときは、その都度必要なサイズを見積もって生成し、無闇に再利用を試みない方が良いでしょう。これは、StringBufferに限った話ではなく、自動的にサイズが拡張されるようなクラスについては等しく言えることです。

Q4. 文字列分割は簡単にできる?

クラスStringBufferは、文字列の連結をはじめ、文字列操作のための汎用的なメソッドを数多く備えており、文字列の操作に関しては万能です。しかし、文字列の分割について特化したクラスも存在します。それが、クラスjava.util.StringTokenizerです。

詳細については、APIドキュメントをご覧いただくとして、そこには、以下のような例が載っています。

StringTokenizer st
	= new StringTokenizer("this is a test");
while (st.hasMoreTokens()) {
	println(st.nextToken());
}

このコードでは、「this is a test」という文字列をクラスStringTokenizerのコンストラクタに渡しています。続くwhile文で使用しているメソッドhasMoreTokensは、分割した要素(トークン)を返せるのであればtrueを返し、さもなければfalseを返します。メソッドnextTokenは分割した要素を返し、次の要素へ移動します。文字列の分割に使う区切り文字(デリミタ)を指定していない場合、空白や改行文字がデフォルトの区切り文字になります。このプログラムの出力は次のようになります。

this
is
a
test

もちろん、クラスStringTokenizerでは、コンストラクタに任意の区切り文字を渡すことができますし、戻り値に区切り文字自身を含めるかどうかを指定することもできます。  StringTokenizerはとても便利なクラスですが、これを使用する際には注意が必要なケースがあります。典型的な例が、CSV(Comma Separated Value)形式のデータです。最も簡単なCSV形式のデータは次のようになります。

A,B,C

区切り文字としてカンマを指定すれば、「A」と「B」と「C」が得られます。それに対し、次のようなデータには工夫が必要です。

"A,B",C,D

このようにカンマを含む要素があると、「"A,B"」、「C」、「D」の3つの文字列ではなく、「"A」、「B"」、「C」、「D」の4つに分割されてしまいます。このような場合は、区切り文字を含んだかたちで分割し、さらにダブル・クォーテーションに対する条件分岐を使って文字を連結しなおす必要があります。  別法として正規表現の利用も検討できます。HTML, SQL, CSVなどで、特殊文字や改行文字を無効な文字に置換することをサニタイジング(無害化)と呼び、古くから正規表現で実現されてきました。Java 2 SDK 1.4以上であれば、パッケージjava.util.regexで実装されています。

簡単な場合にはStringTokenizerが便利です。パフォーマンス上の厳しい要件がある場合や、複雑なデータを取り扱う場合は、クラスStringBufferを使ったほうが有利です。J2SE 1.4以上では、パッケージjava.util.regexのほか、staticメソッドString.split()でも正規表現による文字列分割がサポートされています。



Copyright © 2003 SUGAI, Manabu. All Rights Reserved.