ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

ISO 8601 DateFormat 1.0.0 (Java 向けライブラリ) をリリースしました

2016 年 5 月 3 日に ISO 8601 DateFormat の 1.0.0 をリリースしました。 ISO 8601 形式 (もしくは RFC 3339W3C-DTF 形式) の日付時刻文字列のパースとフォーマットのための DateFormat のサブクラスを提供するライブラリです。

Bintray の JCenter リポジトリで公開しています。

現在は、時刻オフセット付きの拡張形式の日付時刻文字列のみをサポートしています。

  • 2016-01-01T00:30:21Z
  • 2016-01-01T09:30:21+09:00

動機

Java では、ISO 8601 形式の日付時刻文字列をパースする方法がいろいろあります。 例えば、SimpleDateFormat で “yyyy-MM-dd'T'HH:mm:ssX” というフォーマットを使う (Java SE 7 以降) とか、Date and Time API (JSR 310; Java SE 8 以降) を使うとか、Joda-Time ライブラリを使うとか、Apache Commons Lang ライブラリを使うなどです。

しかし、Java SE 6 環境や Android プラットフォームでは、大きなライブラリを導入することなく ISO 8601 形式の文字列をパースすることが簡単ではありませんでした。 そのため、このようなライブラリを公開しました。

使い方

Gradle を使っている場合、以下のようにリポジトリと依存を追加します。

repositories {
    jcenter()
}
dependencies {
    compile 'info.vividcode:date-format-iso8601:1.0.0'
}

あとは、以下のように使うだけです。

import info.vividcode.time.iso8601.Iso8601ExtendedOffsetDateTimeFormat;

DateFormat f = new Iso8601ExtendedOffsetDateTimeFormat();
Date d1 = f.parse("1970-01-01T00:00:00Z");
Date d2 = f.parse("1970-01-01T09:00:00+09:00");

最新の情報はリポジトリの README ファイルを見てください。

ISO 8601 関連の情報

小さなライブラリが欲しいのでなければ、Joda-Time や Apache Commons Lang、JSR 310 のバックポートライブラリなどを使うのが良いかもしれません。

Android の Java で時刻を扱う (Date、Calendar、DateFormat クラス)

Java エンジニアの皆様は Java SE 8 で導入された Date-Time パッケージ (Date and Time API; JSR 310) を便利に使っていることと思います。 残念ながら Android プラットフォームにはそれらの API がありませんので、Android アプリ開発時に時刻を扱う場合は、古くからある API を使用することになります。

この記事では、Android アプリ開発時にお世話になる Date クラス、Calendar クラス、DateFormat クラスについて、それぞれの役割や使い方、気を付けるべきことをまとめます。

追記

(2020-06-03) Android での Date-Time パッケージ

AndroidAPI Level 26 からは java.time パッケージが使えるようになっています。

Android 8.0 (API level 26) adds support for several additional OpenJDK Java APIs:

  • java.time from OpenJDK 8.
  • java.nio.file and java.lang.invoke from OpenJDK 7.
Android 8.0 Features and APIs  |  Android Developers

また、Android Gradle Plugin 4 からは、Java 8+ API desugaring support により、API Level 26 未満のプラットフォームでも java.time パッケージを使用するアプリをビルドできるようになったようです。

If you're building your app using Android Gradle plugin 4.0.0 or higher, the plugin extends support for using a number of Java 8 language APIs without requiring a minimum API level for your app.
(略)
The following set of APIs are supported when building your app using Android Gradle plugin 4.0.0 or higher:

  • (略)
  • A subset of java.time
Use Java 8 language features and APIs  |  Android Developers

(2016-02-18) JSR 310 の Android バックポート

JSR 310 の Android バックポートライブラリの存在を知りました。 Android ではこれを使うのがいいかもしれないですね。

ところで Time クラスは?

Android APIs には Time クラスってやつが含まれています。 API level 22 で非推奨 (deprecated) になったのでそっとしておきましょう。 (これまでありがとうございました。)

気を付けるべきことまとめ

記事が長いので、気を付けるべきことを最初に書いておきます。

  • Date オブジェクト自体はタイムゾーンを持ちません。
    • Calendar を使って指定の Date の年月日や時、分、秒などを取得するとき、あるいはそれらの値を設定したり計算したりするときは、Calendar に適切なタイムゾーンを設定しましょう。 ロケールも必要に応じて設定しましょう。
    • DateFormat を使って Web API などに投げるための時刻の文字列を生成するときは、DateFormat に適切なタイムゾーンを設定しましょう。 ロケールLocale.US が良さそうです。 *1
  • DateFormat を使って Web API などから取得した時刻の文字列を解析する際は、必要であれば適切なタイムゾーンを設定しましょう。 (解析対象の文字列にタイムゾーンが含まれているのであれば必要ない。) ロケールLocale.US が良さそうです。
  • Android *2SimpleDateFormat は、ISO 8601 や RFC 822 で定義されていて実際にしばしば使われる 「Z」 というタイムゾーン表記を解釈できない (Java SE 7 では X というフォーマット文字が導入されて解釈できるようになった) ので、気を付けましょう。
  • ユーザーに表示するための時刻の文字列を生成する際は、andorid.text.format.DateFormat オブジェクトを使うことで端末の設定を反映させると良いでしょう。

DateCalendarDateFormat、それぞれの役割

簡単にまとめると、Date クラスが 「時間軸上の特定の瞬間」 を表すもので、Calendar クラスが年月日や時、分、秒といった情報を扱うクラス、そして DateFormat が時刻を表す文字列と 「時間軸上の特定の瞬間」 の相互変換を行う、という感じです。 詳細は以下に述べます。

Date クラス

Date クラスは、時間軸上の特定の瞬間をミリ秒の精度で表現するクラスです。

純粋に 「時間軸上のある瞬間」 を表現するためのものなので、タイムゾーンを持ちませんし、自身が表現する時刻が何月何日の何時何分なのかを計算したりもしません。 時刻を表現する文字列をパースしたり、逆に時刻を表現する文字列を出力したりもしません。 (歴史的経緯によりそのような機能のメソッドが Date クラスに定義されていますが、非推奨です。)

Calendar クラス

Calendar クラスは、次の 2 つの機能を提供するクラスです。

  • 「時間軸上の特定の瞬間」 と 「何月なのかや何日なのか、何時なのかといった数値情報・カレンダー情報」 の相互変換
  • 何日なのかや何時なのかといった数値情報・カレンダー情報に対する計算。 (例えば、1 週間後の日時を取得する、など。)

Calendar オブジェクトで時刻を表現させようと思えば実際上は可能ではありますが、そういう使い方には Date オブジェクトを用いるべきみたいですね。 年月日などを扱うので、このクラスを使う際は当然 タイムゾーンを気にする必要があります

GregorianCalendar クラス

Calendar クラスは抽象クラスで、変換や計算の処理は各種サブクラスが提供することになっています。 世界のほとんどの地域で使用される標準的な暦体系に対する実装として、GregorianCalendar が提供されています。

DateFormat クラス (java.text パッケージ)

「時間軸上の特定の瞬間」 を表す値から日付・時刻を表す文字列を生成したり、逆にそれらの文字列を解析する機能を持つクラスです。

年月日などを扱うので、このクラスを使う際は当然 タイムゾーンを気にする必要があります

SimpleDateFormat クラス

DateFormat のサブクラスです。 自分で日付・時刻の文字列のフォーマットを指定する場合はこのクラスを使用することになります。

DateFormat クラス (android.text.format パッケージ)

めちゃくちゃややこしいのですが、Android APIs には android.text.format.DateFormat というクラスも含まれています。 このクラス自身もフォーマット機能を持っていますし、java.text.DateFormat オブジェクトを返すファクトリメソッドも持っています。

API level 23 の実装だと、java.text.Date クラスの getDateInstance メソッドや getTimeInstance メソッドも端末の設定を見てくれるようですが、API level 10 では android.text.format.DateFormatgetDateFormat メソッドなどを使わないと端末の設定が反映されないようです。 (どこで変更されたかは調べてない。)

なので、端末の設定を反映させて日時や時刻を表示したい場合は、android.text.format.DateFormat クラスを使ってフォーマットを決める必要があるようです。

使い方

各種ユースケースにおいて、上で紹介したクラスをどのように使うかを簡単に紹介します。

Web API でやり取りする時刻の文字列と Date オブジェクトを相互変換する

Web API のレスポンスで "2016-01-01 00:00:00+0900" みたいな時刻を表す文字列を受け取ることがあると思いますが、そういった文字列を Date オブジェクトに変換するにはフォーマットを指定して SimpleDateFormat オブジェクトを作成しましょう。

java.text.DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US);
// タイムゾーンが含まれていないような日付文字列を解析する場合は、適したタイムゾーンを指定しましょう。
// df.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));

Date d = df.parse("2015-01-01 00:00:00+0900");

ロケールタイムゾーンの処理にも関わってるぽいです (まじか……) し、Locale.US にしておくのが良さそうです。 例えば、パターン Z を使用してタイムゾーンを解析する場合、Locale.US ならば 「EDT」 を処理できますが、Locale.ROOTLocale.JAPAN だとパースエラーになります。 (API level 23 で確認。 ロケールに関係なく RFC 822 で定義されているタイムゾーンは解析して欲しい……。 厳しい。) でも Locale.US でも 「Z」 を処理できないから、数値でなく名前で表現されるタイムゾーンの解析周りについてはそもそも信用しないのがいいかもしれないですね。

Web API などに送るための時刻文字列を生成する場合も、同様に SimpleDateFormat を使いましょう。 タイムゾーンは、受け取り側が期待するであろうものを設定しておくのが安全だと思います。

java.text.DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US);
// タイムゾーンを出力するにしても Web API 側が期待するタイムゾーンを設定しておいた方が安全でしょう。
df.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));

String currentTime = df.format(new Date());

ユーザーに表示するために Date オブジェクトから時刻の文字列を生成する

ユーザーに表示するための時刻文字列は、ユーザーの端末のロケールタイムゾーンを使用すべきでしょう。 Java では DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) という感じでデフォルトロケールDateFormat を取得できますが、Android アプリの場合は、端末の設定を反映したフォーマットの DateFormat を取得するために android.text.format.DateFormat を使用するのが良さそうです。

詳細は以下のページを見てください。

(API level 23 では、DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) みたいな感じで取得した DateFormat オブジェクトも端末の設定を反映しているようでしたが、API level 10 だと反映してなかったので、古い端末もサポートするなら android.text.format.DateFormat を使うべきっぽいです。)

Date オブジェクトが表す日の 1 年後、を計算する

例えば今日の 1 年後を表す日を計算したいとします。 そういうときは Calendar を使います。 *3

// 操作対象の Date オブジェクト。
// 日本時間で 2016 年 2 月 29 日の 00:00:00。
Date d = new Date(145667160_0000L);

// Calendar オブジェクトを使って 1 年後を計算して Date オブジェクトを取得。
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo"), Locale.JAPAN);
c.setTime(d);
c.add(Calendar.YEAR, 1); // 年に 1 加算。
Date dateOneYearAfter = c.getTime(); // 2017 年 2 月 28 日の 00:00:00 (日本時間)。

上の例では、タイムゾーンを Asia/Tokyo に設定しています。 タイムゾーンを適切に設定しないと、渡された Date オブジェクトが表す日付が変わる可能性がありますので、適切にタイムゾーンを設定しましょう。 ロケールも週の開始曜日などに影響するので必要に応じて設定しましょう。 (デフォルトタイムゾーン・デフォルトロケールが良い場合はもちろんデフォルトの値を使えば良いです。)

おわり

時刻を扱うときはタイムゾーンに気を付けなさい」 ということはみんな耳にタコができるぐらい言われてきたと思うので大丈夫だと思いますが、タイムゾーンをちゃんと設定するように気を付けましょう。

*1:ドキュメントにも 『The main reason you'd create an instance this class directly is because you need to format/parse a specific machine-readable format, in which case you almost certainly want to explicitly ask for “US” to ensure that you get ASCII digits (rather than, say, Arabic digits).』 って書かれてる。

*2:少なくとも API level 23 までは。 それより後のバージョンは不明。

*3:単純に 365 日間に相当するミリ秒を加算する、みたいな方法だとうるう年などが考慮できないのでだめですよね。 「1 ヶ月後」 とかだと何日分加算すればいいのかわかんないし。

Java における byte 型について (あるいはバイナリデータを扱うためのキャスト)

この記事は、もともと次のページに書かれていた内容に、加筆・修正を行ったものです。

はじめに

この記事では、ビット列を角括弧で囲み、[00000000] のように表現する事にします。

byte 型の表現範囲

Java のプリミティブ型 (基本データ型) の 1 つに byte 型というものがあります。 その名の通り 1 バイト (8 ビット) のサイズの整数値を表現できます。

つまり、[00000000] から [11111111] まで表現することができます。

数値でいうと 0 (0x00) から 255 (0xFF) まで表現できる、と思うところですが、実際には Javabyte 型は符号付きであり、表現できる範囲は -128 から 127 までとなっています。

The values of the integral types are integers in the following ranges:

  • For byte, from -128 to 127, inclusive
  • (略)
Chapter 4. Types, Values, and Variables

バイナリデータを扱うための byte 型のキャスト

符号なしで 0 から 255 の整数を表現したい

上述のとおり byte 型には符号が付いているのですが、バイナリデータの各バイトを 0 (0x00) から 255 (0xFF) の整数値で扱いたい場面がたまにあります。 当然 byte 型のままでは 128 以上の整数値を扱えませんので、short 型や int 型にキャストする必要があります。 逆に、shortint で表現される 0 から 255 までの整数値を byte にキャストしたい場面もあるでしょう。 (例えば InputStream#read() メソッドは、バイトの情報を 0 から 255 までの整数値として返してきます。)

byte 型からの変換

Java SE 8 からは Byte クラスに便利メソッドが用意されているので、それを使うといいでしょう。 (id:nowokay さんに教えていただきました! ありがとうございます!)

Java SE 7 以下では自分でキャストする必要があります。

キャストする場合 (Java SE 7 以下の場合)

byte から intshort へのキャストは、widening primitive conversion です。 このキャストでは、キャストによって数値の情報が失われることがありません。 (キャスト前とキャスト後で数値として同じ値が表現される。)

バイトを非負整数で表現するという目的においては、単にキャストするだけでは MSB (Most Significant Bit) が 1 の場合 (すなわち、値が負の場合) に、期待する結果にはなりません。

byte valueByte = -1; // ビット列で表すと [11111111]
short valueShort = (short) valueByte; // valueShort の値は -1
                                      // ビット列で表すと [1111111111111111]

short 型にしたときにビット列としては [0000000011111111] となって欲しいところですが、元が負値であるために上位ビットに全て 1 が入ってしまっているわけです。 byte 型をキャストして 0 から 255 の整数値の表現にするには、キャスト後に下位 8 ビット以外を 0 にすれば良いでしょう。

byte valueByte = -1; // ビット列で表すと [11111111]
short valueShort = (short) valueByte; // valueShort の値は -1
                                      // ビット列で表すと [1111111111111111]
// 下位の 8 ビットだけ残し、残りを全て 0 にする。
valueShort &= 0xFF; // 期待通りの 255 という数値になる
                    // ビット列で表すと [0000000011111111]

// キャストと下位 8 ビット以外を削る処理を 1 行で行っても良い。
valueShort = (short) (valueByte & 0xFF);

byte 型へのキャスト

一方で、intshort から byte へのキャストは、narrowing primitive conversion です。 このキャストで整数型を byte に変換する場合、単純に元の数値を表すビット列の下位 8 ビットがキャスト後の byte 値となります。

なので、0 から 255 の整数値で表現される値を byte 型にする場合は、単純にキャストしてやればよいのです。

short valueShort = 0xFF; // 数値としては 255。 ビット列で表すと [0000000011111111]
byte valueByte = (byte) short; // 数値としては -1。 ビット列で表すと [11111111]

byte 値を 16 進数表記の文字列にする

C 言語を使う人にはおなじみの printf ですが、似たものが Java にもあります。 (Java SE 5 より導入された模様。)

これを使用すれば、簡単に byte 値を 16 進数表記の文字列にできます。 標準出力などに出力して目で確認したい時にはなかなか便利です。

byte b = (byte) 0xE8; // 数値としては -24

// String#format メソッドを使用して文字列取得。
String bStr = String.format("%02X", b); // => "E8"

// PrintStream#printf メソッドを使用してそのまま出力する。
System.out.printf("%02X", b);

Java SE 1.4 以前は 「Javaのbyte型を16進数で表現」 みたいにまどろっこしいことをしないとダメだったと思います (もしかしたらもっと楽な方法はあるのかも知れませんが) けど、なかなか便利になったものです。

読んだ: Java エンジニア養成読本

2014 年 12 月に発売された 『Java エンジニア養成読本 [現場で役立つ最新知識、満載!]』 を読みました。 これだけを読んで Java がわかるわけではないけど、歴史から Java EE、周辺技術に至るまで説明されていて、Java 初心者が全体を俯瞰するのに良さそうだと思いました!

内容紹介

本書は、複数の著者による共著になっています。

まず、巻頭記事がきしださんによる 「誰も教えてくれない Java の世界」。 Java の歴史や、Java のエディション (Java SE、EE、ME について)、JDKJREJCP、JSR の紹介や各種 IDE や有名なフレームワークの紹介など、Java を使い始めてみたけど世界観がよくわからない、という人に役立つ情報が凝縮されています。 「お前に Sun が救えるか」 といったネタも紹介されています。

続く特集 1 が irof さんによる 「Java 入門」。 もちろん Java の構文の細かいところまでは説明されていませんが、クラス定義についてや例外処理、標準ライブラリについてのプラクティスなどが紹介されています。

特集 2 は bitter_fox さんによる 「Java SE 8 時代のデータ処理入門」。 Java SE 8 で導入されたラムダ式や Stream API について説明されます。 私もここら辺のことはまだ詳しくは知らなかったのでためになりました。

特集 3 はキクタローさんによる 「現場で役立つ Java EE」。 Java 初心者にとっては Java EE とは何者なのか良くわからない存在だと思いますが、この特集を読むことでなんとなく何者であるかはわかると思います。 Java EE と一口に言ってもその技術要素は多岐に渡りますが、この特集では ServletJSFJAX-RSJPACDIEJB といったわりと良く使われる技術要素について主に説明されます。 私も JSFEJB については詳しくなかったので、本書を読んで勉強になりました。

特集 4 は渡辺さんによる 「現場で役立つチーム開発入門」。 Git や MavenJUnit によるテストや Jenkins といった、開発現場で良く使われている周辺技術が説明されます。

最後に 「イマドキの Java 受託開発の現場で求められる知識」 というタイトルで、提案や設計について、「web 開発の現場では Java の知識だけでなく、HTML や CSS、JS の知識なども必要である」 みたいな話や、テストやデプロイについて説明されます。

感想

Java 初心者が Java を使った web アプリケーション開発について学ぶのにちょうど良い書籍になっていると思います。 もちろん、前半は web アプリケーション開発とは関係なく Java について学べますし、Java を使うなら (web アプリケーション開発をするつもりがなくても) 読むと良いでしょう。

文体がいいのか構成がいいのかムックという本の形態が良いのかわかりませんが、技術書としては読みやすかったです。

既に Java について深い造詣があるならこの本を読む必要はないと思います *1 が、これから Java を使っていきたい人や最近 Java を学び始めた人は、この本を早い段階で呼んでおくと後々の学習が捗ると思います。 Java の良書はいろいろありますが、重量級なものも多いですので、まずは軽く本書から読んでみる、というのをオススメします。

あなたと Java エンジニア養成読本、今すぐ購入!

Kindle 版もあるようですよ!

Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] (Software Design plus)

Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] (Software Design plus)

Javaエンジニア養成読本[現場で役立つ最新知識、満載!]

Javaエンジニア養成読本[現場で役立つ最新知識、満載!]

*1:Java 8 周りはまだ、ということであれば 「Java SE 8 時代のデータ処理入門」 は役立つでしょう

【Retrofit を読む】 利用者が定義したインターフェイスに実装を提供する Java ライブラリの作り方 【リフクレション】

この記事は、はてなエンジニアアドベントカレンダー 2014 の 15 日目のエントリです。 昨日は id:chris4403 による 「開発合宿で何を考えてどう作ったか」 でした。

このエントリでは、Android アプリおよび Java アプリケーション用の REST クライアントライブラリである Retrofit のコードを参照しながら、利用者が定義したインターフェイスの実装を提供するようなライブラリの実装方法について説明します。 主に Java のリフレクションの話になります。

注意点など

  • 本記事中に掲載されている Retrofit のコードは、Apache License, Version 2.0 のもとで公開されているものです。
  • 記事執筆時点の master ブランチの最新のコミットを参照しています。
  • Android アプリ開発者で Retrofit のコードを読みたい人は Android Studio で Retrofit のプロジェクトを開くのが楽で良いと思います。

Retrofit の紹介

Retrofit について簡単に紹介します。 詳しい話は公式サイトの方を見てください。

Retrofit は Square によって開発されている REST クライアントライブラリです。 HTTP リクエストを実行するためのメソッドを持ったインターフェイスを定義すると、Retrofit がそのインターフェイスの実装を提供してくれます。 アノテーションにより、HTTP メソッドの指定やどの引数がなんのパラメータであるか、あるいはリクエストボディであるかといったことを指定できます。

下記は Groovy スクリプトはてなハイクの公開タイムラインを取得する REST クライアントのサンプルコード *1 です。 HTTP メソッドの種類とリクエスト先のパスを GET アノテーション で指定しています。 そして、リクエストごとにクエリパラメータを変更できるように、クエリパラメータの値を引数として受け取るようになっています。

@Grab('com.squareup.retrofit:retrofit:1.8.0')

import retrofit.RestAdapter
import retrofit.http.GET
import retrofit.http.Query

// HTTP リクエストを投げるメソッドを持つインターフェイスを定義する。
interface HaikuService {
    /** はてなハイクの公開タイムラインを取得する */
    @GET("/api/statuses/public_timeline.json")
    List<?> getPublicTimeline(@Query("page") int page, @Query("count") int count)
}

// 定義したインターフェイスの実装を取得。
RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("http://h.hatena.ne.jp")
    .build()
HaikuService haiku = restAdapter.create(HaikuService.class)

// 取得した実装を使用してハイクの公開タイムラインの内容を表示。
def statuses = haiku.getPublicTimeline(1, 10)
for (def status : statuses) {
    println '---'
    println status.user.name
    println status.text
}

利用者側がインターフェイスを定義して、そのインターフェイスの実装をライブラリが提供する、というのが特徴的ですね。

なお、HTTP 通信の実装や、リクエストボディおよびレスポンスボディと Java オブジェクトの変換機能の実装については Retrofit 自体が持っているわけではありません。 (デフォルトで HTTP 通信の実装はプラットフォームに応じたものが使われ、変換機能には Gson が使われますが、好みに応じてそれらは変更できます。)

利用者が定義したインターフェイスに対して実装を提供するために必要な技術

さて、Retrofit のように利用者が定義したインターフェイスに対して実装を提供するようなライブラリを書くことを考えましょう。 Java では、リフレクションを使用することで実現可能です。

任意のインターフェイスに対して実装を提供するために、Proxy クラス を使用することができます。 そして、実行されたメソッドの情報を取得するために、リフレクションのための各種メソッドを使用できます。 この 2 つについて順番に説明します。

Proxy クラス

Proxy クラスは、動的プロキシクラス (実行時に指定されたインターフェイスを実装するクラス) を生成するためのクラスであり、また、動的クラスのスーパークラスにもなるものです。 実際のメソッド呼び出しの処理は InvocationHandler インターフェイス を実装したクラスのインスタンスが担います。 Proxy クラスを使うことで、実行時に任意のインターフェイスに実装を提供することができます。

Retrofit では、retrofit.RestAdapter#create(Class) メソッド の中で動的プロキシクラスのインスタンス生成が行われます。

  /** Create an implementation of the API defined by the specified {@code service} interface. */
  @SuppressWarnings("unchecked")
  public <T> T create(Class<T> service) {
    Utils.validateServiceClass(service);
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new RestHandler(getMethodInfoCache(service)));
  }

retrofit.RestAdapter#create(Class<T>) メソッド

上記コードを見ればわかるように、指定されたインターフェイスの実際のメソッド呼び出しを処理するのは retrofit.RestAdapter.RestHandler クラスです。

リフレクションによるメソッド情報の取得

メソッドが呼ばれた際に実行される HTTP リクエストの内容は、各メソッドに付けられたアノテーションや返り値の型、仮引数の情報によって決まります。 すなわち、メソッドの情報を実行時に見る必要があります。 メソッドの情報を実行時に得るには、Method クラス に定義されている各種メソッドが使用できます。

Retrofit ではどのような処理になっているのか見ていきましょう。 retrofit.RestAdapter.RestHandler#invoke(Object, Method, Object[]) メソッドの実装は次のようになっています。

  @SuppressWarnings("unchecked") //
  @Override public Object invoke(Object proxy, Method method, final Object[] args)
  throws Throwable {
    /* (略) */
    // Load or create the details cache for the current method.
    final RestMethodInfo methodInfo = getMethodInfo(methodDetailsCache, method);
    if (methodInfo.isSynchronous) {
      try {
        return invokeRequest(requestInterceptor, methodInfo, args);
      } catch (RetrofitError error) {
    /* (略) */

retrofit.RestAdapter.RestHandler#invoke(Object, Method, Object[]) メソッド

まず、getMethodInfo(Method) メソッド呼び出しにより指定のメソッドの情報を RestMethodInfo オブジェクトとして受け取り、その後 invokeRequest メソッドを呼び出して実際の HTTP リクエスト処理に入ります。 実際には、同期実行か非同期実行か、あるいは RxJava を使っているかで処理が分かれたりしますが、ここではそこら辺の詳細には立ち入りません。

例えば、HTTP リクエストの結果の受け取り方は RestMethodInfo#parseResponseType() メソッドで解析されます。

  private ResponseType parseResponseType() {
    // Synchronous methods have a non-void return type.
    // Observable methods have a return type of Observable.
    Type returnType = method.getGenericReturnType();

    // Asynchronous methods should have a Callback type as the last argument.
    Type lastArgType = null;
    Class<?> lastArgClass = null;
    Type[] parameterTypes = method.getGenericParameterTypes();
    if (parameterTypes.length > 0) {
      Type typeToCheck = parameterTypes[parameterTypes.length - 1];
      lastArgType = typeToCheck;
      if (typeToCheck instanceof ParameterizedType) {
        typeToCheck = ((ParameterizedType) typeToCheck).getRawType();
      }
      if (typeToCheck instanceof Class) {
        lastArgClass = (Class<?>) typeToCheck;
      }
    }
    /* (略) */

retrofit.RestMethodInfo#parseResponseType() メソッド

同期実行の場合はメソッドの返り値として HTTP リクエストの結果が返され、非同期実行の場合はコールバックオブジェクトが引数に渡されるようになっているので、下記のメソッドを呼ぶことでメソッドの返り値と仮引数との両方を確認しています。

他にも、メソッドに付けられたアノテーション一覧を取得するために getAnnotations() メソッド が使われたり、仮引数につけられたアノテーションを取得するために getParameterAnnotations() メソッド が使われたりしています。

まとめと参考文献

  • Proxy クラスと InvocationHandler インターフェイスを使用することで、任意のインターフェイスに対して動的プロキシ・クラスを生成することができる。
  • リフレクションにより、メソッドの返り値の型などの情報を実行時に取得することができる。
  • これらの機能を使うことで、利用者が定義したインターフェイスに実装を提供するライブラリを記述できる。

リフレクションについては次のドキュメントを参考にすると良いでしょう。

おわりに

この記事では、Retrofit の実装を参照しながら Java のリフレクション機能について紹介しました。 バグの原因になりがちなのでアプリケーションコード中で直接リフレクションを使用するのはできるだけ避けた方がいいと思いますが、リフレクションにより Retrofit のような便利なライブラリを実現することも可能ですので、いい感じに使っていきたいですね。

*1:サンプルコードなのでエラー処理などちゃんと行っていません。