Javaの理論と実践: Java 5 の言語機能を以前の JDK で使う

JDK 1.4 でも Generics は使えます

Java™ 5 には、Generics や列挙、アノテーション、Autoboxing、そして拡張 for ループなど、いくつかの強力な言語機能が追加されています。しかし多くの職場では相変わらず JDK 1.4 やそれ以前のバージョンが使われており、今後もしばらく、それが続くかもしれません。しかしそうした職場の開発者も、Java 5 以前の JVM へのデプロイを続けながら、こうした強力な言語機能を利用できるのです。今回の「Java の理論と実践」では、久しぶりに戻ってきた Brian Goetz が、そのための方法を解説します。

Brian Goetz (brian.goetz@sun.com), Senior Staff Engineer, Sun Microsystems

Brian Goetz photoBrian Goetz はこれまで 20 年間、プロのソフトウェア開発者として活躍してきました。現在は Sun Microsystems のシニア・スタッフ・エンジニアであり、複数の JCP Expert Group の一員でもあります。2006年5月に Addison-Wesley から彼の著書『Java 並行処理プログラミング ― その「基盤」と「最新 API」を究める ―』が出版されています。人気の業界紙に掲載された、Brian のこれまでの記事、そして今後の記事を参照してください。



2007年 2月 27日

Java 6.0 が新たにリリースされことによって、Java 5 の言語機能は「古いニュース」と思っている人もいるかもしれません。しかし現在でさえ、開発にどのバージョンの Java プラットフォームを使用しているかを開発者に尋ねると、たいていの場合は半数の人しか Java 5 を使っておらず、残りの半数はそうした人達に嫉妬を感じているのです。彼らは Generics やアノテーションなど、Java 5 に追加された言語機能を使いたいと思っているのですが、いくつかの要因によって、多くの人がそれを実現できないままでいます。

Java 5 の機能を利用できない開発者達の 1 つのカテゴリーとして、コンポーネントやライブラリー、アプリケーション・フレームワークなどの開発を行う人達がいます。彼らの顧客は相変わらず JDK 1.4 やそれ以前のバージョンを使用しているかもしれず、また Java 5 でコンパイルされたクラスは JDK 1.4 やそれ以前の JVM ではロードできないため、Java 5 の言語機能を使うと、彼らの顧客ベースが既に Java 5 に移行した会社のみに制限されかねません。

Java 5 を使わないままでいる、もう 1 つの開発者グループが、Java EE で作業している人達です。多くの職場では、彼らのアプリケーション・サーバーのベンダーがサポートしていないおそれがあるとして、Java EE 1.4 やそれ以前のバージョンで Java 5 を使おうとしません。こうした職場が Java EE 5 に移行するまでには、しばらく時間がかかるかもしれません。Java EE 5 の仕様と Java SE 5 の仕様の完成時期にずれがある上に、仕様が完成したからといって商用の Java EE 5 コンテナーが必ずしも即座に入手できるとは限らず、企業では必ずしも次のバージョンが入手可能になった途端にアプリケーション・サーバーをアップグレードするわけではありません。それに、たとえアプリケーション・サーバーをアップグレードした後でも、新しいプラットフォーム上で彼らのアプリケーションの動作を保証するためには、しばらく時間がかかるものです。

Java 5 の言語機能の実装

Java 5 に追加された言語機能、つまり Generics や列挙、アノテーション、Autoboxing、拡張 for ループなどは、JVM の命令セットには何も変更が必要なく、ほとんど完全に静的コンパイラー (javac) やクラス・ライブラリーの中で実装されます。Generics が使われている部分にコンパイラーが突き当たると、コンパイラーはタイプ・セーフが保持されていることを検証しようとし (検証できないと「無検査キャスト」警告を出します)、そして次に、Generics を使わない等価なコード (キャストなど) から作成されるものと同じバイトコードを出力します。同様に、Autoboxing と拡張 for ループは単純に、それらと等価で冗長なイディオムに対する「糖衣構文」にすぎず、また列挙は通常のクラスの中にコンパイルされます。

理論的には、javac によって作成されるクラス・ファイルを、以前の JVM にロードすることができます。実際これは、JSR 14 (Generics に責任を持つ Java Community Process ワーキング・グループ) が招集された際に意図されたことだったのです。しかし、他の問題 (アノテーションの保持など) の影響で、クラス・ファイルのバージョンが Java 1.4 と Java 5 の間で変更を余儀なくされ、そのため Java 5 用にコンパイルされたコードを以前のJVM にロードすることができません。さらに、Java 5 に追加された言語機能の一部は Java 5 のライブラリーに依存しています。例えば、あるクラスを javac -target 1.5 でコンパイルし、それを以前の JVM にロードしようとすると UnsupportedClassVersionError が出ます。これは、-target 1.5 オプションがクラス・ファイル・バージョン 49 のクラスを生成し、JDK 1.4 はクラス・ファイル・バージョン 48 までのクラスしかサポートしていないためです。

for-each ループ

拡張 for ループ (for-each ループと呼ばれることもあります) は、あたかもプログラマーがこれと等価な昔のスタイルの for ループを提供したかのように、コンパイラーによって変換されます。for-each ループは、配列、あるいはコレクションの要素に対して繰り返しを行います。リスト 1 は、for-each ループを使ってコレクションに繰り返しを行う構文を示しています。

リスト 1. for-each ループ
Collection<Foo> fooCollection = ...

for (Foo f : fooCollection) { 
    doSomething(f);
}

コンパイラーはこのコードを、これと等価なイテレーター・ベースのループに変換します (リスト 2)。

リスト 2. リスト 1 と等価なイテレーター・ベースのループ
for (Iterator<Foo> iter=f.iterator(); f.hasNext();) { 
    Foo f = (Foo)iter.next();
    doSomething(f);
}

与えられた引数が iterator() メソッドを持っていることを、コンパイラーはどうやって知るのでしょう。javac コンパイラーのアーキテクトは、コレクション・フレームワークを理解した上でコンパイラーを作ることもできたかもしれませんが、その方法では必要以上に制限が厳しくなってしまっていたことでしょう。その代わり新しいインターフェース java.lang.Iterable (リスト 3) が作成され、コレクション・クラスは Iterable を実装するように後から変更されました。こうすることで、コアとなるコレクション・フレームワークを基にして作られていないコンテナー・クラスも、新しい for-each ループを利用できます。しかしそうすると、Iterable は JDK 1.4 のライブラリーには存在しないため、Java 5 のクラス・ライブラリーに依存することになります。

リスト 3. Iterable インターフェース
public interface Iterable<T> {
    Iterator<T> iterator();
}

列挙と Autoboxing

for-each ループの場合と同様、列挙も、クラス・ライブラリーによるサポートが必要です。コンパイラーが列挙型に突き当たると、コンパイラーはライブラリー・クラス java.lang.Enum を継承するクラスを生成します。しかし Iterable とまったく同じように、Enum クラスは JDK 1.4 のライブラリーには存在しません。

Autoboxing も同様に、プリミティブ・ラッパー・クラス (Integer など) に追加されている valueOf() メソッドに依存します。ボクシングによって int から Integer への変換が必要な場合には、コンパイラーは new Integer(int) をコールするのではなく、Integer.valueOf(int) へのコールを生成します。valueOf() メソッドの実装は、一般的に使われる整数値の Integer オブジェクトを Flyweight パターンを使ってキャッシュします (Java 6 の実装では -128 から 127 までの整数をキャッシュします)。これによって余分なインスタンス化がなくなるため、パフォーマンスが向上する可能性があります。そして、Iterable や Enum とまったく同じように、valueOf() メソッドも JDK 1.4 のライブラリーには存在しません。

可変長引数 (vararg)

可変長引数のリストで定義されているメソッドをコンパイラーが見つけると、コンパイラーはそれを、適当なコンポーネント・型の配列を入力とするメソッドに変換します。また可変長引数のリストを持つメソッドの呼び出しをコンパイラーが見つけると、コンパイラーはその引数を配列に格納します。

アノテーション

アノテーションが定義されると、そのアノテーションは @Retention と表記されます。これによって、そのアノテーションを持つクラスやメソッド、あるいはフィールドに対してコンパイラーが何をするかが決まります。定義されている保持ポリシーは、SOURCE (コンパイル時にアノテーション・データを破棄する)、CLASS (クラス・ファイルの中にアノテーションを記録する)、あるいは RUNTIME (クラス・ファイルの中にアノテーションを記録し、それを実行時に保持して、リフレクションによりアクセスできるようにする) です。

ライブラリーに対する他の依存関係

Java 5 以前には、2 つの文字列を連結しようとする動作にコンパイラーが突き当たると、コンパイラーはヘルパー・クラス StringBuffer を使って連結を行いました。Java 5 以降では、コンパイラーは新しい StringBuilder クラスの呼び出しを生成します。そして StringBuilder クラスは JDK 1.4 以前のクラス・ライブラリーには存在しません。


Java 5 の機能を利用する

ライブラリーのサポートが言語機能に依存しているため、たとえ Java 5 のコンパイラーが作成するクラス・ファイルが以前のバージョンの JVM にロードできたとしても、実行するとやはりクラス・ロードする際のエラーで失敗してしまいます。しかしこうした問題は、バイトコードを適切に変換することで解決できるはずです。なぜなら、足りないクラスに重要な新機能が含まれているわけではないからです。

JSR 14

Java の Generics の仕様 (および Java 5 で追加された他の言語機能) の開発期間中、Java 5 の言語機能を利用して Java 1.4 JVM 上で実行可能なバイトコードを生成するという、実験的な機能が javac コンパイラーに追加されました。この機能は正式なサポートはされておらず、文書化もされていませんが、いくつかのオープンソース・プロジェクトでは、Java 5 の言語機能を使ってコーディングできるように、また以前の JVM で利用できる JAR ファイルを作成するために使われています。そして、今や javac がオープンソースとなったので、この機能をサードパーティーがサポートすることも可能になりました。この機能を有効にするには、-source 1.5 オプションと -target jsr14 オプションを付けて javac を呼び出します。

この、javac の JSR 14 ターゲット・モードでは、コンパイラーは Java 5 の下記の言語機能に対応する JDK 1.4 互換のバイトコードを出力します。

  • Generics と可変長引数: Generics が存在する場合にコンパイラーが挿入するキャストはクラス・ライブラリーに依存しないため、Java 5 以前の JVM でも、Java 5 の JVM の場合と同じように適切に実行されます。同様に、可変長引数のリストが存在する場合にコンパイラーが生成するコードも、クラス・ライブラリーには依存しません。
  • for-each ループ: 配列に対して繰り返しを行う場合、コンパイラーは帰納変数と標準的な配列の繰り返しを行うイディオムを生成します。Collection に対して繰り返しを行う場合、コンパイラーは標準的なイテレーター・ベースのイディオムを生成します。Collection ではない Iterable に対して繰り返しを行う場合には、コンパイラーはエラーを起こします。
  • Autoboxing: コンパイラーは、ラッパー・クラスの valueOf() メソッドの呼び出しではなく、コンストラクターの呼び出しを生成します。
  • 文字列の連結: javac の JSR 14 ターゲット・モードでは、コンパイラーは StringBuilder の呼び出しではなく StringBuffer の呼び出しを生成します。
  • 列挙: javac の JSR 14 ターゲット・モードは列挙に対して特別なサポートをしていません。列挙を使おうとするコードは、java.lang.Enum ベース・クラスを探して NoClassDefFoundError で失敗します。

JSR 14 ターゲット・モードを使うことで、「容易な」ケースにおいて、Generics や Autoboxing、for-each ループなどを使用するコードを作成することができます。これでも、多くのプロジェクトでは十分かもしれません。JSR 14 ターゲット・モードは、Java 5 をサポートしていない場合には便利であり、コンパイラーは JDK 1.4 にほぼ互換のバイトコードを 1 回のパスで生成するのです。

Retroweaver

JSR 14 ターゲット・モードでサポートされていない Java 5 の言語機能もいくつかあります (Iterable や列挙など)。Retroweaver や Retrotranslator などのオープンソース・プロジェクトでは代わりの方法を採用しており、-target 1.5 を使ってバイトコードを生成してから、機械的にそのバイトコードを JDK 1.4 と互換のものに変換します。

最初に登場したのは Retroweaver です。Retroweaver は javac -target JSR 14 で処理できるすべてのケースを処理でき、さらに下記を処理することができます。

  • for-each ループ: Retroweaver は Iterable インターフェースの実装を提供しており、Iterable を実装するクラスを、Retroweaver 独自のバージョンを実装するように書き直します。
  • Autoboxing: Retroweaver は、valueOf() メソッドの呼び出しを、対応するコンストラクターに書き直します。
  • 文字列の連結: Retroweaver は、StringBuilder が使われている部分があると、その部分に StringBuffer を使うように変更します。
  • 列挙: Retroweaver は Enum ベース・クラスの実装を提供しており、Enum を実装するクラスを書き直すか、あるいは Retroweaver のメソッドを呼び出して独自のバージョンを使います。

Retrotranslator

オープンソースの世界ではよくあることですが、あるプロジェクトの前進が止まると、そのプロジェクトは死んだと宣言され、(たとえそのプロジェクトが単に休憩中であったとしても) 新しいプロジェクトがそのプロジェクトに置き換わります。Retroweaver の運命は、正にそれに当たります。中心的な維持管理者が休憩に入り、別の似たプロジェクト、Retrotranslator が代わりに登場しました。Retrotranslator は Retroweaver と同じ機能を提供し、さらに、Java 5 に追加された重要なクラス・ライブラリーのサポートを目的とした、下記のような多くの機能を追加しています。

  • java.util.concurrent クラスの呼び出しを、それに対応する、オープンソースの JDK 1.4 バックポートのクラスに置き換えます。
  • Java 5 でコレクション・フレームワークに追加された機能 (例えば Arrays や Collections の新しいメソッドなど) の実装を提供します。同様に、Java 5 のクラス・ライブラリーに追加された他の新しいメソッドやクラスの実装も提供します。
  • アノテーションに対するランタイム・リフレクションをサポートします。

Retroweaver も Retrotranslator も、バイトコード変換を (コンパイル時に) 静的に行うこともでき、また (クラスのロード時に) 動的に行うこともできます。


まとめ

Java 5 の言語機能を使えずにいる不幸な開発者 (残念ながら相変わらず非常に多数います) のために、いくつかのオプションがあります。それらを利用することで、新機能の一部を利用でき、しかも JDK 1.4 およびそれ以前のバイトコードとの互換性を保つことができます。javac の -target jsr14 オプションは正式なサポートはされていませんが、Java 5 の言語機能の一部に対して JDK 1.4互換のバイトコードを生成します。また、オープンソースの Retroweaver プロジェクトと Retrotranslator プロジェクトは、大部分の Java 5 バイトコードを Java 1.4 互換のバイトコードに変換します。当然ですが、どちらのオプションを選択する場合も、本当に互換性があるかどうか、十分に注意してテストすることを忘れないでください。

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=250291
ArticleTitle=Javaの理論と実践: Java 5 の言語機能を以前の JDK で使う
publish-date=02272007