« Back to Home

進化するJavaライブラリ、Guava

GoogleのJavaユーティリティライブラリであるGuavaは、今年9月28日にリリース10.0.0を迎えました。新たなAPIも追加され、更なる進化を遂げています。(執筆時点での最新リリースは10.0.1)

今回は、まだベータ版ながら新しく追加された興味深いAPIを4つ紹介します。
他のAPIについても、JavaDocなどで@Betaアノテーションがついているものはベータ版ですので、今後のリリースで変更される可能性があります。

Stopwatch

プログラムの実行時間を計測したいとき、よく使われるコードがあります。

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);

Cache

キャッシュにまつわる問題は、いつも開発者を悩ませます。キャッシュエントリの数・エントリを削除するタイミング・そしてメモリ・・・。

そうしたキャッシュの実装を手軽にしてくれるのが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

Optional

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

Range

区間を表すクラスです。端点を含む/含まないなどをすっきりと表すことができます。以下、コードとその実行結果です。※端点を含む区間を閉区間、含まない区間を開区間といいます。

// 閉区間 [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

Guava最初のバージョンであるr01がリリースされたのが2009年9月15日。当時は50程度のクラスしか持たない小さなライブラリでした。それからGoogle Collectionsなどの既存ライブラリも取り込みながら成長し、2年あまりで250を超えるクラスを持つまでになりました。

ユーティリティライブラリとしては既に必要充分なAPIを備えた感のあるGuavaですが、今後もescape・mathなどのパッケージの追加が予定されているようです。

洗練されたライブラリは、つまらない繰り返し作業を肩代わりして、開発者の創造的な時間を増やしてくれます。Guavaの今後に期待したいですね。