はじめに
例外が起きてログを取る場合やデバッグ中のオブジェクトの状態を知りたい場合に、そのオブジェクトの参照をロガーやSystem.out.printlnの引数に与えたら、単に「クラス名@16進文字列」と出力されて何の手がかりも得られなかったという経験はありませんか?
Javaではオブジェクト固有の情報を文字列化するためにObject#toStringメソッドが規定されています。そのためObject#toStringメソッドを実装すれば冒頭のような問題は解消します。とは言え、複数のクラスがある場合にすべてにきちんと実装するのはそれなりに手間がかかりますし、利用しているすべての関連するオブジェクトがtoStringで内部情報を出力することを求めるのはいささか無理があるかも知れません。
この記事では、情報を取得したいオブジェクトが持つフィールドの情報を文字列化するユーティリティクラスの実装について解説します。
対象読者
本記事は、Javaプログラミングの比較的初心者を想定したReflection(リフレクション)の紹介記事です。
対象読者としては、Javaの文法を理解している程度の比較的初心者を想定しています。そのためプログラムも単純に記述してあります。実際にプログラムを実行したりソースを修正して試すことでReflectionの利用方法についての学習のとっかかりになるでしょう。
Reflection
Javaを始め多くのオブジェクト指向言語では、実行中のオブジェクトのメタデータ(クラス、メソッド、フィールドなどのプログラムを構成するオブジェクト自身の情報)もオブジェクトとして実装されています。そのため、これらのオブジェクトを操作することでプログラムの状態を様々な角度から調査したり変更したりできます。
Javaでメタデータを操作するために提供されているのがReflectionです。たとえばフレームワークやユーティリティの作成にReflectionの知識は欠かせません。また、通常のビジネスロジックプログラムであっても、Reflectionを利用することでデバッグやテストが簡単になることがあります。
Reflectionを利用することは、単にプログラミング言語をプログラムの実装に利用するだけではなく、プログラミング言語自身をプログラムの実装から利用することでもあります。このことは、システムを構築するのに必要となる複眼的な思考を養う良い練習になるでしょう。
必要な環境
ソースはJ2SE 1.4.2およびJ2SE 1.5.0でコンパイル/実行が可能なことを確認しています。
その他に用意しておいたほうが良いもの
ant 1.6以上またはNetBeans4.0以上
ソースのビルドや、サンプルの実行を補助するためにantのビルドスクリプトを用意してあります。一括してビルドするには、antそのものか、antスクリプトをプロジェクトで直接扱えるNetBeans4.0以上を利用するのが良いでしょう。
JUnit 3.8.1
ソースファイルの中にテストプログラムを用意してあります。実際にテストプログラムをビルドして動かすにはjunitが必要となります。
ファイル構成
ダウンロードしたファイルはzipで圧縮してあります。展開すると「dumpobj」というディレクトリを頂点としたディレクトリ階層ができます。すぐに実行できるようにコンパイル済みのクラスファイルも添付してあります。また、ソースファイルはすべてシフトJISでエンコードしています。
「dumpobj」ディレクトリ
- build.xml
「dumpobj\src」ディレクトリ
- Sample.java
DumpObject(本記事で説明するプログラム)の動作説明のためのサンプルプログラムです。「dumpobj\src\com\example\tool」ディレクトリ
- DumpObject.java
「dumpobj\test\com\example\tool」ディレクトリ
- DumpObjectTest.java
- DumpObjectSecurityTest.java
サンプルの実行
DumpObjectの説明の前に、実際に確認可能なプログラムで簡単に動作について説明します。
添付サンプルプログラムについて
動作確認用サンプルとして「Sample.java」というプログラムを、展開したディレクトリ直下の「src」ディレクトリに用意してあります。実行するには、コマンドプロンプトからzipを展開したディレクトリ「dumpobj」へ入り、
java -cp build/classes Sample
と打ち込んでください。または、antがインストール済みでパスが通っていれば、
ant sample
と入力することでクリーンビルド後に実行することができます。
import com.example.tool.DumpObject; public class Sample { String name; Sample partner; Sample(String initName) { name = initName; } void createPartner(String name) { partner = new Sample(name); partner.connect(this); } void connect(Sample newPartner) { partner = newPartner; } public static void main(String[] args) throws Exception { Sample sample = new Sample("hello"); DumpObject dumper = new DumpObject(sample); System.out.println("---- initial state ----"); System.out.println(dumper); sample.createPartner("bye"); System.out.println("---- second state ----"); System.out.println(dumper); } }
Sampleクラスは、nameとpartnerの2つのフィールドを持ちます。
mainメソッドを実行すると、最初にnameとして"hello"を持ったオブジェクトを生成しDumpObjectのコンストラクタに与えています。次に最初のSampleクラスのオブジェクトのcreatePartnerメソッドに"bye"という文字列を与えることで、新たなオブジェクトを生成しています。この2つのオブジェクトはそれぞれのpartnerフィールドへお互いの参照を格納します(createPartnerメソッドとconnectメソッド)。
この例で示されているようにDumpObjectは文字列化対象のオブジェクトをコンストラクタに与えて生成し、toStringメソッドを呼び出して対象のオブジェクトを文字列化するユーティリティです。
なおSampleクラスでは、DumpObjectを利用して、生成直後とcreatePartnerメソッドの呼び出し後の2カ所でオブジェクトをプリントしています。
サンプルの出力結果について
サンプルを実行すると以下のように出力されます。
$ java -cp build/classes Sample ---- initial state ---- Sample@ffbd68 name="hello" partner=null ---- second state ---- Sample@ffbd68 name="hello" partner=Sample@d1e604 name="bye" partner=(Sample@ffbd68) $
出力結果について
最初の時点ではpartnerフィールドの値としてnullと出力されているのに対し、次の時点ではcreatePartnerメソッドの呼び出しによって作成されたSample@d1e604というObject#toStringメソッドの結果(次項参照)が出力されています。DumpObjectはこの例のようにフィールドに格納されたオブジェクトについてはインデントを付けて出力します。さらに、新たなオブジェクトのpartnerフィールド(これは最初に作成したオブジェクトの参照です)についてはオブジェクト全体ではなく単に(Sample@ffbd68)と出力しています。DumpObjectはオブジェクトの出力時に既にフィールドの内容を出力済みかどうかをチェックし、既に出力済みであればここでの出力のように単に()内にObject#toStringの結果の文字列を出力します。
Object#toStringの規定動作について
実行結果の複数箇所に出現しているSample@16進数字という文字列は、Object#toStringメソッドの規定動作で、クラス名とオブジェクトのハッシュコードを組み合わせたものです。ちなみにJ2SEのAPIドキュメントのObject#toStringの項には
getClass().getName() + '@' + Integer.toHexString(hashCode())
というこの文字列の生成方法が記述されています。
また、@の右側の値は実行時に変化するため、実際に試された場合はここで示されたものとは異なる値となります。
プログラムの説明
オブジェクトの出力方法について
DumpObjectは、オブジェクトの文字列化を以下の手順で実行します。
- 対象のオブジェクトが
nullならば"null"を利用する(最初のpartner=null) - 対象のオブジェクトがプリミティブ型か、
ObjectクラスそのもののインスタンスならばtoStringを呼び出す - 対象のオブジェクトが文字列の仲間(
CharSequenceインターフェイスを実装したクラス)であれば前後に"を付加した文字列そのものとする(name="hello"の行など) - 対象のオブジェクトが配列であれば、最初に
[を出力し、各要素について再帰的に1.以降を呼び出し、最後に]を付加する - もし対象のオブジェクトを既に文字列化していれば、
(と)の中にtoStringの呼び出し結果を入れる(最後のpartner=の行) - もし対象のオブジェクトが
Object#toStringメソッドをオーバーライドしていればtoStringメソッドの呼び出し結果を利用する - それ以外の場合は、最初に
toStringの呼び出し結果を出力する(先頭のSample@ffbd68の行など) - 次に各フィールドについて、フィールド名、=、2.以降の呼び出し結果を出力する
それでは各処理の要所について順に見ていきましょう。
公開メソッド
public String toString() { embeded = new HashMap(); String s = toString(target, 0); embeded = null; return s; }
DumpObjectは無引数のtoStringメソッドを利用するプログラムからの呼び出し用に公開します。実際の文字列化処理はここから呼び出している2引数のtoStringメソッドとなります。
toStringメソッドの役割で重要なものに、オブジェクトの循環参照を検出するためのHashMapの生成と削除があります。toStringの呼び出しは複数回可能なため、呼び出しの間にそれぞれのオブジェクトのフィールドの内容が変化する可能性があります。そのため、オブジェクトの参照はtoStringメソッドの呼び出しの都度、新たに作成しなければなりません。また、toStringの呼び出し間隔が短いか長いかは判断できないため、作成したHashMapを保持しておくことは内部に格納した参照が残ることからガベージコレクターの動作を妨害する可能性を持ちます。そのため、メソッドから呼び出し元へ戻る前に参照を削除する必要があります。
別解について
なお、循環参照の検出に利用しているembededフィールド(HashMap)については別の実装方法が2種類あります。
バグに影響されないことを前提とする実装
1つは、embededフィールドに格納するHashMapをコンストラクタで作成し、toStringの呼び出しの最後にHashMap#clearを呼び出すことで出力対象の参照を削除する方法です。この方法でももちろん構いませんが、可能性としてDumpObject自体のバグによってtoStringメソッドの呼び出しが例外で中断された場合に不要な参照が残ってしまう問題を挙げることができます。元の実装でも例外で中断された場合、embededフィールドに不要な情報が残りますが、次のtoStringの呼び出し時点で新たに作成し直すため、実際の文字列化処理が不要な参照に影響されることはありません。
この問題を回避するには、
public String toString() { // コンストラクタでembeded = new HashMap();を実行するので // ここでは生成しない String result; try { result = toString(target, 0); } catch (Exception e) { // 例外となった場合は無理をしない result = ""; } finally { embeded.clear(); } return result; }
のように実装する必要があります。逆にこのように例外の発生を呼び出し側に通知しない実装は、特にDumpObjectのように障害時のログ取得やデバッグ時のヘルパーとして利用するユーティリティとしては望ましいことです。
たとえば、targetオブジェクトが以下のように実装されたオブジェクトだった場合を想像してみましょう。
public class Buggy { ResultSet resultSet; public Buggy() { } public String toString() { return "resultSet=" + resultSet.toString(); } ... }
Buggyクラスは、toStringメソッドでわざわざ内包しているResultSetのtoStringメソッドを呼び出していますが、もし生成直後であればフィールドの内容はnullなのでNullPointerExceptionとなってしまいます。なお、この例でのResultSetのtoString呼び出しに意味があるかどうかは微妙ですが、正しくは
return "resultSet=" + resultSet;
とすべきです。
このようにユーティリティクラスを作成する場合には、常に操作対象のクラスにバグがあることを前提しなければなりません。
フィールドを利用しない実装
もうひとつの実装方法は、embededフィールドを利用しないことです。
なぜ、循環参照の検出用HashMapをフィールドに保持しているかと言えば、文字列化の実行中に複数のメソッド内で参照する必要があるからです。この場合、メソッドの引数として与えるようにすれば、フィールドに保持する必要はありません。特に、DumpObject#toStringは1回の呼び出しで処理が完結するため、最後の呼び出し時点の情報を覚えておく必要はありません(必要がないため、元の実装ではnullをembededフィールドに設定しているわけです)。
また、フィールドとして持たずにローカル変数として作成し、メソッドの引数として与えるようにすると、同時に複数のスレッドから呼び出しても安全に操作できるようになります。元の実装では、public toStringは同時には1つのスレッドからの呼び出しにしか対応できません。なぜならば同時に呼び出されると最後に処理を開始したスレッドが設定したHashMapによって上書きされてしまうからです。その場合、既に文字列化済みの参照の情報が失われて正しく処理できなくなる可能性が生じます。
フィールドを利用しなければオブジェクト自体が不要となるためstaticメソッドとして実装することができます。ユーティリティクラスはリソースを消費しなければしないほど望ましいため、通常ユーティリティクラスはstaticメソッドだけで構成します。しかし、メソッドの引数が増えるとプログラムそのものが読みにくくなりますので、この例のように複数のメソッドが参照するオブジェクトを利用する場合にはフィールドの利用を検討しても良いでしょう。もっともDumpObjectの場合は文字列化対象となるオブジェクトの分身としての利用を想定しているため、いずれにしてもstaticメソッドだけで構成したユーティリティとして実装することは考えていません。
文字列化対象オブジェクトのクラスの取得
String toString(Object o, int level) { if (o == null) { return "null"; } else { return dumpValue(o.getClass(), o, level); } }
処理1.に該当するメソッドです。
与えられたオブジェクトがnullであればnullを返送します。そうでなければクラスを取得してdumpValueメソッドを呼び出します。このリストで示されたように、実行時にオブジェクトのクラスを取得するには、Object#getClassメソッドを呼び出します。
なお、このメソッドは公開メソッドのtoStringの他、配列の各要素の文字列化のためにも呼び出されます。
既知のオブジェクトの文字列化
String dumpValue(Class c, Object o, int level) { if (o == null) { return "null"; } else if (c.isPrimitive() || c == Object.class) { return o.toString(); } StringBuffer buffer = new StringBuffer(); if (CharSequence.class.isInstance(o)) { buffer.append('"').append(o).append('"'); } else if (c.isArray()) { buffer.append("[ "); for (int i = 0, n = Array.getLength(o); i < n; i++) { buffer.append(toString(Array.get(o, i), level + 1)); buffer.append(", "); } buffer.append(']'); } else { EmbededKey ekey = new EmbededKey(c, o); if (embeded.containsKey(ekey)) { buffer.append('(').append(embeded.get(ekey)).append(')'); } else { buffer.append(dumpObject(c, o, level)); } } return new String(buffer); }
処理2~5.に該当するメソッドです。
直前のtoString(Object,int)でもnullチェックをしましたが、ここでも実行しています。重複処理は避けるべきにも関わらずこのような記述を行うには理由があります。それはこのメソッドを呼び出すのがtoString(Object,int)だけではないからです。それ以外の呼び出し箇所ではパラメータとして与えたオブジェクトがnullかどうかは検証されないため、ここでチェックする必要があります。そうでなければ、プログラムのさらに多くの箇所でのnullチェックが必要になってしまうでしょう。
実際には、nullをチェックしてからさらに後の内容を実行するようにdumpValueメソッドを2つのメソッドに分割することも可能です。しかし、そのように実装するとメソッド構成が煩雑になることと、本来1つの処理を2つに分離することに意味を見いだせないことからここではこのように実装しています。
次にClass#isPrimitiveを実行することで、与えられたオブジェクトがプリミティブ型かどうかをチェックしています。プリミティブ型か、オブジェクトのクラスがObjectの場合はtoStringメソッドを呼び出して完了します。
なお、この後各オブジェクトの内容を文字列化する過程(dumpObjectメソッド)で、クラスの継承関係をたどって行きます。なぜならば、フィールドの全取得は個々のクラスで定義した分についてしか行えないからです。一方、既にサンプル実行などで見たように、Object#toStringの結果の文字列は作成する文字列の先頭に配置します。このため、ここより後の呼び出しでClassパラメータにObjectクラスを与えると最初と最後が同じ文字列となってしまいます。そのため、この時点より後でObjectクラスをClassパラメータとして利用しないように制御していることになります。
プリミティブ型でもObjectクラスのオブジェクトでも無いと判定した場合は、次に文字列かどうかを検証します。Class#isInstanceメソッドは引数のオブジェクトが該当クラスのインスタンスかどうかを判定します。ここでは、CharSequenceインターフェイス(ReflectionではinterfaceとclassはどちらもClassクラスのオブジェクトとなります)かどうかで判定します。なお、ここで記述しているように、クラス名に.classを付けることで静的にClassオブジェクトを取得するコードを記述することができます。実行時にオブジェクトからClassオブジェクトを取得するにはObject#getClassメソッドを、コンパイル時に静的にClassオブジェクトを取得するには、「クラス名.class」を利用することは覚えておいたほうが良いでしょう。
次に配列かどうかをClass#isArrayメソッドを呼び出して判定します。配列と判定した場合は、個々の要素をArray#getで取得してtoString(Object,int)を呼び出します。この時、2番目の引数(呼び出しの深さ)には、現在の深さに1を加えたものを与えています。なお呼び出しの深さ引数を実際に利用するのはフィールドを文字列化する時です。
最後にembededフィールドにパラメータとして与えられたオブジェクトと文字列化に利用したクラスが登録済みかをチェックして、既に文字列化が完了しているかを調べます。このプログラムはメソッドを再帰的に呼び出すため、もし途中で文字列化処理中のオブジェクトが出てくると無限にループして、最終的にはStackOverflowErrorになってしまいます。また、仮に文字列化中でなくても出力が冗長になるため、この検証は必要です。ここで利用しているEmbededKeyクラスはコンストラクタへ与えられたクラスとオブジェクトを覚えておき、equalsメソッドで同一のクラスとオブエジェクトの組み合わせかどうかを比較するクラスです。なお、ここで文字列化に利用したクラスかどうかを確認するのは、継承元のクラスを文字列化する場合に再度同じオブジェクトが与えられるからです。
ここまででプリミティブ型でもObjectクラスのオブジェクトでも配列でも既に文字列化済みでも無かった場合には、未知のオブジェクトの出現と判断してオブジェクトの文字列化処理を呼び出します。
未知のオブジェクトの文字列化
String dumpObject(Class c, Object o, int level) { assert o != null && !c.isArray() && !c.isPrimitive() && !CharSequence.class.isInstance(c); try { Method m = c.getMethod("toString", new Class[0]); if (!TOSTRING.equals(m)) { return o.toString(); } } catch (NoSuchMethodException e) { // never return o.toString(); } embeded.put(new EmbededKey(c, o), o.toString()); StringBuffer buffer = new StringBuffer(); buffer.append(o.toString()); buffer.append(dumpFields(c, o, level)); c = c.getSuperclass(); if (c != null && c != Object.class) { buffer.append(LF); buffer.append(dumpValue(c, o, level)); } return new String(buffer); }
dumpObjectメソッドは、処理6~7.に該当するメソッドです。
最初のassert文は、宣言的にメソッドの呼び出し条件を記述したものです。assertは実行時にjavaコマンドに-eaオプションを与えることで偽の場合にAssertionErrorをスローさせることもできますが、通常は無効にしておいてあくまでもソースコード上にメソッドの呼び出し条件などを記述するために利用するものです。ここではdumpObjectメソッドは、直前のdumpValueメソッドからしか呼ばれないため、assertで示した条件が成り立っていなければおかしいということを示しています。
次に、与えられたクラスがObject#toStringをオーバーライドしているかどうかを調べます。もしオーバーライドしていれば、そのtoStringメソッドを呼び出します。この処理を実行するためにはクラスのtoStringメソッドのオブジェクトを取得しなければなりません。Methodクラスのオブジェクトを取得するにはClass#getMethodを利用します。getMethodメソッドは指定した名前/引数のクラスが存在すればMethodクラスのオブジェクトを返すメソッドです。ただし見つからなかった場合には、NoSuchMethodException(チェック例外)がスローされるため、try節で囲っています。なお、Object#toStringは必ず存在するため、ここで例外がスローされる可能性は実際にはありません。またここで比較に用いているTOSTRING定数には、あらかじめstaticイニシャライザで取得したObject#toStringのMethodオブジェクトを格納してあります。
次に指定されたオブジェクトの文字列化に取りかかる前にembededフィールドにクラスとオブジェクトの組を登録します。ちなみに、dumpObjectメソッドの先頭で登録しないのは、いずれにしろObject#toStringの結果を利用するため、オーバーライドしている場合には()内に囲って出力する意味があまり感じられないからです。また、ここより後の時点で登録すると次のように自分自身の参照を持つオブジェクトが出現した場合に無限ループとなるため、この時点での登録が必然です。
public Looper { Looper me; public Looper() { me = this; } }
次にパラメータで与えられたクラスで定義された個々のフィールドを文字列化します。最後にクラスの継承関係を親の方向へ向かって再帰的にdumpValueを呼び出します。Class#getSuperClassは文字通りスーパークラス(派生元クラス)のClassオブジェクトを取得するためのメソッドです。なお、dumpValueメソッドで説明したように、getSuperClassメソッドの呼び出し結果がObjectクラスとなった時点でdumpValueメソッドの呼び出しを終了します。
各フィールドの文字列化
String dumpFields(Class c, Object o, int level) { String indent = createIndent(level); StringBuffer buffer = new StringBuffer(); Field[] f = c.getDeclaredFields(); for (int i = 0; i < f.length; i++) { buffer.append(LF); buffer.append(indent).append(f[i].getName()).append('='); if (!f[i].isAccessible()) { changeAccessible(f[i], true); } try { buffer.append(dumpValue(f[i].getType(), f[i].get(o), level + 1)); } catch (IllegalAccessException e) { buffer.append("EACCESS"); } } return new String(buffer); }
dumpFieldsは処理8.に該当するメソッドです。
最初にインデント幅を第3パラメータから得ています。
ここで呼び出しているcreateIndentメソッドはインデント分のスペースを持つ文字列を返すメソッドです(後述)。
次にClass#getDeclaredFieldsメソッドを呼び出して、このクラスで定義したすべてのフィールドを取得します。getDeclaredFieldsメソッドはFieldクラスのオブジェクトの配列を返すメソッドです。Fieldクラスのオブジェクトを利用するとフィールドの名前の取得やフィールドの内容の取得/設定ができます。
途中で呼び出しているField#isAccessibleメソッドはフィールドのアクセス指定子に従って呼び出し元のアクセス可能性を検証するメソッドです。たとえばprivateフィールドであれば、他のクラスからのisAccessibleメソッド呼び出しには偽が返ります。とは言え、DumpObjectはオブジェクトの内容をログしたりデバッグ用に出力したりするプログラムですからprivateかpublicかという設計に依存したアクセス指定子に影響されては逆に困ります。そのため、アクセス権を変更してフィールドの内容を取得してdumpValueメソッドを利用して文字列化します。ここで呼び出しているField#getTypeメソッドはフィールドの型をClassクラスのオブジェクトで取得するメソッドで、Field#getメソッドは与えたオブジェクトの該当フィールドの値を取得するメソッドです。なお、アクセス権の変更に失敗するとField#getメソッドによってフィールドの内容を取得することはできず、代わりにIllegalAccessExceptionがスローされます。このプログラムではその場合にはアクセス失敗の意味で「EACCESS」という文字列をフィールドの値として設定するようにしています。
なお、アクセス権を変更した後に元に戻すかどうかはちょっと微妙な問題です。一般論としてはDumpObjectのようなユーティリティの呼び出し前後でオブジェクトの状態が変わることは望ましくないので元のアクセス権に直しておくべきですが、あまり意味が無い処理だというのも事実です。そのため、ここでは元に戻していません。
ソースコードのzip内に含まれているDumpObjectSecurityTestのようにSecurityManagerを設定してかつセキュリティポリシーとしてReflectionアクセスを特に認めていない場合にはいずれにしろアクセス権の変更はできません(実行すると「EACCESS」と表示されますので実際に試してみてください)。また、Reflectionを利用しない通常のプログラムではコンパイル時に検証されたアクセス権がそのまま利用されます。すなわち、セキュリティ設定でReflectionによるアクセス権の変更を許可している場合には、いずれにしろReflectionを利用するアクセスは可能であるため戻しても意味がなく、あらかじめ静的なアクセス権にしたがってコンパイルされたプログラムではアクセス権が存在しないフィールドへアクセスするコードは含まれていないため、やはり元のアクセス権に戻さなくても安全だということです。
ちなみに、ここでアクセス権の変更のために呼び出しているchangeAccessibleメソッドのソースを以下に示します。
void changeAccessible(Field f, boolean tobe) { try { f.setAccessible(tobe); } catch (SecurityException e) { } }
Field#setAccessibleメソッドにtrueを与えるとアクセスが可能になり、falseを与えるとアクセスできなくなります。また、アクセス権の変更がセキュリティポリシーで認められていない場合にはSeccurityExceptionがスローされます。
インデント用文字列の作成処理
Javaには文字と数字の指定によるStringのコンストラクタが存在しません。
これがC#であればnew String(' ', 8);のような記述で8個の空白の文字列が作成できますし、VBであればString(" ", 8)、C++ならばnew std::string(8, ' ');、Rubyならば" " * 8というように多くのプログラミング言語で同一文字の連続による文字列というのは比較的簡単に作成できるのでなんとなく奇妙な感じです。
DumpObjectではインデント用として複数の空白から構成される文字列を作成するために次のメソッドを利用しています。
String createIndent(int level) { char[] c = new char[level * INDENT_WIDTH]; Arrays.fill(c, ' '); return new String(c); }
同一文字の連続からなる文字列の作成にはもっと簡潔な方法があるかも知れませんが、筆者には現時点ではここで示した文字配列を利用した作成方法が一番良さそうに思えます。
まとめ
本記事ではReflectionが提供する情報取得機能を利用して、オブジェクトのフィールドを文字列化するプログラムを紹介しました。
JavaではReflectionを利用して実行時に未知のオブジェクトの内容を取得することができます。これにより、個々のクラスにtoStringのようなメソッドを実装しなくてもフレームワークやユーティリティによって代替機能を提供することが可能となります。
なお本記事のサンプルソースは自由に使ってかまいません。いろいろ試してJavaの学習などに利用してみてください。
参考資料
- J2SE API ドキュメント
その他の情報
JakartaプロジェクトのCommons-Langには、本記事で紹介したプログラムと同様なReflectionToStringBuilderというReflectionを利用したtoStringメソッドの実装ヘルパーがあります。ちなみにReflectionToStringBuilderでは記事中で少し触れた複数のスレッドからの同時呼び出しの問題をThreadLocalオブジェクトを利用することで解決しています。参照してみるとおもしろいかも知れません。
謝辞
角谷さんにReflectionToStringBuilderの情報を提供していただきました。どうもありがとうございます。
