こんにちは!
アキバです。
...T3ブログは初登場かもしれません。ハジメマシテ。
以後お見知りおきを。
いよいよ、2014年3月、Java8が正式公開されますね。
なんと言っても、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点くらいですね。
- 区切り文字を自分で追加しなくなった
- 接頭辞と接尾辞を指定できる(CSV全体をカッコで囲むみたいなことができます)
- 要素を追加していない時の文字列を指定できる
れっつ、ベンチマーク!
ところで、いつのバージョンでも「文字列結合の性能ネタ」はありますが、上記の新APIは、パフォーマンス的にはどうなんでしょうか?
ここでは、以前に @cero-t も紹介していた マイクロベンチマークツール「JMH」を使って、10000個の5バイト文字列("abcde")をカンマ区切りで連結する処理の実行時間を測定してみました。
(※正式リリース前のJava8で実施しているため、参考情報としてご覧ください)
パターンは以下の5つ。
(カッコ内は、マイクロベンチマーク用に記述したメソッド名になります)
- StringBufferを使って結合する (bufferJoin)
- StringBuilderを使って結合する (builderJoin)
- String#joinを使って結合する (joinJoin)
- StringJoinerを使って結合する (joinerJoin)
- 「+」で文字列を結合する(※いわゆるアンチパターン)(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()を使っているので、ほとんど同じです。
ここまで、前半として、文字列操作とファイルからのテキスト読み込みについての追加APIを紹介しました。
なんで、今までこんなメソッドが無かったのか?というくらい普通に使いたくなるものじゃないでしょうか?
ある意味、経験を積んだJavaコーダーさんなら、イディオムとして手が勝手に動くようなコードもあるかと思います。
でも、これからは自分の手じゃなくてコンパイラに仕事をさせて、短くなったコードでもうちょっと違うアイデアを考えていきたいものですね。
次回(後編)は、Map操作とConcurrent系のちょっとしたAPIの追加について、です。
では。
Acroquest Technologyでは、キャリア採用を行っています。
- 日頃勉強している成果を、Hadoop、Storm、NoSQL、HTML5/CSS3/JavaScriptといった最新の技術を使ったプロジェクトで発揮したい。
- 社会貢献性の高いプロジェクトに提案からリリースまで携わりたい。
- 書籍・雑誌等の執筆や対外的な勉強会の開催を通した技術の発信や、社内勉強会での技術情報共有により、技術的に成長したい。
- OSSの開発に携わりたい。
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
キャリア採用ページ