GoogleのJavaユーティリティライブラリであるGuavaは、今年9月28日にリリース10.0.0を迎えました。新たなAPIも追加され、更なる進化を遂げています。(執筆時点での最新リリースは10.0.1)
今回は、まだベータ版ながら新しく追加された興味深いAPIを4つ紹介します。
他のAPIについても、JavaDocなどで@Betaアノテーションがついているものはベータ版ですので、今後のリリースで変更される可能性があります。
プログラムの実行時間を計測したいとき、よく使われるコードがあります。
long t1 = System.currentTimeMillis(); someExpensiveCompute(); long t2 = System.currentTimeMillis(); System.out.printf("time: %sms%n", t2 - t1);
このt1とかt2とかいう、よくわからない変数は何でしょう? それに時間を表示するために面倒な引き算をしていますね。Stopwatchを使うとこう書くことができます。
Stopwatch stopwatch = new Stopwatch().start(); someExpensiveCompute(); stopwatch.stop(); System.out.printf("time: %s%n", stopwatch);
キャッシュにまつわる問題は、いつも開発者を悩ませます。キャッシュエントリの数・エントリを削除するタイミング・そしてメモリ・・・。
そうしたキャッシュの実装を手軽にしてくれるのがcom.google.common.cacheパッケージです。最も単純な使用法は以下のようになります。せっかくなので、実行時間も先ほどのStopwatchを使って計測してみましょう。
Cache cache = CacheBuilder.newBuilder() .build(new CacheLoader() { public Object load(String key) { return someExpensiveCompute(key); } }); try { Stopwatch stopwatch = new Stopwatch().start(); System.out.printf("key0: %s%n", cache.get("key0")); System.out.printf("time: %s%n", stopwatch); stopwatch.reset().start(); System.out.printf("key0: %s%n", cache.get("key0")); System.out.printf("time: %s%n", stopwatch); } catch (ExecutionException e) { e.printStackTrace(); }
このコードを実行すると、例えば以下のような結果になります。
key0: 2305843005992468481 time: 1.794 s key0: 2305843005992468481 time: 201.0 μs
1度目の結果がキャッシュされて、2度目の実行コストが節約されているのがわかります。
このCacheBuilderはいくつかのオプションを備えています。少し大きなオブジェクトをキャッシュするために、エントリの最大数を10にし、かつメモリ不足を予見して値がGCの対象になるようにしてみましょう。
Cache cache = CacheBuilder.newBuilder() .maximumSize(10) .softValues() .build(new CacheLoader() { public Object load(String key) { return someLargeObject(key); } }); try { // 大きなオブジェクトを4つキャッシュしようとする cache.get("key0"); cache.get("key1"); cache.get("key2"); cache.get("key3"); Stopwatch stopwatch = new Stopwatch().start(); cache.get("key3"); System.out.println("get key3"); System.out.printf("time: %s%n", stopwatch); stopwatch.reset().start(); cache.get("key0"); System.out.println("get key0"); System.out.printf("time: %s%n", stopwatch); } catch (ExecutionException e) { e.printStackTrace(); }
このコードを実行すると、私のマシンでは以下のような結果になりました。
get key3 time: 29.79 ms get key0 time: 4.229 s
エントリの最大数には余裕があるはずですが、値がGCの対象になったため、key0は新しく計算し直されたのです。
もちろん充分なメモリを与えて実行すれば、結果は以下のように変わります。
get key3 time: 23.54 ms get key0 time: 244.0 μs
Nullオブジェクトの実装です。・・・と、これだけでは説明になっていないので、コードを見ていただきましょう。まずはOptionalを使用しないコードです。
guavatest.TestOptional1
package guavatest;
import java.util.HashMap;
import java.util.Map;
public class TestOptional1 {
private static final Map<String, String> map
= new HashMap<String, String>();
static {
map.put("Taro", "Sales");
map.put("Jiro", null); // 次郎は無職
map.put("Saburo", "Engineer");
}
public static void main(String[] args) {
String job1 = getJob("Jiro");
System.out.printf("What's job of Jiro ?: %s%n", job1);
String job2 = getJob("Shiro");
System.out.printf("What's job of Shiro ?: %s%n", job2);
}
private static String getJob(String name) {
return map.get(name);
}
}
このコードを実行すると以下の結果が得られます。
What's job of Jiro ?: null What's job of Shiro ?: null
このコードの問題点は、「値が無いことが分かっている」場合と「そもそも分からない」場合との区別が明確にできないところにあります。
Optionalを使用すると以下のように書くことができます。
guavatest.TestOptional2
package guavatest;
import java.util.HashMap;
import java.util.Map;
import com.google.common.base.Optional;
public class TestOptional2 {
private static final Map<String, Optional<String>> map
= new HashMap<String, Optional<String>>();
static {
map.put("Taro", Optional.of("Sales"));
map.put("Jiro", Optional.<String>absent()); // 次郎は無職
map.put("Saburo", Optional.of("Engineer"));
}
public static void main(String[] args) {
Optional<String> job1 = getJob("Jiro");
System.out.printf("What's job of Jiro ?: %s%n",
job1 == null? "Unknown":
job1.isPresent()? job1: "Nothing");
Optional<String> job2 = getJob("Shiro");
System.out.printf("What's job of Shiro ?: %s%n",
job2 == null? "Unknown":
job2.isPresent()? job2: "Nothing");
}
private static Optional<String> getJob(String name) {
return map.get(name);
}
}
このコードを実行すると以下の結果が得られます。
What's job of Jiro ?: Nothing What's job of Shiro ?: Unknown
区間を表すクラスです。端点を含む/含まないなどをすっきりと表すことができます。以下、コードとその実行結果です。※端点を含む区間を閉区間、含まない区間を開区間といいます。
// 閉区間 [0,100] Range<Integer> range1 = Ranges.closed(0, 100); System.out.printf("contains -1: %s%n", range1.contains(-1)); System.out.printf("contains 0: %s%n", range1.contains(0)); System.out.printf("contains 100: %s%n", range1.contains(100)); System.out.printf("contains 101: %s%n", range1.contains(101)); System.out.println(); // 開区間 (0,100) Range<Integer> range2 = Ranges.open(0, 100); System.out.printf("contains 0: %s%n", range2.contains(0)); System.out.printf("contains 1: %s%n", range2.contains(1)); System.out.printf("contains 99: %s%n", range2.contains(99)); System.out.printf("contains 100: %s%n", range2.contains(100));
contains -1: false contains 0: true contains 100: true contains 101: false contains 0: false contains 1: true contains 99: true contains 100: false
Guava最初のバージョンであるr01がリリースされたのが2009年9月15日。当時は50程度のクラスしか持たない小さなライブラリでした。それからGoogle Collectionsなどの既存ライブラリも取り込みながら成長し、2年あまりで250を超えるクラスを持つまでになりました。
ユーティリティライブラリとしては既に必要充分なAPIを備えた感のあるGuavaですが、今後もescape・mathなどのパッケージの追加が予定されているようです。
洗練されたライブラリは、つまらない繰り返し作業を肩代わりして、開発者の創造的な時間を増やしてくれます。Guavaの今後に期待したいですね。