はじめに
例外が起きてログを取る場合やデバッグ中のオブジェクトの状態を知りたい場合に、そのオブジェクトの参照をロガーや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の情報を提供していただきました。どうもありがとうございます。