Taste of Tech Topics

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

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

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

本日3/18、ついに、Java8が正式リリースされますね!
もうダウンロードされましたか?ってまだですかね?私はまだです(だって公開前にエントリ書いてるんだもんね)

2014/03/19追記:Oracleのページが更新されました!→こちら


さて、前回に続いて、Java8で追加された地味で便利なAPIを紹介していきます。

今回は、みんな大好きMapConcurrent、あとちょびっとComparatorです。

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

3. Map操作編

(1) Map#getOrDefault()

これまでは、Mapから値を取得してnullだったらデフォルト値を使用する、みたいなコードを以下のように書いていたと思います。

Map<String, String> map;    // 何らかのMap

String value = map.get("key");
if (value == null) {
    value = defaultValue;
}

Map#getOrDefault()を使うと、この処理が1行になります。

String value = map.getOrDefault("key", defaultValue);

それだけです。ちょっとしたところからスッキリしますね。

(2) Map#replace()

Map#replace()を使うと、該当するキーに対する値を入れ替えられます。

キーが存在することが必須条件ですが、値の指定には2種類あります。

1つ目は、キーさえあれば、強制的に値を書き換えるもの。

Map<String, String> map;    // 何らかのMap
map.replace("key1", "newValue");

この場合は、key1が存在すれば、値はかならずnewValueになります。
key1が存在しないと、何も起こりません。

2つ目は、キーがあって値が一致したものだけ書き換えるもの。

Map<String, String> map;    // 何らかのMap
map.replace("key1", "oldValue", "newValue");

この場合、key1がoldValueである場合に限り、newValueになります。
それ以外の場合は、何も起こりません。

いずれの呼び出しも、戻り値として置換前の値が得られます。
置換しなかった場合は、nullとなるので、書き換わったかどうかを調べることもできます。

(3) Map#computeIfPresent()

Map#computeIfPresent()は、キーが存在している(かつnullではない)時に、値を加工するためのメソッドです。

...なんとなく便利そうですが、いまいちイメージしづらいですね。
長々と説明するよりも、実際のコードを見てみましょう。

例として、Mapのキーを探索して、存在する場合に先頭に「★」を追加する処理を書いてみます。


今までは、以下のように書いていたと思います。

Map<String, String> map;   // 何らかのMap

// キーが存在する名前だけ、先頭に「★」を追加する
String[] keys = new String[] { "a", "c", "d" };
for (String key : keys) {
    if (map.get(key) != null) {
        String oldValue = map.get(key);
        String newValue = '★' + oldValue;
        map.put(key, newValue);
    }
}


Java8では、以下のように書けます。

Map<String, String> map;   // 何らかのMap

// キーが存在する名前だけ、先頭に「★」を追加する
String[] keys = new String[] { "a", "c", "d" };
for (String key : keys) {
    map.computeIfPresent(key, (k, s) -> '★' + s);
}

イディオム的にコードを書くよりも、computeIfPresentという名前で何をしたいのか/何をしているのかがハッキリして良いと思います。
しかも、コードはスッキリ。

ラムダ式になっているところは、BiFunctionというJava8で追加された関数インタフェースです。
ちなみに、Map#computeIfPresent() は default メソッドになっていて、以下のコードであるとAPIドキュメントに書かれています。

if (map.get(key) != null) {
    V oldValue = map.get(key);
    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue != null)
        map.put(key, newValue);
    else
        map.remove(key);
}

最初に書いたコードとほとんど同じですね。
また、これを見ると、BiFunctionに渡される2つの引数は、1つ目がキー、2つ目が(処理前の)値であることがわかります。

4. Concurrent編

(1) LongAdder

Javadocを見ると、「初期値をゼロとした、1つ以上の値の合計を扱う」とあります。
要は、値を合計するためのクラスなのですが、java.util.concurrent.atomicパッケージに属しているだけあって、マルチスレッドからのアクセスに対応しています。

シングルスレッドで使っているとつまらないのですが、こんな感じです。

long[] longArray = { 1, 2, 3, 4, 5 };

LongAdder adder = new LongAdder();
LongAdder count = new LongAdder();
for (long longValue : longArray) {
    adder.add(longValue);
    count.increment();
}

System.out.println("elements count=" + count.sum() + ", sum=" + adder.sum());
// → elements count=5, sum=15 と表示される

他にも、sumThenReset() があるので、一定期間毎の合計を出したりできるかもしれません。

似たような用途に AtomicLong も使えるのですが、APIドキュメントを見ると
「スレッドの競合が高い状況下では LongAdderの方がメモリを消費する代わりに、高速に動作する」
という主旨のコメントが書かれています。

Java8のソースを見る限りでは、合計値の算出タイミングの違いが影響しているようですね。

AtomicLongは、値を追加(addAndGet)する毎に計算を行っていますが、LongAdderの方はCellという内部用のオブジェクトを配列で保持するようになっていて、合計を参照する(sumなど)のタイミングで初めて合計値を計算する仕組みになっているようです。

ということは、複数スレッドが競合するタイミングで計算を含んだ処理でロックの取り合いにはならないということでしょうか。


どのような条件で、どれだけ高速なのでしょうか?
今回もベンチマークしようと思っていたのですが、海外のエントリでベンチマークをとった結果が出ていました。
→日本語訳の引用あり:Java 8ニュース:新しいアトミックナンバーを含むRC版を公開、モジュール化は外れる
 (「新しいアトミックナンバーの実装」という項を見てください)
→原文:Java 8 Performance Improvements: LongAdder vs AtomicLong - Palomino Labs Blog

これによると、シングルスレッドではAtomicLongの方が速いけど、マルチスレッドで競合アクセスさせた場合はLongAdderの方が性能が良いとなっています。純粋なカウントアップ処理を行っているとのことですが、特性としてはドキュメントに書かれている通りになりました。

(2) LongAccumulator

Javadocを見ると、「1つ以上の値の集合を提供された関数で更新する」とあります。
なんとなくLongAdderと同じようなクラスですが、コンストラクタにはLongBinaryOperatorを
指定することになっており、ちょっと凝った動作ができるようです。

簡単な例として、値の2乗を合計するようにしてみましょう。

long[] longArray = { 1, 2, 3, 4, 5 };

LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y * y, 0L);
for (long longValue : longArray) {
    accumulator.accumulate(longValue);
}

System.out.println("square sum=" + accumulator.get());
// → square sum=55 と表示される
//    (1 + 4 + 9 + 16 + 25 = 55)

こちらも、マルチスレッドで処理する場合には利用を検討してみましょう。

5. Comparator編

(1) naturalOrder() / reverseOrder()

最後に、皆さんもよく使っているであろう Comparator で見つけたメソッドです。

唐突ですが、今までJava8の勉強をしてきて、たくさんラムダ式を見たりしてきましたよね。
なので、「文字列を自然順序付けの昇順/降順のソートをする」と聞くと、つい、以下のように書きたくなるかもしれません。

// 昇順にソートする
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));

// 降順にソートする
Arrays.sort(array, (s1, s2) -> s2.compareTo(s1));

このくらいの処理ならば、ラムダ式でも十分にわかりやすいとは思いますが、Comparator#naturalOrder() と Comparator#reverseOrder() を使うとよりシンプルに記述できます。

// 昇順にソートする
Arrays.sort(array, Comparator.naturalOrder());

// 降順にソートする
Arrays.sort(array, Comparator.reverseOrder());

メソッドの名前からソート順を理解しやすく、さらにコードもスッキリしましたね。


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

いかがでしたでしょうか。

他にも追加されたAPIはまだまだたくさんあります。

前回書いたベンチマークのように、構文は便利になったが、果たして実用に耐えられるのか?ということは常に注意して使わなければなりませんし、その為に中身の動作をよく理解しておく必要があります。

それでも、知らなければ使おうとも思わないという面もあると思います。

ぜひ、皆さん自身でも試してみて、開発効率を上げていきましょう!


ではでは~

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


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

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