今回は、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を使うと良い