Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

あなたのJavaコードをスッキリさせる、地味に便利な新API 10選(前編)

こんにちは!
アキバです。

...T3ブログは初登場かもしれません。ハジメマシテ。
以後お見知りおきを。

いよいよ、2014年3月、Java8が正式公開されますね。
f:id:acro-engineer:20140311021635j:plain

なんと言っても、Java8の注目機能はラムダ式ですので、ラムダ式型推論に関する記事は多いです。
世の中で「Java8」と検索すると、皆さんいろいろと記事を書かれているので、おおよその事はこれで分かっちゃうような気がします。

が、

実は地味に便利なAPIが追加されていたりすることを最近知りました。

これはあまり触れられていないぞ、と。

というわけで、このエントリでは、あまり日本語で情報の無い、しかし地味に便利なAPIに実際に触れてみます。

大事なところなので2回強調して書いてみました。


今回は、前編として4つ紹介します。

APIの紹介と言いつつ、コードにはラムダ式を使ったコードが普通に出てるので、ご了承ください。
 (むしろ、こういったコードを見ながらラムダ式の実際を理解するのもイイかも?)

1. 文字列結合編

これまでのコード

これまでの文字列結合は、おおよそ以下のような感じだと思います。
ここでは、Stringの配列からカンマ区切りの文字列を作る例を考えてみましょう。
(この例は配列ですが、通常はList<String>を使うことが多いと思います)

String[] strArray = { "abc", "def", "123", "456", "xyz" };
String separator = ",";

StringBuilder sb = new StringBuilder();
for (String str : strArrayt) {
    if (sb.length() > 0) {
        sb.append(separator);
    }
    sb.append(str);
}

System.out.println(sb.toString());
// → abc,def,123,456,xyz という文字列が表示される

こんな処理を簡単に書くためのAPIがJava8で追加されました。
(もっと昔からあっても良かったようなものですよね…)

主なものは次の2つです。

(1) String#join(CharSequence, Iterable)

1つ目の新APIは、Stringクラスに追加されたjoinメソッド
文字列を直接並べることもできますが、メインとなるのは配列やListなどのIterableを使う方でしょう。
String#join()を使うと、先ほどのコードは以下のように書き換えることが出来ます。

String[] strArray = { "abc", "def", "123", "456", "xyz" };
String separator = ",";

System.out.println(String.join(separator, strArray));
// → abc,def,123,456,xyz という文字列が表示される

なんと、ループもStringBuilderもなく一行で書けてしまいました。スッキリ。

(2) StringJoiner

2つ目の新APIは、StringJoinerというクラスです。
目的はString#join()とほぼ同じです。
というか、このクラスは、String#joinの中からも呼び出されていたりします

早速、同じように書き換えてみましょう。

String[] strArray = { "abc", "def", "123", "456", "xyz" };
String separator = ",";

StringJoiner sj = new StringJoiner(separator);
for (String str : strArray) {
    sj.add(str);
}

System.out.println(sj.toString());
// → abc,def,123,456,xyz という文字列が表示される

…ループもあるし、意外とスッキリしてませんね(笑)

使い方もStringBuilderと似ていますが、主な違いは以下の3点くらいですね。

  1. 区切り文字を自分で追加しなくなった
  2. 接頭辞と接尾辞を指定できる(CSV全体をカッコで囲むみたいなことができます)
  3. 要素を追加していない時の文字列を指定できる

れっつ、ベンチマーク

ところで、いつのバージョンでも「文字列結合の性能ネタ」はありますが、上記の新APIは、パフォーマンス的にはどうなんでしょうか?

ここでは、以前に @cero-t も紹介していた マイクロベンチマークツール「JMH」を使って、10000個の5バイト文字列("abcde")をカンマ区切りで連結する処理の実行時間を測定してみました。
(※正式リリース前のJava8で実施しているため、参考情報としてご覧ください)

パターンは以下の5つ。
(カッコ内は、マイクロベンチマーク用に記述したメソッド名になります)

  1. StringBufferを使って結合する (bufferJoin)
  2. StringBuilderを使って結合する (builderJoin)
  3. String#joinを使って結合する (joinJoin)
  4. StringJoinerを使って結合する (joinerJoin)
  5. 「+」で文字列を結合する(※いわゆるアンチパターン)(plusJoin)

※2014/03/18 ソースコードは、GitHubからどうぞ。
https://github.com/otomac/StringJoinBenchmark



実行条件:

java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b127)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b69, mixed mode)

JMH実行時のコマンドラインは「-wi 5 -i 10 -f 10」です。
つまり、以下のようになります。
  (A) ウォームアップ実行 5回
  (B) 計測のための実行 10回
  (C) 上記(A)+(B)の繰り返し 10回

計測のための実行は、各パターン100回ずつということになります。

さて、結果は以下のようになりました。

Result : 0.005 ±(99.9%) 0.001 ops/ms
  Statistics: (min, avg, max) = (0.003, 0.005, 0.005), stdev = 0.001
  Confidence interval (99.9%): [0.004, 0.006]


Benchmark                             Mode   Samples         Mean   Mean error    Units
s.j.b.StringJoinStudy.bufferJoin     thrpt       100        5.134        0.033   ops/ms
s.j.b.StringJoinStudy.builderJoin    thrpt       100        5.386        0.023   ops/ms
s.j.b.StringJoinStudy.joinJoin       thrpt       100        5.350        0.019   ops/ms
s.j.b.StringJoinStudy.joinerJoin     thrpt       100        5.357        0.044   ops/ms
s.j.b.StringJoinStudy.plusJoin       thrpt       100        0.005        0.000   ops/ms

この結果から、String#joinを使うパターン(joinJoin)とStringJoinerを使うパターン(joinerJoin)は、StringBuilderを使うパターン(builderJoin)とほぼ同等の速度が出せることがわかります。
StringBufferを使うパターン(bufferJoin)は、想定通り若干遅いくらいの結果になりました。
「+」を使うパターン(plusJoin)も、予想通り論外の性能でしたね(笑

このように、配列やリストのように、既に連結する文字列の要素が揃っている場合はString#joinやStringJoinerを使っても性能に遜色はないことがわかりました。
ぜひ使っていきたいところですね。

2. ファイル読み込み編

これまでのコード

これまでのテキストファイル読み込みは、おおよそ以下のような感じだと思います。
ただ読み込むだけだとつまらないので、XMLファイルの"<" と ">"を丸括弧 "(" と ")" に置換するサンプルを考えてみます。

※2014/03/18追記:うらがみ (id:backpaper0) さんからご指摘いただいて、Pathオブジェクトの生成コードを修正しました。コメントありがとうございます。

List<String> lines = new ArrayList<String>();

Path path = Paths.get("data.xml");
String line;
try (BufferedReader br = Files.newBufferedReader(path)) {
    while ((line = br.readLine()) != null) {
        String replaced = line.replaceAll("<", "(").replaceAll(">", ")");
        lines.add(replaced);
    }
} catch(IOException ex) {
    // 省略
}

whileの条件文に行読み込みの呼び出しがネストしていたりして、若干ややこしい感じがします。
かといって以下のように書くのも間延びする感じがしますね。

List<String> lines = new ArrayList<String>();

Path path = Paths.get("data.xml");
try (BufferedReader br = Files.newBufferedReader(path)) {
    while (true) {
        String line = br.readLine();
        if (line == null) {
            break;
        }
        String replaced = line.replaceAll("<", "(").replaceAll(">", ")");
        lines.add(replaced);
    }
} catch(IOException ex) {
    // 省略
}

さて、Java8ではそんなテキストファイル読み込みも便利にしてくれるAPIが追加されています。
ここでも、2種類見てみましょう。

(1) BufferedReader#lines()

InputStreamやReaderがあらかじめある場合は、こちらの方を使うことになるでしょう。
先ほどのコードは以下のようになります。
(書き換えた結果、結局Pathを作ってBufferedReaderを作っていますが、そのくらいなら良いのかなと)

List<String> lines = new ArrayList<String>();

Path path = Paths.get("data.xml");
try (BufferedReader br = Files.newBufferedReader(path, Charset.forName("UTF-8"))) {
    br.lines()
       .forEach(l -> { strList.add(l.replaceAll("<", "(").replaceAll(">", ")")); });
} catch(IOException ex) {
    // 省略
}

先ほどのコードに比べて、whileループが無いのでスッキリ。

ちなみに、上の例に書いたように、Filesクラスの多くのメソッドでは、Charsetで文字コードを指定することができます。
これも、今まではFileReaderでは文字コードを指定できず、InputStreamReaderを介して指定しなければならなかったのですが、Filesクラスで指定できるようになって、非常に便利になったところですね。
(※追記: UTF-8のような標準的な文字コードは、StandardCharsets.UTF_8でも可です)


読み込んだデータの加工をすることを考えると、ラムダ式やStream APIに慣れておかないといけませんが、単にリストに格納するだけなら以下のようにするのが簡単です(※これはJava7からある書き方です)。

※2014/03/13追記: id:nowokay さんから指摘をいただき、ファイル読み込みを Files#readAllLines() に変更しました。コメントありがとうございます。

Path path = Paths.get("data.xml");
List<String> allLines = Files.readAllLines(path, Charset.forName("UTF-8"));

ただし、メモリを大量に消費する危険性がありますので、大きいファイルを読み込む処理で使ってはいけません。

(2) Files#lines()

ファイルのパスが分かっているだけの場合や、Files#find()を使う場合などは、こちらの方を使うことになるでしょう。

先ほどのコードは以下のようになります。

 List<String> lines = new ArrayList<String>();
 
  Path path = Paths.get("data.xml");
 try (Stream<String> stream = Files.lines(path, Charset.forName("UTF-8"))) {
     stream.forEach(l -> { strList.add(l.replaceAll("<", "(").replaceAll(">", ")")); });
 } catch(IOException ex) {
     // 省略
 }

わざわざファイルからInputStreamやReaderを作らなくてよいので、非常にスッキリしてますね。

…なんとなく予想付いちゃってる方もいらっしゃるかと思いますが、こちらも、内部でFiles.newBufferedReader()を使っているので、ほとんど同じです。



f:id:acro-engineer:20140311021635j:plain:small

ここまで、前半として、文字列操作とファイルからのテキスト読み込みについての追加APIを紹介しました。
なんで、今までこんなメソッドが無かったのか?というくらい普通に使いたくなるものじゃないでしょうか?

ある意味、経験を積んだJavaコーダーさんなら、イディオムとして手が勝手に動くようなコードもあるかと思います。
でも、これからは自分の手じゃなくてコンパイラに仕事をさせて、短くなったコードでもうちょっと違うアイデアを考えていきたいものですね。

次回(後編)は、Map操作とConcurrent系のちょっとしたAPIの追加について、です。

では。


Acroquest Technologyでは、キャリア採用を行っています。


  • 日頃勉強している成果を、Hadoop、Storm、NoSQL、HTML5/CSS3/JavaScriptといった最新の技術を使ったプロジェクトで発揮したい。
  • 社会貢献性の高いプロジェクトに提案からリリースまで携わりたい。
  • 書籍・雑誌等の執筆や対外的な勉強会の開催を通した技術の発信や、社内勉強会での技術情報共有により、技術的に成長したい。
  • OSSの開発に携わりたい。

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
 キャリア採用ページ