Javaリフレクションガイド

Java Reflectionガイド

1. 概要

この記事では、クラス、インターフェース、フィールド、メソッドの実行時属性を検査または変更できるJavaリフレクションについて説明します。 これは、コンパイル時に名前がわからない場合に特に便利です。

さらに、新しいオブジェクトをインスタンス化し、メソッドを呼び出し、リフレクションを使用してフィールド値を取得または設定できます。

2. プロジェクトのセットアップ

To use java reflection, we do not need to include any special jars、特別な構成またはMavenの依存関係。 JDKには、この目的のためにjava.lang.reflectパッケージにバンドルされているクラスのグループが付属しています。

そのため、次のインポートをコードにインポートするだけです。

import java.lang.reflect.*;

そして、私たちは行ってもいいです。

インスタンスのクラス、メソッド、およびフィールド情報にアクセスするために、オブジェクトのランタイムクラス表現を返すgetClassメソッドを呼び出します。 返されたclassオブジェクトは、クラスに関する情報にアクセスするためのメソッドを提供します。

3. 簡単な例

足を濡らすために、実行時に単純なJavaオブジェクトのフィールドを検査する非常に基本的な例を見てみましょう。

nameフィールドとageフィールドのみを含み、メソッドをまったく含まない単純なPersonクラスを作成しましょう。 Personクラスは次のとおりです。

public class Person {
    private String name;
    private int age;
}

ここで、Javaリフレクションを使用して、このクラスのすべてのフィールドの名前を検出します。 反射の力を理解するために、Personオブジェクトを作成し、参照型としてObjectを使用します。

@Test
public void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() {
    Object person = new Person();
    Field[] fields = person.getClass().getDeclaredFields();

    List actualFieldNames = getFieldNames(fields);

    assertTrue(Arrays.asList("name", "age")
      .containsAll(actualFieldNames));
}

このテストは、オブジェクトへの参照がそのオブジェクトの親タイプである場合でも、personオブジェクトからFieldオブジェクトの配列を取得できることを示しています。

上記の例では、これらのフィールドの名前にのみ関心がありましたが、さらに多くのことができます。これについては、以降のセクションでさらに例を参照します。

ヘルパーメソッドを使用して実際のフィールド名を抽出する方法に注目してください。これは非常に基本的なコードです。

private static List getFieldNames(Field[] fields) {
    List fieldNames = new ArrayList<>();
    for (Field field : fields)
      fieldNames.add(field.getName());
    return fieldNames;
}

4. Javaリフレクションのユースケース

Javaリフレクションのさまざまな機能に進む前に、Javaリフレクションの一般的な用途について説明します。 Javaのリフレクションは非常に強力であり、さまざまな方法で非常に役立ちます。

たとえば、多くの場合、データベーステーブルには命名規則があります。 学生データを含むテーブルがtbl_student_dataと呼ばれるように、テーブル名の前にtbl_を付けることにより、一貫性を追加することを選択できます。

このような場合、学生データを保持するJavaオブジェクトにStudentまたはStudentData.という名前を付けることができます。次に、CRUDパラダイムを使用して、Create操作のみが受信するように操作ごとに1つのエントリポイントがあります。 Objectパラメータ。

次に、リフレクションを使用して、オブジェクト名とフィールド名を取得します。 この時点で、このデータをDBテーブルにマップし、オブジェクトフィールド値を適切なDBフィールド名に割り当てることができます。

5. Javaクラスの検査

このセクションでは、JavaリフレクションAPIの最も基本的なコンポーネントについて説明します。 前述したように、javaクラスのオブジェクトを使用すると、オブジェクトの内部詳細にアクセスできます。

オブジェクトのクラス名、それらの修飾子、フィールド、メソッド、実装されたインターフェイスなどの内部の詳細を調べます。

5.1. 準備をする

Javaクラスに適用され、さまざまな例があるリフレクションAPIをしっかりと把握するために、Eatingインターフェイスを実装する抽象Animalクラスを作成します。 このインターフェースは、作成する具体的なAnimalオブジェクトの摂食行動を定義します。

まず、Eatingインターフェイスは次のとおりです。

public interface Eating {
    String eats();
}

次に、Eatingインターフェイスの具体的なAnimal実装:

public abstract class Animal implements Eating {

    public static String CATEGORY = "domestic";
    private String name;

    protected abstract String getSound();

    // constructor, standard getters and setters omitted
}

動物の動きを説明するLocomotionという別のインターフェースも作成しましょう。

public interface Locomotion {
    String getLocomotion();
}

ここで、Animalを拡張し、Locomotionを実装するGoatという具象クラスを作成します。 スーパークラスはEatingを実装するため、Goatはそのインターフェイスのメソッドも実装する必要があります。

public class Goat extends Animal implements Locomotion {

    @Override
    protected String getSound() {
        return "bleat";
    }

    @Override
    public String getLocomotion() {
        return "walks";
    }

    @Override
    public String eats() {
        return "grass";
    }

    // constructor omitted
}

これ以降、Javaリフレクションを使用して、上記のクラスおよびインターフェイスに表示されるJavaオブジェクトの側面を検査します。

5.2. クラス名

Classからオブジェクトの名前を取得することから始めましょう:

@Test
public void givenObject_whenGetsClassName_thenCorrect() {
    Object goat = new Goat("goat");
    Class clazz = goat.getClass();

    assertEquals("Goat", clazz.getSimpleName());
    assertEquals("com.example.reflection.Goat", clazz.getName());
    assertEquals("com.example.reflection.Goat", clazz.getCanonicalName());
}

ClassgetSimpleNameメソッドは、宣言に表示されるオブジェクトの基本名を返すことに注意してください。 次に、他の2つのメソッドは、パッケージ宣言を含む完全修飾クラス名を返します。

完全修飾クラス名しかわからない場合に、Goatクラスのオブジェクトを作成する方法も見てみましょう。

@Test
public void givenClassName_whenCreatesObject_thenCorrect(){
    Class clazz = Class.forName("com.example.reflection.Goat");

    assertEquals("Goat", clazz.getSimpleName());
    assertEquals("com.example.reflection.Goat", clazz.getName());
    assertEquals("com.example.reflection.Goat", clazz.getCanonicalName());
}

静的forNameメソッドに渡す名前にはパッケージ情報が含まれている必要があることに注意してください。含まれていない場合、ClassNotFoundExceptionが取得されます。

5.3. クラス修飾子

Integer.を返すgetModifiersメソッドを呼び出すことにより、クラスで使用されている修飾子を判別できます。各修飾子は、設定またはクリアされるフラグビットです。

java.lang.reflect.Modifierクラスは、返されたIntegerを分析して、特定の修飾子の有無を調べる静的メソッドを提供します。

上で定義したいくつかのクラスの修飾子を確認しましょう。

@Test
public void givenClass_whenRecognisesModifiers_thenCorrect() {
    Class goatClass = Class.forName("com.example.reflection.Goat");
    Class animalClass = Class.forName("com.example.reflection.Animal");

    int goatMods = goatClass.getModifiers();
    int animalMods = animalClass.getModifiers();

    assertTrue(Modifier.isPublic(goatMods));
    assertTrue(Modifier.isAbstract(animalMods));
    assertTrue(Modifier.isPublic(animalMods));
}

プロジェクトにインポートするライブラリjarにあるクラスの修飾子を検査できます。

ほとんどの場合、本格的なインスタンス化ではなく、forNameアプローチを使用する必要があります。これは、メモリの多いクラスの場合、コストのかかるプロセスになるためです。

5.4. パッケージ情報

Javaリフレクションを使用することにより、クラスまたはオブジェクトのパッケージに関する情報も取得できます。 このデータは、クラスオブジェクトのgetPackageメソッドの呼び出しによって返されるPackageクラス内にバンドルされます。

テストを実行してパッケージ名を取得します。

@Test
public void givenClass_whenGetsPackageInfo_thenCorrect() {
    Goat goat = new Goat("goat");
    Class goatClass = goat.getClass();
    Package pkg = goatClass.getPackage();

    assertEquals("com.example.reflection", pkg.getName());
}

5.5. スーパークラス

また、Javaリフレクションを使用して、任意のJavaクラスのスーパークラスを取得することもできます。

多くの場合、特にライブラリクラスまたはJavaの組み込みクラスを使用しているときは、使用しているオブジェクトのスーパークラスが事前にわからない場合があります。このサブセクションでは、この情報を取得する方法を示します。

それでは、先に進んでGoat,のスーパークラスをさらに決定しましょう。また、java.lang.Stringクラスがjava.lang.Objectクラスのサブクラスであることも示します。

@Test
public void givenClass_whenGetsSuperClass_thenCorrect() {
    Goat goat = new Goat("goat");
    String str = "any string";

    Class goatClass = goat.getClass();
    Class goatSuperClass = goatClass.getSuperclass();

    assertEquals("Animal", goatSuperClass.getSimpleName());
    assertEquals("Object", str.getClass().getSuperclass().getSimpleName());
}

5.6. 実装されたインターフェース

Javaリフレクションを使用して、get the list of interfaces implemented by a given classを実行することもできます。

GoatクラスとAnimal抽象クラスによって実装されたインターフェースのクラスタイプを取得してみましょう。

@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect(){
    Class goatClass = Class.forName("com.example.reflection.Goat");
    Class animalClass = Class.forName("com.example.reflection.Animal");

    Class[] goatInterfaces = goatClass.getInterfaces();
    Class[] animalInterfaces = animalClass.getInterfaces();

    assertEquals(1, goatInterfaces.length);
    assertEquals(1, animalInterfaces.length);
    assertEquals("Locomotion", goatInterfaces[0].getSimpleName());
    assertEquals("Eating", animalInterfaces[0].getSimpleName());
}

アサーションから、各クラスは単一のインターフェースのみを実装することに注意してください。 これらのインターフェイスの名前を調べると、コードに表示されているとおり、GoatLocomotionを実装し、AnimalEatingを実装していることがわかります。

Goatは抽象クラスAnimalのサブクラスであり、インターフェイスメソッドeats()を実装し、次にGoatEatingインターフェイスを実装していることに気付いたかもしれません。

したがって、クラスがimplementsキーワードで実装されていると明示的に宣言しているインターフェイスのみが、返される配列に表示されることに注意してください。

したがって、スーパークラスがそのインターフェイスを実装しているためにクラスがインターフェイスメソッドを実装していても、サブクラスがimplementsキーワードを使用してそのインターフェイスを直接宣言していない場合でも、そのインターフェイスはインターフェイスの配列に表示されません。

5.7. コンストラクター、メソッド、およびフィールド

Javaリフレクションを使用すると、オブジェクトのクラスのコンストラクター、およびメソッドとフィールドを検査できます。

後で、クラスのこれらの各コンポーネントをさらに詳しく調べることができますが、今のところは、名前を取得して、予想と比較するだけで十分です。

Goatクラスのコンストラクターを取得する方法を見てみましょう。

@Test
public void givenClass_whenGetsConstructor_thenCorrect(){
    Class goatClass = Class.forName("com.example.reflection.Goat");

    Constructor[] constructors = goatClass.getConstructors();

    assertEquals(1, constructors.length);
    assertEquals("com.example.reflection.Goat", constructors[0].getName());
}

次のように、Animalクラスのフィールドを検査することもできます。

@Test
public void givenClass_whenGetsFields_thenCorrect(){
    Class animalClass = Class.forName("com.example.java.reflection.Animal");
    Field[] fields = animalClass.getDeclaredFields();

    List actualFields = getFieldNames(fields);

    assertEquals(2, actualFields.size());
    assertTrue(actualFields.containsAll(Arrays.asList("name", "CATEGORY")));
}

Animalクラスのメソッドを検査できるのと同じように:

@Test
public void givenClass_whenGetsMethods_thenCorrect(){
    Class animalClass = Class.forName("com.example.java.reflection.Animal");
    Method[] methods = animalClass.getDeclaredMethods();
    List actualMethods = getMethodNames(methods);

    assertEquals(4, actualMethods.size());
    assertTrue(actualMethods.containsAll(Arrays.asList("getName",
      "setName", "getSound")));
}

getFieldNamesと同様に、Methodオブジェクトの配列からメソッド名を取得するためのヘルパーメソッドを追加しました。

private static List getMethodNames(Method[] methods) {
    List methodNames = new ArrayList<>();
    for (Method method : methods)
      methodNames.add(method.getName());
    return methodNames;
}

6. コンストラクターの検査

Javaリフレクションを使用すると、任意のクラスのinspect constructors、さらにはcreate class objects at runtimeを使用できます。 これは、java.lang.reflect.Constructorクラスによって可能になります。

以前は、Constructorオブジェクトの配列を取得する方法のみを確認し、そこからコンストラクターの名前を取得できました。

このセクションでは、特定のコンストラクターを取得する方法に焦点を当てます。 私たちが知っているように、javaでは、クラスの2つのコンストラクターがまったく同じメソッドシグネチャを共有することはありません。 したがって、この一意性を使用して、多くのコンストラクターから1つのコンストラクターを取得します。

このクラスの機能を理解するために、3つのコンストラクターを使用してAnimalBirdサブクラスを作成します。 Locomotionを実装しないので、コンストラクター引数を使用してその動作を指定し、さらに多様性を追加できます。

public class Bird extends Animal {
    private boolean walks;

    public Bird() {
        super("bird");
    }

    public Bird(String name, boolean walks) {
        super(name);
        setWalks(walks);
    }

    public Bird(String name) {
        super(name);
    }

    public boolean walks() {
        return walks;
    }

    // standard setters and overridden methods
}

このクラスには3つのコンストラクターがあることを振り返って確認しましょう。

@Test
public void givenClass_whenGetsAllConstructors_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Constructor[] constructors = birdClass.getConstructors();

    assertEquals(3, constructors.length);
}

次に、コンストラクターのパラメータークラスタイプを宣言された順序で渡すことにより、Birdクラスの各コンストラクターを取得します。

@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect(){
    Class birdClass = Class.forName("com.example.reflection.Bird");

    Constructor cons1 = birdClass.getConstructor();
    Constructor cons2 = birdClass.getConstructor(String.class);
    Constructor cons3 = birdClass.getConstructor(String.class, boolean.class);
}

指定された順序で指定されたパラメータータイプを持つコンストラクターが存在しない場合、NoSuchMethodExceptionを取得し、テストが自動的に失敗するため、アサーションは必要ありません。

最後のテストでは、パラメーターを提供しながら実行時にオブジェクトをインスタンス化する方法を確認します。

@Test
public void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Constructor cons1 = birdClass.getConstructor();
    Constructor cons2 = birdClass.getConstructor(String.class);
    Constructor cons3 = birdClass.getConstructor(String.class,
      boolean.class);

    Bird bird1 = (Bird) cons1.newInstance();
    Bird bird2 = (Bird) cons2.newInstance("Weaver bird");
    Bird bird3 = (Bird) cons3.newInstance("dove", true);

    assertEquals("bird", bird1.getName());
    assertEquals("Weaver bird", bird2.getName());
    assertEquals("dove", bird3.getName());

    assertFalse(bird1.walks());
    assertTrue(bird3.walks());
}

ConstructorクラスのnewInstanceメソッドを呼び出し、宣言された順序で必要なパラメーターを渡すことにより、クラスオブジェクトをインスタンス化します。 次に、結果を必要な型にキャストします。

bird1の場合、Birdコードから自動的に名前をbirdに設定するデフォルトのコンストラクターを使用し、テストでそれを確認します。

次に、名前とテストのみを使用してbird2をインスタンス化します。最後の2つのアサーションに見られるように、デフォルトでfalseに移動動作を設定しない場合は、覚えておいてください。

7. フィールドの検査

以前は、フィールドの名前のみを検査しました。このセクションでは、we will show how toget and set their values at runtimeです。

実行時にクラスのフィールドを検査するために使用される主なメソッドは、getFields()getField(fieldName)の2つです。

getFields()メソッドは、問題のクラスのアクセス可能なすべてのパブリックフィールドを返します。 クラスとすべてのスーパークラスの両方のすべてのパブリックフィールドを返します。

たとえば、Birdクラスでこのメソッドを呼び出すと、Bird自体が宣言していないため、スーパークラスAnimalCATEGORYフィールドのみが取得されます。パブリックフィールド:

@Test
public void givenClass_whenGetsPublicFields_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Field[] fields = birdClass.getFields();

    assertEquals(1, fields.length);
    assertEquals("CATEGORY", fields[0].getName());
}

このメソッドには、フィールドの名前を取得して1つのFieldオブジェクトのみを返すgetFieldというバリアントもあります。

@Test
public void givenClass_whenGetsPublicFieldByName_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Field field = birdClass.getField("CATEGORY");

    assertEquals("CATEGORY", field.getName());
}

スーパークラスで宣言され、子クラスで宣言されていないプライベートフィールドにはアクセスできません。 これが、nameフィールドにアクセスできない理由です。

ただし、getDeclaredFieldsメソッドを呼び出すことにより、処理しているクラスで宣言されたプライベートフィールドを検査できます。

@Test
public void givenClass_whenGetsDeclaredFields_thenCorrect(){
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Field[] fields = birdClass.getDeclaredFields();

    assertEquals(1, fields.length);
    assertEquals("walks", fields[0].getName());
}

フィールドの名前がわかっている場合は、他のバリアントも使用できます。

@Test
public void givenClass_whenGetsFieldsByName_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Field field = birdClass.getDeclaredField("walks");

    assertEquals("walks", field.getName());
}

フィールドの名前を間違えたり、存在しないフィールドに入力したりすると、NoSuchFieldExceptionが返されます。

次のようにフィールドタイプを取得します。

@Test
public void givenClassField_whenGetsType_thenCorrect() {
    Field field = Class.forName("com.example.reflection.Bird")
      .getDeclaredField("walks");
    Class fieldClass = field.getType();

    assertEquals("boolean", fieldClass.getSimpleName());
}

次に、フィールド値にアクセスして変更する方法を見ていきます。 フィールドの値を取得できるようにするには、フィールドの設定はもちろんのこと、最初にFieldオブジェクトでsetAccessibleメソッドを呼び出してアクセス可能に設定し、ブール値のtrueをフィールドに渡す必要があります。

@Test
public void givenClassField_whenSetsAndGetsValue_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Bird bird = (Bird) birdClass.newInstance();
    Field field = birdClass.getDeclaredField("walks");
    field.setAccessible(true);

    assertFalse(field.getBoolean(bird));
    assertFalse(bird.walks());

    field.set(bird, true);

    assertTrue(field.getBoolean(bird));
    assertTrue(bird.walks());
}

上記のテストでは、walksフィールドの値がtrueに設定される前に、実際にfalseであることを確認します。

Fieldオブジェクトを使用して、処理しているクラスのインスタンスと、場合によってはフィールドにそのオブジェクトに持たせたい新しい値を渡すことで、値を設定および取得する方法に注目してください。

Fieldオブジェクトについて注意すべき重要な点の1つは、public staticとして宣言されている場合、それらを含むクラスのインスタンスは必要なく、nullを渡すだけでよいということです。次のように、フィールドのデフォルト値を配置して取得します。

@Test
public void givenClassField_whenGetsAndSetsWithNull_thenCorrect(){
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Field field = birdClass.getField("CATEGORY");
    field.setAccessible(true);

    assertEquals("domestic", field.get(null));
}

8. 検査方法

前の例では、メソッド名の検査にのみリフレクションを使用しました。 ただし、Javaリフレクションはそれよりも強力です。

Javaリフレクションを使用すると、コンストラクターの場合と同じように、invoke methods atruntimeを使用して、必要なパラメーターを渡すことができます。 同様に、各パラメーターの型を指定することにより、オーバーロードされたメソッドを呼び出すこともできます。

フィールドと同様に、クラスメソッドを取得するために使用する2つの主要なメソッドがあります。 getMethodsメソッドは、クラスおよびスーパークラスのすべてのパブリックメソッドの配列を返します。

つまり、このメソッドを使用すると、toString, hashCodenotifyAllのようなjava.lang.Objectクラスのパブリックメソッドを取得できます。

@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect(){
    Class birdClass = Class.forName("com.example.java.reflection.Bird");
    Method[] methods = birdClass.getMethods();
    List methodNames = getMethodNames(methods);

    assertTrue(methodNames.containsAll(Arrays
      .asList("equals", "notifyAll", "hashCode",
        "walks", "eats", "toString")));
}

関心のあるクラスのパブリックメソッドのみを取得するには、getDeclaredMethodsメソッドを使用する必要があります。

@Test
public void givenClass_whenGetsOnlyDeclaredMethods_thenCorrect(){
    Class birdClass = Class.forName("com.example.java.reflection.Bird");
    List actualMethodNames
      = getMethodNames(birdClass.getDeclaredMethods());

    List expectedMethodNames = Arrays
      .asList("setWalks", "walks", "getSound", "eats");

    assertEquals(expectedMethodNames.size(), actualMethodNames.size());
    assertTrue(expectedMethodNames.containsAll(actualMethodNames));
    assertTrue(actualMethodNames.containsAll(expectedMethodNames));
}

これらの各メソッドには、名前がわかっている単一のMethodオブジェクトを返す特異なバリエーションがあります。

@Test
public void givenMethodName_whenGetsMethod_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Method walksMethod = birdClass.getDeclaredMethod("walks");
    Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);

    assertFalse(walksMethod.isAccessible());
    assertFalse(setWalksMethod.isAccessible());

    walksMethod.setAccessible(true);
    setWalksMethod.setAccessible(true);

    assertTrue(walksMethod.isAccessible());
    assertTrue(setWalksMethod.isAccessible());
}

個々のメソッドを取得し、それらが取るパラメータタイプを指定する方法に注目してください。 パラメータタイプをとらないものは、空の変数引数で取得され、メソッド名という1つの引数だけが残ります。

次に、実行時にメソッドを呼び出す方法を示します。 デフォルトでは、Birdクラスのwalks属性はfalseであることがわかっているので、そのsetWalksメソッドを呼び出してtrueに設定します。

@Test
public void givenMethod_whenInvokes_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Bird bird = (Bird) birdClass.newInstance();
    Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);
    Method walksMethod = birdClass.getDeclaredMethod("walks");
    boolean walks = (boolean) walksMethod.invoke(bird);

    assertFalse(walks);
    assertFalse(bird.walks());

    setWalksMethod.invoke(bird, true);

    boolean walks2 = (boolean) walksMethod.invoke(bird);
    assertTrue(walks2);
    assertTrue(bird.walks());
}

最初にwalksメソッドを呼び出し、戻り値の型を適切なデータ型にキャストしてから、その値を確認する方法に注目してください。 その後、setWalksメソッドを呼び出してその値を変更し、再度テストします。

9. 結論

このチュートリアルでは、Java Reflection APIについて説明し、それを使用して、コンパイル時までに内部の予備知識がなくても、実行時にクラス、インターフェイス、フィールド、およびメソッドを検査する方法を説明しました。

このチュートリアルの完全なソースコードと例は、私のGithubプロジェクトにあります。