谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

レビューで鍛えるJavaコーディング力 その5(文字列操作)

今回は、Apache Commons Langを用いた問題です。

問題

以下のコードの問題を指摘し、修正してください。
ただし、問題は複数あることもあれば、全くないこともあります。

import org.apache.commons.lang.StringUtils;

public class TemplateReader {
	private String template;
	private String[] templateLines;

	public TemplateReader(String resourceName) {
		this.template = readTemplate(resourceName);
		this.templateLines = StringUtils.split(this.template, "\r\n");
	}

	/**
	 * テンプレートの本文全体を取得します。
	 * @return テンプレートの本文全体
	 */
	public String getTemplate() {
		return this.template;
	}

	/**
	 * テンプレートを改行で分割した配列を取得します。<br>
	 * 空行も空文字列として、配列に含めます。
	 * @return テンプレートを改行で分割した配列
	 */
	public String[] getTemplateLines() {
		return this.templateLines;
	}

	/**
	 * テンプレートのリソースを読み込みます。<br>
	 * テンプレートが読み込めない場合には、空の文字列を返します。
	 * @param resourceName リソース名
	 * @return リソース名に対応したテキストファイルの本文
	 */
	protected String readTemplate(String resourceName) {
		String result = null;
		// 省略
		return result;
	}

ファイル(リソース)読み込みを行い、その後に文字列操作を行なっています。

コラム

今週の1/27(水)に、関西Javaエンジニアの会(関ジャバ)の勉強会を開催します。


今回は「T2フレームワーク」「Griffon」「GWT vs XWT」など、
まだちょっと手を出してない人が多そうな(隙間の?)ネタが中心になっているので
興味はあるけど、実物を見たことがない、という人にもオススメです。


http://kokucheese.com/event/index/1199/
まだ少しだけ残席があるので、ぜひ今からでも応募してくださいね!


また、関ジャバでは発表者も募集しています。
Javaに直接関係ないネタでも構いませんので、とにかく「話したい!」という想いのある人は、
メールでもコメントでも、Twitterでも何でも構わないので、ぜひ、声を掛けてください!

解答

今回は、ライブラリの利用方法に誤りがあります。

this.templateLines = StringUtils.split(this.template, "\r\n");


splitに正規表現を使いたくない場合によく使うStringUtilsですが、
APIをきちんと確認しないと、思わぬ挙動に悩まされることがあります。


StringUtils#splitは、以下ような挙動になるのです。

  • 空要素はスキップする(連続したセパレータは、一つのセパレータとみなす)
  • セパレータに指定された文字列の一文字ずつで分割する


今回の例では、テキストファイルを読み込み、空行も空文字列として扱うのですから
空要素をスキップされては困ります。
そのため、空要素をスキップしない「splitPreserveAllTokens」を利用する必要があります。

this.templateLines = StringUtils.splitPreserveAllTokens(this.template, "\r\n");


しかし、これでもまだ問題があります。
StringUtils#splitは、String#splitと違い、引数に指定されたセパレータを
それぞれ1文字ずつに分割し、それぞれをセパレータとして扱います。


要するに、第二引数の「\r\n」は「\r」と「\n」に分解され
「\r」で分割し、「\n」でも分割されてしまうわけです。
そうすると、たとえば

StringUtils.splitPreserveAllTokens("abc\r\ndef", "\r\n");

この結果は {"abc", "def"} ではなく {"abc", "", "def"} となるのです。


引数に指定したセパレータ文字列を、まとめて1つのセパレータとして扱いたい場合は、
「splitByWholeSeparatorPreserveAllTokens」メソッドを利用します。

this.templateLines = StringUtils.splitByWholeSeparatorPreserveAllTokens(this.template, "\r\n");


これで、期待通りの挙動になるはずです。

補足

上の解答の対処で、少しだけ気になる所があります。
それは末尾の改行に関して、です。


テキストファイルを作成する際、末尾に改行を入れたり入れなかったりすることがありますが、
仮に末尾に改行が入っていた場合、その次の行は空行とみなすべきでしょうか、
それとも無視すべきでしょうか?


たとえば同じApache CommonsのFilesUtils#readLinesなどでは、
末尾の改行コードは無視するような挙動となっています。
(末尾に改行コードが連続していれば、そこまでの空行はきちんと保持されます)


無視するか、無視しないか、どちらが正解かは仕様次第でしょうけど
現実的には、末尾の改行は無視する方が妥当だと思います。


末尾の改行を削除するには、StringUtils#chompメソッドを使えば良いでしょう。
「\r\n」「\r」「\n」のいずれでも、末尾の改行を一つだけ削除してくれます。

まとめ

それでは、今回の問題をおさらいしておきましょう。

  • StringUtils#splitは、第二引数を分解して1文字ずつのセパレータとして扱う
  • StringUtils#splitは、空の要素をスキップする
  • 末尾の改行を削除する場合は、StringUtils#chompを使うと良い