« Back to Home

Blog

What Are Little Boys Made Of?

2011-11-20 01:22:28 +0900

What are little boys made of?
What are little boys made of?
Frogs and snails
And puppy-dogs' tails,
That's what little boys are made of.

What are little girls made of?
What are little girls made of?
Sugar and spice
And everything nice,
That's what little girls are made of.

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

2011-11-14 01:16:43 +0900

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の今後に期待したいですね。

JAX-RSのリファレンス実装、Jerseyの使い方(2)

2011-11-07 00:31:46 +0900

前回は基本となるアノテーションを軸に、きわめて単純なRESTサービスを構築する手順を紹介しました。今回はやはりRESTといえばXML/JSONということで、XMLやJSONを返すサービスを構築しましょう。

(前回と言っても、ポストしたのがだいたい1年前の日付だということにたった今気づきました。)

XMLを返す

次のようなリソースクラスを作成します。

jerseytest.resources.LocalDocumentResource

package jerseytest.resources;

import java.io.IOException;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;

@Path("/indianTeas")
public class LocalDocumentResource {
	@GET
	public Document getDocument() {
		try {
			File f = new File("/path/to/indianTeas.xml");
			Document doc = DocumentBuilderFactory.newInstance()
					.newDocumentBuilder().parse(f);
			return doc;
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		return null;
	}
}

少しコード量が増えたもののやっていることは単純で、ローカルにあるXMLファイルをDocumentオブジェクトにして返しているだけです。

ではサービスを参照してみましょう。

$ curl "http://localhost:8080/jerseytest/rest/indianTeas"

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<teas>
	<tea>Assam</tea>
	<tea>Darjeeling</tea>
	<tea>Nilgiri</tea>
</teas>

ちゃんとインドの紅茶リストが返ってきましたね。

リソースクラスのメソッドがデフォルトで返すことのできる型は、Document以外にも

  • javax.xml.transform.Source
  • byte[]
  • java.io.InputStream
  • java.io.Reader
  • java.io.File
  • その他のJAXBオブジェクト
などがあります。中でもJAXBオブジェクトを扱えることで、サービスの提供と利用がとても手軽になるのですが、その解説は次の機会に譲ります。

JSONを返す

XMLができたら、次はJSONを返してみたくなるのが人情というものです。

JSONの文字列を自前で作って返すことも勿論出来るのですが、ここはjerseyが用意してくれている機能に頼りましょう。依存ライブラリが多いので(10個以上)ここはmavenを使用することをお勧めします。pom.xmlに以下の記述を追加します。

<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.4</version>
</dependency>

次に、MapをJSONで返すリソースクラスを作成してみます。

jerseytest.resources.JsonResource

package jerseytest.resources;

import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.GET;
import javax.ws.rs.Path;

import org.codehaus.jettison.json.JSONObject;

@Path("/json")
public class JsonResource {
	@GET
	public JSONObject get() {
		Map m = new HashMap();
		m.put("name", "George");
		m.put("age", "35");
		m.put("gender", "male");
		return new JSONObject(m);
	}
}

HashMapをJSONObjectでラップして返しているだけですね。ではサービスを参照してみましょう。

$ curl "http://localhost:8080/jerseytest/rest/json"

{"age":"35","name":"George","gender":"male"}

確かにJSON形式のMapになって返ってきました。
Collectionも同様にJSONArrayでラップして返すことができます。

MediaType(MIMEタイプ)の指定

HTTPでは、”リクエスト側が要求するリソースの種類”と”レスポンス側が実際に返すリソースの種類”を表すためにMIMEタイプの指定が可能です。

要求されたMIMEタイプに基づいてレスポンスの種類を変えるリソースクラスを作成してみましょう。

jerseytest.resources.GeorgeResource

package jerseytest.resources;

import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.codehaus.jettison.json.JSONObject;

@Path("/george")
public class GeorgeResource {
	@GET
	@Produces(MediaType.TEXT_PLAIN)
	public String name() {
		return "I'm George";
	}

	@GET
	@Produces(MediaType.APPLICATION_JSON)
	public JSONObject doc() {
		Map m = new HashMap();
		m.put("name", "George");
		m.put("age", "35");
		m.put("gender", "male");
		return new JSONObject(m);
	}
}

新しいアノテーションが出てきました。

@Produces
MIMEタイプでリソースの種類を示す。

ではそれぞれのMIMEタイプを指定して、サービスを参照してみましょう。
まずは"text/plain"からです。

$ curl -H "Accept: text/plain" "http://localhost:8080/jerseytest/rest/george"

I'm George.

次に"application/json"です。

$ curl -H "Accept: application/json" "http://localhost:8080/jerseytest/rest/george"

{"age":"35","name":"George","gender":"male"}

うまく行きました。親切なgeorgeサービスは、プレーンテキストを要求した場合は名前だけ、JSONを要求した場合は名前以外にも年齢と性別を教えてくれました。

次回へ

今回はXMLやJSONを返すサービスを構築する手順を紹介しました。これでかなりRESTっぽくなってきましたね。

次回はJAXBオブジェクトの扱いと、余裕があればクライアント(サービスを利用する側)の実装手順を紹介しようと思います。

やっとわかった、java.util.logging

2011-11-06 03:00:00 +0900

こんばんは。先日ブログの設定をいじっていたら、全てのページ(管理画面も!)でPHPエラーが表示されてかなり気落ちしました。PHPを書くのは久しぶりだったので、どこか書き間違えたんでしょう・・・。

さて、今日はロギングの話をします。

ロギングと言えばlog4jですが、JavaSEにもロギングを扱うための標準APIがあります(1.4から)。それがjava.util.loggingです。標準APIであるにも関わらず、なぜか情報が少ないんですね。簡単なプログラムを書こうとして、ついでだから標準API使ってみるか、と思い立つも、結局設定の仕方がわからずにlog4jに戻ったことが何度かありました。

まず使ってみる

まずは一番簡単な使い方から。

Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
logger.info("Loggerテストです。info");
logger.severe("Loggerテストです。severe");

これだけです。このコードを実行すると、私のマシンでは以下の出力が得られます。

2011/11/05 23:34:29 Sample1 main
情報: Loggerテストです。info
2011/11/05 23:34:29 Sample1 main
致命的: Loggerテストです。severe

簡単ですね。

標準出力に出力する

実際実行してみるとわかる通り、先ほどのコードでは、ログはエラー出力に出力されます。標準出力に出力するには、ハンドラを定義します。

Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
logger.addHandler(new StreamHandler(){
    { setOutputStream(System.out); }
});
logger.setUseParentHandlers(false);

logger.info("Loggerテスト2です。");

parentHandlerについては後で説明することにして、このコードを実行すると以下の出力が(標準出力に)得られます。

2011/11/05 23:47:55 Sample2 main
情報: Loggerテスト2です。

ログレベルの変更

では更に出力されるログのレベルを変更してみましょう。「どうせsetLogLevel()だろう?」ほぼ正解です! 以下、コードとその実行結果です。

Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
logger.addHandler(new StreamHandler(){
  {
    setOutputStream(System.out);
    setLevel(Level.ALL);
  }
});
logger.setUseParentHandlers(false);
logger.setLevel(Level.ALL);

logger.fine("Loggerテスト3です。fine");
logger.info("Loggerテスト3です。info");
logger.severe("Loggerテスト3です。severe");
2011/11/06 0:10:22 Sample3 main
詳細レベル (低): Loggerテスト3です。fine
2011/11/06 0:10:22 Sample3 main
情報: Loggerテスト3です。info
2011/11/06 0:10:22 Sample3 main
致命的: Loggerテスト3です。severe

loggerとhandler、両方にレベルを設定していることに注意してください。

設定ファイルを使用する

loggerインスタンスを作成する度にコード内で設定を書くのはいかにも面倒です。log4jのように設定ファイルを使って設定したいものです。

まずは、先ほど横着してインナークラスにしていた”標準出力へログ出力するハンドラ”を通常のクラスとして定義しておきます。

sample.StdConsoleHandler

package sample;
import java.util.logging.StreamHandler;

public class StdConsoleHandler extends StreamHandler {
  {
    setOutputStream(System.out);
  }
}

次にログ設定ファイルを作成します。

customLogging.properties

handlers= sample.StdConsoleHandler
.level= ALL
sample.StdConsoleHandler.level = ALL

最後にログ出力するためのコードです。

try {
  LogManager.getLogManager().readConfiguration(
        Sample4.class.getResourceAsStream("customLogging.properties"));
} catch (SecurityException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);

logger.fine("Loggerテスト4です。fine");
logger.info("Loggerテスト4です。info");
logger.severe("Loggerテスト4です。severe");

ログ設定ファイルと実行コードを同じディレクトリに置いて実行してください。すると以下の出力が得られます。

2011/11/06 0:47:32 Sample4 main
詳細レベル (低): Loggerテスト4です。fine
2011/11/06 0:47:32 Sample4 main
情報: Loggerテスト4です。info
2011/11/06 0:47:32 Sample4 main
致命的: Loggerテスト4です。severe

ロガーツリー

getLogger()をコールするときにロガーの名前を指定します。ここまでは簡易的に汎用ロガーを使用してきましたが、実際の開発では幾つものモジュールを開発するので、モジュール毎にログ設定をしたいことも多いでしょう。

ロガーを使い分けるには、以下のようなコードにします。ロガーを3つ使ってみました。

try {
  LogManager.getLogManager().readConfiguration(
        Sample5.class.getResourceAsStream("customLogging.properties"));
} catch (SecurityException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

Logger loggerX = Logger.getLogger("xxx");
Logger loggerY = Logger.getLogger("xxx.yyy");
Logger loggerZ = Logger.getLogger("xxx.zzz");

loggerX.fine("LoggerXテスト5です。fine");
loggerX.info("LoggerXテスト5です。info");
loggerY.fine("LoggerYテスト5です。fine");
loggerY.info("LoggerYテスト5です。info");
loggerZ.fine("LoggerZテスト5です。fine");
loggerZ.info("LoggerZテスト5です。info");

それから、先ほどのログ設定ファイルを以下のように書き換えましょう。

# custom handler
sample.StdConsoleHandler.level = ALL

# custom logger
xxx.handlers = sample.StdConsoleHandler
xxx.level = INFO
xxx.useParentHandlers = false

xxx.zzz.level = ALL

コードを実行すると、以下の出力が得られます。

2011/11/06 1:32:15 Sample5 main
情報: LoggerXテスト5です。info
2011/11/06 1:32:15 Sample5 main
情報: LoggerYテスト5です。info
2011/11/06 1:32:15 Sample5 main
詳細レベル (低): LoggerZテスト5です。fine
2011/11/06 1:32:15 Sample5 main
情報: LoggerZテスト5です。info

お気づきでしょうか? loggerZではfineのログまで出力されていますが、loggerX、loggerYではinfoまでしか出力されていません。

これがロガーツリーです。Logger.getLogger("xxx")で取得したロガーは、Logger.getLogger("xxx.yyy")・Logger.getLogger("xxx.zzz")で取得したロガーの親ロガーになります。

ここにはポイントが2つあります。

  • ログレベルを設定していないロガーのログレベルは、親ロガーのログレベルと同じになる。
  • ロガーはデフォルトで、自身のハンドラと親ロガーのハンドラへログメッセージを発行する。

デフォルトのログ設定ファイルとルートロガー

実はJREにはデフォルトのログ設定ファイルがあります。通常$JAVA_HOME/lib/logging.propertiesにありますので開いてみてください。".level"・"handlers"・"ConsoleHandler"・"FileHandler"などの設定がされているのがわかると思います。

先ほど、"xxx"のロガーは"xxx.yyy"のロガーの親ロガーになると言いましたが、実際にはさらに全ての(明示的な親ロガーを持たない)ロガーの親としてルートロガーなるものが存在しています。そしてルートロガーのログレベルは設定ファイルの".level"、ハンドラは設定ファイルの"handlers"で決定されます。

getLogger(Logger.GLOBAL_LOGGER_NAME)で取得できるロガーの親ロガーもルートロガーです。最初の例でエラー出力に出力されたのはルートロガーのハンドラがConsoleHandlerだったからなんですね。ただConsoleHandlerがなぜエラー出力を使う仕様になっているのかは謎です(笑)。少し不便ですよね。

また、2番目以降の例でsetUseParentHandlers(false)を使用しているのは、ルートロガーのハンドラ(ConsoleHandler)へログメッセージを発行しないようにするためでした。

標準ロギングAPI java.util.logging

ここまでjava.util.loggingを見てきましたが、いかがでしょうか。log4jなどに慣れていると戸惑う部分もありますが、標準でついている機能を「わからないから使わない」のではもったいないと思います。違いを理解した上で使い分けたいものですね。

私の参考書の選び方

2011-09-18 00:01:03 +0900

別に喧伝するようなことではないのですが、そういえばこんなことを気にしているなあ、と思ったので書いてみます。

参考書を買おうとして書店に行くと、思っている事柄についての参考書が大抵何冊かはあるものです。そういうとき、私はだいたい次のようなポイントを見ています。

  1. 色は2色まで。
  2. 脚注・傍注欄がないこと。注釈専用ページなどもってのほか。
  3. 表紙がシンプル。

これは好みで言っているわけではなくて、ちゃんと理由があります。

なぜ参考書を読むのか?

そこに参考書があるから、ではありません。

何か知りたいと思っていることがあるからです。つまりその参考書を読んで知りたい内容を理解できて初めて、その参考書を読む価値があるというものです。

参考書として重要なのは、内容が巧く解説されているかでしょう。

色は2色まで

これは、著者の文章表現力を示します。テキストは2色、図表には3色使ってあってもいいと思います。

たくさんの色を使わなければ解説できないということは、端的に言って文章が下手だと考えて差し支えありません。

脚注・傍注欄がないこと

これは、著者の文章の流れを作る力を示します。

一連の内容を滞りなく理解させるためには、文章の流れを途切れさせないことが重要です。脚注・傍注はその文章の流れを断ち切るもの、まして注釈ページなどその最たるものです。

必要な解説は全て本文中で為されるべきなのです。

表紙がシンプル

これは、著者の考え方を示します。

装丁には著者の趣向が象徴的に表れるものです。そしてある事柄について焦点を当て、ブレの無いまとまった解説をするためには、整理された知識に拠るシンプルな説明が効果的と言えます。

 

・・・というところです。ついでに前書きと目次を眺めてから、違和感がなければ買うことが多いです。

これらを全て満たす参考書であれば、だいたいハズレたことはありません。