String非推奨の勧め

Javaプログラムにおいて,クラスを作ることを厭う人たちが多い.
そのような人たちの多くはデータを桁数依存にて構造が存在する文字列にして扱うことを好む.
しかしJavaにおいてStringを解析することは多くの例外の原因となり,ひいてはシステム障害の原因となることが多い.
またStringの演算は重く,Stringはメモリ消費量が多い.

この文章では,Java利用システムにおいてStringの濫用を戒め,適切な型の利用と適切なクラス設計を行うことを勧める.*1

Stringの問題

多発する例外

Stringを利用することにより発生する例外には次のものがある.

  1. NullPointerException
  2. StringIndexOutOfBoundsException
  3. IndexOutOfBoundsException
  4. IllegalArgumentException
  5. UnsupportedEncodingException
  6. PatternSyntaxException
  7. IllegalFormatException

特に問題が大きいのが最初の3つである.


NullPointerExceptionは全てのオブジェクトの使用において考慮せねばならない例外である.もちろんStringの使用においても多く発生する.
日本のSI現場ではStringをプリミティブなデータ構造として用いる場合が多い.また設計がオブジェクト指向設計ではない場合が多いため,必然的にStringを関数引数として使いまわす場合が多い.このためプログラムのあちこちに引数がnullでないか,空文字列でないかを確認するコードがコピペされている.


しかし実際には多くのプログラマは親関数がチェックしていることを理由に黙ってそのチェックを回避してしまう.これはそうすることで単体テスト仕様書,報告書の仕事量を減らせるからだ.if文を少しでも減らすことは高速化という理由とともに,テストのカバレッジが調査されるため,少しでもテスト項目を減らす目的でプログラムが記述される.しかし仕様変更の多い現場では簡単に前提が覆り,テストの見直しだけは行われず,本番運用に入って初めてNullPointerExceptionが発生する場合が多い.*2


IndexOutOfBoundsExceptionはさらに問題発生率が高い.多くのプログラマはStringがcharの配列であるということを理解していない.それでも日本のSI現場では桁数依存のデータがあまりにも多い.日本のSI現場では何でも桁数依存の文字列にてデータを表現し,何桁目の文字が何だから何を行う,というような効率の悪い設計を行ってしまう.


しかし多くのプログラマは文字列の長さを事前に測る必要も忘れて目的の文字を取り出してしまう.上記と同じように正常系しかテストしないためにある日突然例外が発生する.IDの設計が変わってしまうことも多い.チェックコードの集約がなされていないため,変更箇所はシステム中に分散し,変更漏れが発生する.定数がハードコードされていることも珍しく無い.*3


あるプロジェクトでは本番稼動に入ってからも大量に障害が発生したが,そのほとんどがStringの例外によるものであった.そのプロジェクトで使用したWebフレームワークはほとんどのAPIの引数がStringか,Stringの配列であった.例えばJDBCのラッパがあるのだが,Stringしか使えないという具合である.


J2EEの開発において例外処理はやっかいなものである.通常はフレームワークに一任されているが,フレームワークができる処理もログを取り,エラー画面に遷移させることくらいである.これは障害の迷惑を直接被るユーザには何の救いにもならない.


これらの問題は本来データ構造をまともに設計しておけば問題が発生しにくい.
プログラマに責任を押し付けるケースが多いが,実際には設計レベルの低さ故の問題である.

Stringの問題(2)

Stringはメモリ効率が悪い

Stringはメモリ効率がとても悪い.
Stringのサイズは文字列の長さにより変化し,大きさが以下の公式にて求めることができる.
あるStringが文字数nの場合,Intel製32bitCPU上,Windows上にて

Sun, BEA 40+(n/4)*8
IBM 48+(n/4)*8

(割り算の少数は切り捨て.また実際には変数nに0から3のずれが生じる.詳しくは*4参照)

上の式から明らかなようにStringは空文字列でも40バイト(IBMなら48バイト)を消費する.
8バイト境界でcharが2バイトのため,文字数に応じてより多くのメモリを消費する.

intなら4バイト,charなら2バイト,char[]なら16+(n/4)*8である.

class Id {
  char group;
  char class;
  int number;
}

は,24バイトだ.

次のことが言える.

  • クラスを設計したほうが,文字列IDを使うよりメモリ効率が良い
  • クラスを設計したほうが,チェックメソッドを集約できる
  • クラスを設計したほうが,setter, constructorにて異常値を拒否できる
  • クラスを設計したほうが,データ構造の仕様変更に強い
  • クラスを設計したほうが,静的型チェックを利用することができる

日本式に文字列IDを表示したいときにはtoString()を実装する.
toString()は一時的にStringを生成してしまうが,これの使用時間を短く抑える*5ことができれば世代別GCの機能によりメモリ獲得も解放も非常に少ないコストにて実行することができる.*6


Stringインスタンスはcharの配列インスタンスを共有することが出来る.
ソースコード上の文字列定数は常に同一の物が共有される.
また長い文字列の部分文字列生成も共有される.
Stringインスタンス自身も参照レベルで共有できる.
このことをしっかり理解しておけばある程度のメモリ効率を上げることができる.
しかしStringの演算を行えば新しい文字列が生成される.
またデータベースや,ネットワークをデータソースにすれば必ず新しい文字列が作成されている.これは無視できない容量となる.仮にこれらのStringをデータ構造を設計したクラス側にてvalueOf(String)メソッドを用意してインスタンス化してしまえば,以降の処理ではプログラムの記述が楽になる上に,大きなStringがメモリ上に長時間残ることを防ぐことができる.

Stringの問題(3)

Stringの演算は重い

どこの現場でも見るのが次のような定数クラスだ.

class BigConstant {
  public final static String NameA = "NameA";
  public final static String NameB = "NameB";
   :
   :
   :
}

まず定数(専用)クラス自体の存在に優れた設計者なら疑問を持つだろう.
Joshua BlochのEffective Javaでは定数クラスを外部に実装の詳細を公開するものとして推奨していない.
しかし日本のSI現場では定数クラスが好まれて使われているのを多く見てきた.
今は定数クラスの良し悪しは置いておくとして,この定数の無駄を論じたい.


定数名を値として持つ,String定数を作るのが日本のSI現場では流行のようだ.これなら値のない名前の集合を作成する場合に,値を考えなくてすむと考えたのだろう.このルールに則って定数を作成すればフィールド変数名がコンパイラ重複チェックされるので,巨大なシステムの全ての定数を扱うクラスを作成しても重複した定数値を作成することを避けることができると考えたのであろう.また定数名を値に持てばログを取るときに簡単だと考えたのかもしれない.


しかし,このString定数にはやはり問題がある.
まず既に指摘したようにString定数はメモリの無駄である.
値に意味が無いのであるのならばintでも良いはずだ.
それだけで無駄なメモリ消費がかなり改善される.
Stringの定数を入れることをプログラマが想定していても,String型の変数(又は引数)にはどんな長い文字列も入ってしまう.Stringという不安定な型を定数に採用してしまうマイナス面は大きい.


次にStringの比較はintのそれに比べ重い.
String#equalsのソースを読めば簡単にわかるが,Stringの比較は全ての文字を比較すると言うことである.定数がintにて定義されていれば比較は1回ですむ.しかしStringの場合は参照を比較し,長さを比較し,最後に全ての文字が一致するまで比較する.
1回の比較ならまだ大した差にはならないかもしれない.しかし技術力の低いプログラマはfor文でString配列を全部比較するようなことを平気でしてしまう.設計レベルでこのようなことが起こらないようにするべきだ.


またString定数にはswitch文が使えないという問題がある.
HashMapを濫用することはメモリ効率上避けたい.空のHashMapでも64Kバイトほど消費する.
またHashMapに多くのStringの参照を残しておくと,世代別GCでもGCの対象とならず,結果,比較的早くにOutOfMemoryにてJVMが死んでしまう.


String定数はやめるべきだ.
もしJDK5.0を使えるならばenumを使うべきだ.*7
enumを用いれば,静的な型チェックを行え,比較もインスタンス参照のみで速く,switch文も使えるようになる.enumはtoStringもvalueOfも実装しているので文字列との相互変換も楽だ.
enumは変数間で参照が共有されるのみで,新しい値が作成できないことが保障されているのでメモリ効率上もとても好ましい.
JDK1.4以下しか使えない場合はTypesafeEnumパターンを利用することになる.
TypesafeEnumパターンを利用すればほぼJDK5.0のenumと同じことが実現できる.ただし,TypesafeEnumパターンにはシリアライズが行われるとインスタンス参照の値が異なってしまうケースが発生する.*8この場合はStringと同じようにequals()を使うように心がければ大丈夫だ.JDK5.0ではシリアライズ実装がenum専用に拡張されているためこの問題は起こらない.


ところで今でもStringをIntに変換し,演算して,またStringに戻すプログラムを良く見かける.
実に無駄な処理だ.
また今だにStringの足し算を使うのを禁止し,StringBufferの使用を強制する静的解析ルールを見かける.
そもそもは設計者が無意味なStringの使用をやめるべきだ.
これらは全て設計者のレベルの問題である.

なぜStringが濫用されるのか

プログラムで使用されるデータは個別のIDを用いて区別される場合が多い.
日本のSIではIDを文字列とし,値の桁依存により複雑な構造を取る場合が多い.
例えば"WQD22GD335"のようなものである.
日本のSI現場では無用なID付けが濫用され,それが全てのデータをStringにしてしまう原因になるようだ.


通常はIDの先頭にそのIDが何であるか(IDの型)を示す文字がくる.*9
次にそのIDの作成グループや責任者などの識別子が入れられるようだ.
複数のデータに階層構造がある場合には親IDが必ず子IDの頭,または胴体に付く場合も多い.この場合下の層にいくほどIDが長くなる.
末尾には個別のナンバリングが付く場合が多い.しかし番号順の意味は特に無い場合がほとんどだ.並びを気にする場合には値が5や10ごとに間を空けて付けられる.レベルの低い設計者になるとナンバリングの後にさらに英数字を用いる者も存在する.長さも酷いものでは20文字に近くなる場合がある.


そもそも全ての情報にIDが必要なのか疑わしい.しかしIDを付けることがSEの仕事だと思っている設計者も多いようだ.例えば全てのクラス名,酷いところになると全てのメソッド名にもIDを付ける設計者を見た.全てのパッケージ名もIDである.このような設計がEclipseの補完機能のような生産性向上ツールを無力にし,生産性を下げていることを設計しか行わないSEには気付く機会すらないのであろう.最近はメソッドは見なくなったが,パッケージ名とクラス名がIDであるのは今でも当たり前のように見かける.


データの集合は別に名前の集合であってかまわない.
ナンバリングは必須ではない.必要なら後から自動で付けることは可能だ.
個別の認識が常に名前で行われれば,番号が変わることを考慮する必要はない.
しかし私が過去にお付き合いしたSEは値のユニーク性に非常に強いこだわりがあるようだ.
そしてその簡単な実現方法として,常に全ての関係する要因をcharにし,さらに日付や時間やナンバリングを加えてユニークさを確保するようだ.
エクセルでもユニークな列を作るということは簡単だと思うのだが,残念ながらメソドロジーにて求められているようだ.つまりは上に上がるにしたがってチェックを通らなくなってしまうと言うことのようだ.


もう一つ日本のSI業界で(私にとって)不思議な風習がある.
設計者の中に弱い型を好む人たちが少なからずいるのである.
彼らの説明によるとインターフェースは弱い型を用いなければ拡張性が保持できないと主張する.
そのせいで彼らの書くプログラムは,

    return "OK";

というものになる.
プログラマの生産性とプログラムの品質が低下するのは確かだ.
Joshua Blochの"How to design Good API and why it matters"*10では適切でない場合にStringをAPIに用いることを否定している.

桁数依存の弊害

2000年問題というものがあった.2000年になると年のデータを2桁で保存しているプログラムはその処理に問題を起こすというものであった.現場の人間は正月から現場に泊り込みとなり泥をかぶった.


あの問題を経験して,今なお桁数依存のデータを作り続ける設計者達を不思議な思いで見ている.


桁数依存のデータは拡張性が無い.
桁数依存のデータは仕様変更に弱い.
桁数依存のデータは処理が複雑だ.


適切なデータ構造を設計すれば,拡張性が高まる.良いインタフェースを設計すればさらに拡張性は高まる.
適切なデータ構造を設計すれば,仕様変更にも強くなる.処理を共有し,定数を内包し,隠蔽すれば,それらの変更は外に影響を与えない.
適切なデータ構造を設計すれば,文字列のパースに比べ,データへのアクセスは簡単だ.またコンパイラによる静的な型チェックも行うことができ,品質を高めることができる.

もし値の桁数を保障したいのならば,それはコンストラクタ,setterの仕事だ.
XMLならSchemaで行うのもいいのかもしれない.
しかし,データの型をcharにして桁数を保障するのは既におかしなやり方だと言えよう.

まとめ

日本の現場ではなんでもID付けを行い,IDが複雑なcharの桁依存になっているため様々な問題が生じている.
特にJavaでのStringによる実装はメモリー消費の効率,処理量の効率,例外の扱いにくさからいって問題が多い.桁数依存のStringではなく,データ構造の設計をあらかじめしっかりと行うことが必ず後の問題を防ぐことになる.また適切なデータ構造の設計は生産性を向上させる.
データの適切な型を考慮する場合,Stringが向いている場合は多くはないはずだ.
クラスのメンバがStringばかりであったり,テーブルの列がCHARやVARCHARばかりであるのならば,再考が必要なサインだと言えるだろう.

*1:何を当たり前のことをと思われるかもしれないが,大量生産を主眼とする現在の日本のSI現場ではJavaでもオブジェクト指向を利用しないのが,少なくとも私の周りでは,当たり前である.最近でもオブジェクト指向など役に立たないとブログに書く社長がいたり,Blancoのようにオブジェクト指向は巨大開発に向かないと主張するフレームワークもSI業界には数多い.これらの全てに反対するわけではないが,それらの意見を悪用する無知な技術者により現在のSI現場にて多くの問題が発生している状態を少しでも改善したい次第である.

*2:もちろん,このような現場ではテスト駆動開発が行われることはまれだ.

*3:ウォーターフォールの開発では共通部品を作るということが非常に困難なことがこのような問題の前提にあると思われる.

*4:http://www.javaworld.com/javaworld/javatips/jw-javatip130-p2.html

*5:例えばprintfに渡すだけ等

*6:http://www.atmarkit.co.jp/fjava/rensai3/javavm02/javavm02_1.html

*7:http://www.javacamp.org/designPattern/enum.html

*8:http://www.javaworld.com/javaworld/javatips/jw-javatip122.html

*9:よって同じ「型」の文字列は頭の部分が全て同じだ.これらが全部無駄にメモリを消費しているのだが設計者は気にならないようだ.

*10:http://lcsd05.cs.tamu.edu/#keynote