■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 2009年11月22日 Java総合講座 - 初心者から達人へのパスポート vol.180 セルゲイ・ランダウ バックナンバー: http://www.flsi.co.jp/Java_text/ ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ------------------------------------------------------- ・現在、このメールマガジンは以下の2部構成になっています。 [1] 当初からのコース:毎週日曜の深夜に発行 これは現在、中級レベルになっています。 [2] 2009年11月開講コース:毎週水曜の深夜に発行 これは現在、初心者向けのレベルになっています。 ・このメールマガジンは、画面を最大化して見てください。 小さな画面で見ていると、不適切な位置で行が切れてしまう など、問題を起すことがあります。 ・このメールマガジンに掲載されているソース・コード及び 文章は特に断らない限り、すべて筆者が著作権を所有してい ます。また、これらのソース・コードは学習用のためだけに 提供しているものです。 ------------------------------------------------------- ======================================================== ◆ 01.ログ出力のための技術(続き) ======================================================== では、まずEclipseを起動しておいて下さい。 (1) 「パッケージ・エクスプローラー」の中のReflectionTest(プロジェクト名)配下 のsrcを右クリックし、「新規」→「パッケージ」を選択します。 (2) 「新規Javaパッケージ」ウインドウにおいて、「名前」欄に jp.co.flsi.lecture.reflect と入力し、「完了」ボタンをクリックします。 (3) このjp.co.flsi.lecture.reflectパッケージの配下に ReflectionToStringMaker というクラスを作成し、下記のようなソース・コードに編集しましょう。 -------------------------------------------------------- ・ ・ ・ (このソース・コードは都合により廃止します。代わりにvol.190の ValueLogStringMakerを参照して下さい。) ・ ・ ・ -------------------------------------------------------- (4) 前回作成したjp.co.flsi.lecture.test.reflectionパッケージの配下に ReflectionToStringMakerTest というクラスを作成し、下記のようなソース・コードに編集しましょう。 (これは文字通りReflectionToStringMakerをテストするためのクラスです。) -------------------------------------------------------- package jp.co.flsi.lecture.test.reflection; import jp.co.flsi.lecture.reflect.ReflectionToStringMaker; public class ReflectionToStringMakerTest { /** * @param args */ public static void main(String[] args) { School school = new School(); Teacher teacher = new Teacher(); teacher.setName("佐々貴子"); teacher.setSubject("英語"); school.setTeacher(teacher); Student student1 = new Student(); student1.setName("聖都一郎"); student1.setAge(11); school.addStudent(student1); Student student2 = new Student(); student2.setName("聖都二郎"); student2.setAge(12); school.addStudent(student2); ReflectionToStringMaker stringMaker = new ReflectionToStringMaker(); System.out.println("[ログ出力]\n" + stringMaker.getFields(school)); } } -------------------------------------------------------- 編集が終わったら、ファイルをすべて保管した後、ReflectionToStringMakerTestを 実行してみて下さい。 前回と同じような結果になりますね。ただし、出力の様式はちょっと変えてありますが。 試しに、Teacherクラス、Studentクラス、Schoolクラスからそれぞれ toString()メソッドを削除しても同じ実行結果になることを確認して下さい。 このようにユーティリティー的なクラスを用意しておけば、前回のようなtoString() メソッドを各クラスに組み込む必要もありませんし、またpublicなgetメソッドを 持つフィールドだけをログ出力させることができますので、セキュリティー上も 問題がありません。 リフレクションの機能を使って、自動的に各オブジェクトのフィールドに対する getメソッドを調べ上げ、それらを片っ端から呼び出して値を取り出すので、未知の クラスであっても、オブジェクトのデータを自動的に取り出してくれます。 なお、上記のReflectionToStringMakerのソース・コードでは、publicなgetメソッ ドを持つフィールド(要するにJavaBeansにおけるプロパティー)のみを出力の対象 とし、 public int age = 0; のような、フィールド自体がpublicでgetメソッドを持たない場合の処理はしていま せんので、もしそのような処理が必要な場合はもう少しロジックを追加する必要が あります。 ただし、そもそも、そのように変数自体にpublic指定することは好ましくなく、 JavaBeansのルールに従えば、フィールド(プロパティー)の変数自体は非公開に して、公開するにはpublicなgetメソッドを用意すべきです。 (ただし、 public static final int UPPERLIMIT = 100; のように、定数として一般公開するフィールドの場合は例外です。そもそも定数は 予め値がわかっているので、ログ出力する必要はありません。) ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ところで、このリフレクションを使った方法は、普通にフィールドやgetメソッドを 呼び出して値を取り出す場合に比べて処理に随分時間がかかります(あるベンチマー ク・テストの結果では、普通にメソッドを呼び出す場合に比べて50倍〜1000倍以上の 時間がかかることがわかっています)。 よって、このリフレクションを使った方法を何度も繰り返すとパフォーマンス(性能) を大きく落とすことになります。 (ReflectionToStringBuilderも内部でリフレクションを使用しているので同じこと が言えます。前回、ReflectionToStringBuilderのお話の最後に 「(それに、もう一つ大きな問題がありますので、後ほどお話しします。)」 と書いたのは、このパフォーマンスの問題のことなのです。) したがって、迅速さを要求されるアプリケーションや頻繁に呼び出されるメソッド の中では、リフレクションの使用は避けるべきです。 ^^^^^^^^^^^^^^^^^^^ では、どうするかと言うと、リフレクションを使ったやり方は直接アプリケーションの 中で呼び出すのではなく、対象となる引数や戻り値のデータのログ出力を行う部分の ソース・コードを自動的に生成するツールを作り、そのツールの中でリフレクションの 機能を利用するようにするのです。そうすれば、そのツールはリフレクションを使って も、ツールが生成したソース・コードの中ではリフレクションは使用されないので、 アプリケーションのパフォーマンスが落ちることはありません。 その具体的なやり方は、また後に上級コースでお話することとして、今後しばらくの間は、 ログ出力には上記のReflectionToStringMakerを利用していきます(話を簡単にするため です)。 (現実のプロジェクトでは、このReflectionToStringMakerは使わないように注意して 下さい。後の上級コースで自動化のツールを学んでから使用するようにして下さい。) ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ では、ReflectionToStringMakerのソース・コードを簡単に解説しておきましょう。 まず、15行目の return buffer.toString(); ですが、このようにStringBufferのtoString()メソッドを呼び出すと、その文字列 データがString型のオブジェクトに変換されて返されます。(つまり、StringBuffer はObjectのtoString()メソッドをオーバーライドしています。同様にStringの toString()メソッドもObjectのtoString()メソッドをオーバーライドしています。) 次に19行目の Class clazz = object.getClass(); ですが、ここでClassのインスタンスの変数名をclazzにしているのは、classという 名前は予約語(クラスを表すキーワード)になっていて変数としては使用できない からです。つまりclassと書きたいところをsをzで代用してclazzと書いたのです。 ここで、このClassという名前のクラスは、今まで何度か出てきた(一番最初はvol.023 で出てきた)ように、クラスを管理するクラスで、一般には「メタ・クラス」と呼ばれる ものです。このClassというクラスのインスタンスは各クラスを表し、各クラスに関する 情報を保持します。 ここでは、objectという引数のgetClass()メソッドを呼び出すことによってClassクラス のインスタンスを取り出していますが、このgetClass()メソッドは(すべてのオブジェクト のスーパークラスである)Objectのメソッドで、そのオブジェクト(ここではobjectと いう変数で表されるインスタンス)が何クラスのインスタンスであるかを調べて、その インスタンスのクラスのClassオブジェクト(Classクラスのインスタンス)を返して くれます。 そうすると、そのClassオブジェクト(ここではclazzというインスタンス)を使って objectのクラスに関する情報を取り出すことが可能になります。 その次の20行目の buffer.append(clazz.getName()); では、さっそくClassオブジェクトのgetName()メソッドを呼び出すことによって、 objectのクラス名を取り出しています。 ここのappend()というメソッドはStringBufferの文字列データの最後に、引数で指定し た文字列を追加するメソッドで、ここではobjectのクラス名を最後に追加しています。 21行目の buffer.append("@" + Integer.toHexString(object.hashCode()) + "[\n"); では、StringBufferのデータの最後にさらに文字列を追加していますが、ここで、 hashCode()というメソッドはそのオブジェクトのハッシュコードを取り出す、Object のメソッドです。ここではさらにInteger.toHexString()メソッドを使うことによって ハッシュコードを16進数表示に変換しています。 続いて22行目の Field[] fieldList = clazz.getDeclaredFields(); では、ClassオブジェクトのgetDeclaredFields()メソッドを呼び出すことによって、 objectのクラスに宣言(定義)されているフィールドの情報を取り出しています。 ここで、Fieldというクラスはリフレクションのパッケージ(java.lang.reflect)に 含まれるクラスで、フィールドの情報を管理するクラスです。 getDeclaredFields()メソッドはField型の配列という形式で複数のフィールドの 情報を返します。 続いて24行目の appendProperty(field.getName(), field.getType().getName(), object); では、その下で定義されているappendProperty()メソッドを呼び出していますが、その 引数に指定しているfield.getName()は、FieldオブジェクトのgetName()メソッドを 呼び出すことによってフィールドの名前(変数名)を取り出しています。 また、field.getType().getName()は、FieldオブジェクトのgetType()メソッドを呼び 出すことによってフィールドの型(getType()メソッドはClassオブジェクトを返すこと によってフィールドの型の情報を返す)を取り出し、そのClassオブジェクトのgetName() メソッドを呼び出すことによって型の名前を取り出しています。 続いて29行目以降では、appendProperty()メソッドを定義していますが、この メソッドは、publicなgetメソッドを持つフィールドのみを処理対象としており、 いわゆるJavaBeansのプロパティーのみを対象としているのでappendProperty() というメソッド名にしました。 33〜38行目の if (fieldType.equals("boolean")) { getterMethodName = "is" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); } else { getterMethodName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); } では、フィールド名からgetメソッド(ゲッター・メソッド)の名前を組み立てて いますが、JavaBeansの標準のルールに従っています。 つまり、フィールド名の先頭の文字を大文字にして、通常のフィールドの場合は、 その前にgetをつけ、boolean型のフィールドの場合には、その前にisをつけて、 メソッドの名前としています。 ここで、charAt(0)というメソッドは、Stringの先頭(0番目)の文字(char型)を 取り出すメソッドで、Character.toUpperCase()は引数に指定した文字を大文字に 変換するメソッドです。 また、substring(1)というメソッドはStringの2番目以降の文字列を返すメソッド です(引数に指定した数字はインデックスで、これが0なら1番目すなわち先頭、1なら 2番目を意味する。charAt()メソッドの引数も同様)。 続いて41行目の method = object.getClass().getMethod(getterMethodName, new Class[0]); は、ClassオブジェクトのgetMethod()メソッドを呼び出すことによって、第一引数に 指定した名前のメソッドの情報を取り出しています。メソッドの情報はMethod型の オブジェクトという形式で返されます。 なお、getMethod()メソッドの第二引数には情報を取り出したいメソッドの引数の 型を配列の形式で指定しますが、ここでは標準的なgetメソッドが引数無しである ため、空の配列を指定しています。 なお、Methodクラスもリフレクションのパッケージ(java.lang.reflect)に 含まれるクラスで、メソッドの情報を管理するクラスです。 このメソッドを呼び出したとき、該当するメソッドが存在しない(そのClassオブジェ クトが表すクラスに定義されていない)場合はNoSuchMethodExceptionという例外 が投げられますのでそれを44〜47行目の } catch (NoSuchMethodException e) { // getメソッドのないものは無視する。 return; } で処理しています。 ここでは、getメソッドのないフィールドは無視することにしているので、たんに returnしているだけです。 50行目の result = method.invoke(object, new Object[0]); では、このMethodオブジェクトに実際にメソッドを実行させています。 その実行結果はObject型のオブジェクト(ここではresultという変数を割り当てて いる)として返されます。 なお、このinvoke()メソッドの第二引数には実行したいメソッドに渡す引数を配列 の形式で指定しますが、ここでは引数がないため、空の配列を渡しています。 ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ (次回に続く) では、今日はここまでにします。 ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ★ホームページ: http://www.flsi.co.jp/Java_text/ ★このメールマガジンは 「まぐまぐ(http://www.mag2.com)」 を利用して発行しています。 ★バックナンバーは http://www.flsi.co.jp/Java_text/ にあります。 ★このメールマガジンの登録/解除は下記Webページでできます。 http://www.mag2.com/m/0000193915.html ★このメールマガジンへの質問は下記Webページにて受け付けて います。わからない所がありましたら、どしどしと質問をお寄 せください。 http://www.flsi.co.jp/Java_text/ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Copyright (C) 2009 Future Lifestyle Inc. 不許無断複製 |