読者です 読者をやめる 読者になる 読者になる

速さが足りない

プログラミングとか、設計とか、ゲームとかいろいろ雑記。

Java Reflection(インスタンスを生成する)

Java Reflection

Java」「Reflection」「リフレクション」といったキーワード、自分でframeworkぽいものを作ったり、ある程度共通化したいと思ったときに使った事があると思います。

どのような機能があり、どういった事ができるか、等の入門的な記事はたくさんありますが、もう少し掘り下げて書かれた記事はなかなか見当たらないため、今回は掘り下げた内容の記事を書いてみようと思います。そのため、そもそもリフレクションとは何ぞや?という方は他のサイトの記事を見た方が参考になると思います・・・その点はご了承ください。

ということでまとめると

  • ・ある程度frameworkを使ったことがある
  • Javaにおけるリフレクションの概要を知っている
  • ・実際にリフレクションを細かく使っていきたい
  • ・エセframeworkつくりたい

方に向けた記事になっています。

という事で、今回はタイトル通りインスタンス生成についてです。まずは定番ですが簡単な生成から見ていきましょう。

まずはクラス定義。

package jp.co.companyname.pjname.domain;

public class Foo{
  private String bar;
  static{
    System.out.println("static block.");
  }
  public Foo(){
    System.out.println("constructor.");
  }
  public Foo(String bar){
    this.bar = bar;
  }
  public String getBar(){
    return this.bar;
  }
  public void setBar(String bar){
    this.bar = bar;
  }
}

この「Foo」クラスを生成するやり方は幾つかあります。まずはどの様な手段があるか見ていきましょう。

  Class<?> fooClazz = Foo.class;
  Foo fooInstance = fooClazz.newInstance();
  Foo foo = new Foo();
  Class<?> fooClazz = foo.getClass();
  Foo fooInstance = fooClazz.newInstance();
  Class<?> fooClazz = Class.forName("jp.co.companyname.pjname.domain.Foo");
  Foo fooInstance = fooClazz.newInstance();
  Class<?> fooClazz = 
    Thread.currentThread().getContextClassLoader()
      .loadClass("jp.co.companyname.pjname.domain.Foo");
  Foo fooInstance = fooClazz.newInstance();

どの例も「Foo」クラスの定義情報を取得し、そこから新しいインスタンスを作成しているわけですね。
違いは「Foo」のクラス定義情報をどうやって取得しているか、な訳です。



さて、1つ目と2つ目はほぼ使う事もないので飛ばすとして、3つ目と4つ目の違いを見ていきましょう。
ぱっと見た限り、3つ目と4つ目は、「Class.forName("X")」で「Class<?> fooClazz」を読み込んでいるか、「ClassLoader#loadClass("X")」で「Class<?> fooClazz」を読み込んでいるわけです。
それ以外に違いはないのか?と思いますよね。ということでもう少し掘り下げていきましょう。



動作を確認するために自前のクラスローダを用意します。これを使って違いを見ていきましょう。

FooClassLoader extends ClassLoader{
  @Override
  public Class<?> loadClass(String name, boolean resolve) 
    throws ClassNotFoundException{
    System.out.println("load class.");
    return super.loadClass(name, resolve);
  }
}


  Class<?> fooClazz = 
    Class.forName("jp.co.companyname.pjname.domain.Foo");
  System.out.println("new instance.");
  Foo fooInstance = fooClazz.newInstance();
実行結果:
static block.
new instance.
constructor.



  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo");
  System.out.println("new instance.");
  Foo fooInstance = fooClazz.newInstanace();
実行結果:
load class.
new instance.
static block.
constructor.

ということで、Class#forName("X")を経由してクラス定義をロードした場合は、staticブロックが即時に初期化され、ClassLoader#loadClass("X")を経由してクラス定義をロードした場合は始めてクラスが利用されたときにstaticブロックが初期化されるわけですね。


実際にstaticブロック満載なコードは少ないと思いますが、気を付けるならClassLoader#loadClass("X")の方が安全でしょうし、自前でClassLoader作るのはちょっと・・・っていう状態であれば「Thread.currentThread().getContextClassLoader()」で取ってきたClassLoaderを使えばまぁ基本問題ないでしょう。



さて、単純な状態のクラスのインスタンスは今までの「Class#newInstance()」で取れますが、FooクラスのStringを1つ引数に受けるコンストラクタを呼び出してインスタンス化することはできません。
勿論、そういったクラスのインスタンスを生成する機能もありますので、次はそちらを見ていきましょう。

  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo");
  Constructor[] constructors = fooClazz.getConstructors();
  Foo fooInstance = null;
  for(Constructor constructor : constructors){
    if(constructor.getParameterTypes().length == 0)
      fooInstance = constructor.newInstance();
    if(constructor.getParameterTypes().length == 1)
      fooInstance = constructor.newInstance("bar");
  }

はい、見ての通りClass型にはコンストラクタを取得する「Class#getConstructors()」というメソッドが用意されていて、そちらを使っていく形になります。
Class#getConstructors()を呼ぶと全てのコンストラクタが取れてしまうので、ifで分岐している訳ですね。もうちょっとスマートに書くと

  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo");
  Constructor constructorNoArg = fooClazz.getConstructor();
  Foo fooInstanceDefaultConstructor = 
    constructorNoArg.newInstance();
  Constructor constructorStringArg = fooClazz.getConstructor(String.class);
  Foo fooInstanceString1Constructor = 
    constructorStringArg.newInstance("bar");

のように書くことができ、引数なしのコンストラクタはClass#newInstance()と同じ挙動になるわけです。
今回はClassLoader#loadClass("X")でクラス定義を読み込んでいるので、staticブロックはConstructor#newInstance()が呼ばれたタイミングで実行されます。

さて、もうちょっと先に進みましょう。

package jp.co.companyname.pjname.domain;

public class Foo{
  private String bar;
  static{
    System.out.println("static block.");
  }
  public Foo(){
    System.out.println("constructor.");
  }
  private Foo(Integer bar){
    this.bar = bar.toString();
  }
  public Foo(String bar){
    this.bar = bar;
  }
  public String getBar(){
    return this.bar;
  }
  public void setBar(String bar){
    this.bar = bar;
  }
  class InnerFoo{
  }
}
  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo");
  InnerFoo innerFooInstance = fooClazz.newInstance();

さて、InnerFooのインスタンスが作れそうに思えますが、実際に動かしてみると作れないんですよね。
実はFooクラスをコンパイルする際にInnerFooクラスにFooクラスを1つ受け取るコンストラクタが合成メソッドとして自動で生成されていて、それを使わないとインスタンスを生成できないわけです。
なぜInnerFooにコンストラクタが追加されるかというと、InnerFooはインナークラスですから、Fooの外からは見えないわけですね。
これを解決するのに合成メソッド(のコンストラクタ)が生成されるってことですね。

前置きが長くなりましたが、実際にインスタンス化してみましょう。

  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo");
  Constructor innerFooConstructor = fooClazz.getDeclaredConstructor(Foo.class);
  InnerFoo innerFooInstance = innerFooConstructor.newInstance(new Foo());

ということで、これでインスタンス化ができましたね。
さて、実は、Clsss型は誰がエンクロージングなのか(インナークラスの大元のクラスなのか)を知っているので、それを使ってもうちょっと固有のコードを減らしていきましょう。

  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo");
  Constructor innerFooConstructor = 
    fooClazz.getDeclaredConstructor(fooClazz.getDeclaringClass());
  InnerFoo innerFooInstance = 
    innerFooConstructor
      .newInstance(fooClazz.getDeclaringClass()
      .newInstance());

綺麗になりましたね。今回はインナークラスでしたが、無名クラスも同様ですので、Class#getEnclosingConstructorやClass#getDeclaredConstructorを使ってインスタンスを生成しましょう。
ちなみに、判定を盛り込んだらこんなかんじになります。

  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo.InnerFoo");
  if(fooClass.isMemberClass()){
    Constructor innerFooConstructor = 
      fooClazz.getDeclaredConstructor(fooClazz.getDeclaringClass());
    InnerFoo innerFooInstance = 
      innerFooConstructor
        .newInstance(fooClazz.getDeclaringClass()
        .newInstance());
  }
  else if(fooClass.cz.isAnonymousClass(){
    //今回は省略
  }
  else{
    fooClazz.newInstance();
  }


ついでにprivateなコンストラクタの呼出しもやってしまいましょう。

  FooClassLoader loader = new FooClassLoader();
  Class<?> fooClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Foo.Foo");
  Constructor fooConstructor = 
    fooClazz.getDeclaredConstructor(Integer.class);
  fooConstructor.newInstance(1);

Class#getDeclaredConstructorでprivate(protected)なコンストラクタが取れますので、これでいけるか?って思いますがやっぱりいけません。IllegalAccessExceptionが飛ぶわけですね。一般的な実行環境であれば

  fooConstructor.setAccessible(true);
  fooConstructor.newInstance(1);

としてあげると、アクセスが許可され、private(protected)なコンストラクタでもリフレクション経由で呼び出すことができます。

ただし、SecurityManagerに設定がされている場合はその限りではありませんので、javaを起動している実行ファイル(の引数)をチェックしましょう。起動オプションが「java (省略) -Djava.security.manager -Djava.security.policy=ポリシファイル 対象クラス (省略)」の形になっていれば、ポリシファイル次第では上記のコードは動かなくなります。ポリシファイルの構文はここにルールがありますので、リフレクションで何か作ったあと環境違いで動かなくなったとき、起動オプションでセキュリティ設定している場合はリンク先を参照して、コードを直すかポリシファイルを書き換えるかしましょう。




さて、これで設定ファイル(xmlなど)からクラス名と引数を受け取ればインスタンスを生成する機能が作れるかな?と思いますが・・・もうちょっと続きます。そうGenericsを忘れていますね?ちょっと難しいですが、見ていきましょう。

package jp.co.companyname.pjname.domain;

public class Bar<T extends Set<V>,V>{
  private T uniqueList;
  private List<V> allList;
  private String name;
  public Bar(T uniqueList, List<V> allList){
    this.uniqueList = uniqueList;
    this.allList = allList;
  }
  public Bar(V allList){
    this.allList = allList;
  }
  public Bar(String name, List<V> allList){
    this.name = name;
    allList.forEach(new Consumer<String>() {
      @Override
      public void accept(String t) {
        if(!uniqueList.contains(t))
          uniqueList.add(t);
      }
    });
  }
}

はい、まずこのコンストラクタを取るところからして困難なわけですね。コンストラクタが1つしかない場合はClass#getConstructors()[0]ってやればいいのですが、2つ以上になったらお手上げです。
さて、困ったなぁと思う前に当初の目的に戻りましょう。基本的にメタ的なプログラミングをするのであれば、spring frameworkにあるようなapplication-context.xmlみたいな、定義(設定)ファイルがあるわけですよね。実際にメタ的なハコはリフレクションで作る訳ですが、何のクラスで動作するかは設定ファイルに書くわけです。ここのBarクラスでいうと・・・

  List<String> allList = new ArrayList<String>();
  Set<String> uniqueList = new HashSet<String>();
  allList.forEach((t) -> {
      if(!uniqueList.contains(t))
        uniqueList.add(t);
  });
  Bar<Set<String>, String> t = 
    new Bar<Set<String>,String>(uniqueList,allList);

みたいな形ですね。TとVの部分に当たるものは、「T = Set」「V = String」なわけです。これらの情報は事前に知っていて、TとList<V>を受け取るコンストラクタを経由してインスタンスを生成するという前提の元、リフレクションでインスタンスを生成できるかやってみましょう。

  Set<String> arg1 = new HashSet<String>();
  List<String> arg2 = new ArrayList<String>();
  
  FooClassLoader loader = new FooClassLoader();
  Class<?> barClazz = 
    loader.loadClass("jp.co.compnayname.pjname.domain.Bar");
  
  Constructor barConstructor = 
    barClazz.getConstructor(arg1.getClass(), arg2.getClass());
  Bar barInstance = barConstructor.newInstance(arg1,arg2);

はい、これだけだと案外簡単ですね。ではもう少しコンストラクタを扱うのが難しいドメインオブジェクトを見てみましょう。

package jp.co.companyname.pjname.domain;

public class Hoge<X extends Map<K.V>,Y extends ConcurrentMap<K,V>,K,V>{
  public Hoge(X x){
    System.out.println("x constructor");
  }
  public Hoge(Y y){
    System.out.println("y constructor");
  }
}

さて、まずは普通にコードを書いたときの状態を見てみましょう。

  Hoge<Map<String,Integer>,ConcurrentMap<String,Integer>,String,Integer> hoge = 
    new Hoge<>(new ConcurrentSkipListMap<String,Integer>());


実行結果:
y constructor

generics引数1を受けるコンストラクタHoge(X x)、generics引数2を受けるコンストラクタHoge(Y y)があります。
継承構造はMap→ConcurrentMap→ConcurrentNavigableMapとなっていて、ConcurrentNavigableMapの実装がConcurrentSkipListMapになります。
よって、ConcurrentSkipListMapはgenerics引数1、2どちらにも当てはまるのでHoge(X x)とHoge(Y y)のうち、直近の実装が近い方であるConcurrentMapの方、Hoge(Y y)が呼ばれている訳ですね。
もちろん、Hoge(X x)の方にもタイプ値としては当てはまるわけですから、コンストラクタさえ取れれば使えます。
ということで、Hoge(X x)の方を呼び出すコードを考えてみましょう。

  Integer[] constructorGenericsIndex = {0};
  
  FooClassLoader loader = new FooClassLoader();
  Class<?> hogeClazz = loader.loadClass("jp.co.compnayname.pjname.domain.Hoge");
  Map<String,String> hogeArg1 = new ConcurrentSkipListMap<>();
  TypeVariable<?>[] hogeGenericTypeVariables = hogeClazz.getTypeParameters();
  List<TypeVariable<?>> constructorTypeVariables = new ArrayList<>();
  for(Integer index : constructorGenericsIndex){
    if(index > hogeGenericsTypeVariables)
      //指定したHogeのgenerics引数のインデックスが実際の数を超えていたら無視する
      continue;
    constructorTypeVariables.add(hogeGnericsTypeVariables[index]);
  }  

  Constructor hogeConstructor = null;
  for(Constructor each : hogeClazz.getConstructors()){
    Type[] genericsTypeParameters = each.getGenericParameterTypes();
    if(genericsTypeParameters.length != constructorTypeVariables.length)
      continue;
    boolean isMatch = true;
    for(int i=0;i<genericsTypeParameters.length;i++){
      TypeVariable<?> param = (TypeVariable<?>)genericsTypeParameters[i];
      TypeVariable<?> constructorTypeVariable = constructorTypeVariables[i];
      isMatch &= param.getName().equals(constructorTypeVariable.getName());
    }
    if(isMatch)
      hogeConstructor = each;
  }
  if(hogeConstructor==null)
    throw new InstantiationException();
  Hoge hogeInstance hogeConstructor.newInstance(hogeArg1);


実行結果:
x constructor

さくっと書きましたがこんな感じでしょうか。Class<Hoge>からは、Hogeの型引数がClass#getTypeParameters()で取れるので、このコードでは「0番目」のgeneric引数をコンストラクタとして受け取るメソッドを探してそれを使っているわけですね。なお、今回のコンストラクタgenerics型しか受け取らないため、Constructor#getGenericParameterTypes()の結果「Type[] genericsTypeParameters」は全て「TypeVariable<Class<?>>」でしか返ってきませんが、通常の型が混じっている場合(StringやList<V>など)、ParameterizedType(実際はParameterizedTypeImpl)等も含まれるので、instanceof等を使って上手く分岐しましょう。


さて、genericsも終わったところで案外使うEnum型にいってみましょう。まずはenumを宣言しましょう。

public enum FooEnum{
  hoge1(1),
  hoge2(2),
  hoge3(3),
  ;
  private Integer id;
  public static final FooEnum defaultValue = hoge1;
  public FooEnum(Integer id){
    this.id = id;
  }
  public id(){
    return id;
  }
}

次にこのEnumを実体化してみましょう。

  String configValue = "hoge2";
  Enum<?>[] enums = FooEnum.class.getEnumConstants();
  //enumのconstantsのみ取れる。ここではdefaultValueは入ってこない
  for(Enum<?> each : enums){
    if(configValue.equals(each.name())
      return each;
  }

enumの定数のみはClass#getEnumConstants()で取得できるので、これで全て取ってきます。
ここで、Enum#name() で実際の FooEnum.hoge1.name() とした値が取れるので、これを使って判別しましょう。
今回は設定値であるconfigValue = "hoge2" と一致したものを返してる訳です。




さて、これで大まかに使いそうなところは網羅できたと思います。しかし、これらを毎回コードに書くのは非効率ですし手間もかかるのでユーティリティとして1つのクラスに纏めてみましょう。

public class ReflectionUtil{
  private ClassLoader _loader;
  private ClassLoader _defaultLoader = Thread.currentThread().getContextClassLoader();
  private Class<?> nullType = Object.class;
    
  public void setClassLoader(ClassLoader _loader){
    this._loader = _loader;
  }
  public ClassLoader getClassLoader(){
    return this._loader;
  }
  private ClassLoader getLoader(){
    if(_loader!=null)
      return _loader;
    return _defaultLoader;
  }
  private void setAccessible(final AccessibleObject target, boolean flag){
    if(!target.isAccessible())
      AccessController.doPrivileged(() -> {
        target.setAccessible(flag);
        return target;
      });
  }
  private Class<?> loadClass(String className) throws ClassNotFoundException{
    return getLoader().loadClass(className);
  }
  private Object[] addParam(Object[] base, Object opt){
    Object[] dest = new Object[base.length+1];
    System.arraycopy(base, 0, dest, 0, base.length);
    dest[base.length] = opt;
    return dest;
  }
  public <T> T newInstance(String className, Object...params) throws 
    ClassNotFoundException,InstantiationException,NoSuchMethodException,
    InvocationTargetException,IllegalAccessException{
      return newInstance(loadClass(className),params);
  }
  public <T> T newInstance(Class<?> clazz, Object...params) throws 
    ClassNotFoundException,InstantiationException,NoSuchMethodException,
    InvocationTargetException,IllegalAccessException{
      List<Class<?>> typeParameters = new ArrayList<Class<?>>();
      boolean isVoidParameter = params == null || params.length == 0;
      boolean isInnerClass = clazz.isMemberClass() || clazz.isLocalClass() || clazz.isAnonymousClass();
      if(!isVoidParameter){
        Arrays.asList(params).forEach((each) -> {
          if(each==null){
            typeParameters.add(nullType);
            return;
          }
          typeParameters.add(each.getClass());
      });
      if(isInnerClass){
        boolean isDeclaringClass = clazz.isMemberClass();
        boolean isEnclosingClass = clazz.isAnonymousClass() || clazz.isLocalClass();
        List<Class<?>> enclosingTypeParameters = new ArrayList<Class<?>>();
        if(isDeclaringClass)
          enclosingTypeParameters.add(clazz.getDeclaringClass());
        else if(isEnclosingClass)
          enclosingTypeParameters.add(clazz.getEnclosingClass());
        enclosingTypeParameters.addAll(typeParameters);
        Constructor<?> constructor = null;
        if(isDeclaringClass)
          constructor = clazz.getDeclaredConstructor((Class<?>[])enclosingTypeParameters.toArray());
        else if(isEnclosingClass)
          constructor = clazz.getEnclosingConstructor();
        setAccessible(constructor, true);
        Object enclosingInstance = null;
        if(isDeclaringClass)
          enclosingInstance = newInstance(clazz.getDeclaringClass());
        else if(isEnclosingClass)
          enclosingInstance = newInstance(clazz.getEnclosingClass());
        Object[] enclosingParams = addParam(params,enclosingInstance);
          return (T)constructor.newInstance(enclosingParams);
      }
      else if(clazz.isPrimitive()){
        //short
        if(Short.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Short.valueOf((short)0);
          return (T) Short.valueOf(params[0].toString());
        }
        //int
        if(Integer.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Integer.valueOf(0);
          return (T) Integer.valueOf(params[0].toString());
        }
        //long
        if(Long.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Long.valueOf((long)0);
          return (T) Long.valueOf(params[0].toString());
        }
        //float
        if(Float.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Float.valueOf(0);
          return (T) Float.valueOf(params[0].toString());
        }
        //double
        if(Double.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Double.valueOf(0.0);
          return (T) Double.valueOf(params[0].toString());
        }
        //byte
        if(Byte.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Byte.valueOf((byte)0);
          return (T) Byte.valueOf(params[0].toString());
        }
        //char
        if(Character.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Character.valueOf((char)0x00);
          return (T) Character.valueOf(params[0].toString().charAt(0));
        }
        //boolean
        if(Boolean.TYPE.equals(clazz)){
          if(isVoidParameter)
            return (T) Boolean.valueOf(false);
          return (T) Boolean.valueOf(params[0].toString());
        }
        throw new InstantiationException();
    }
    else if(clazz.isAnnotation()){
      throw new InstantiationException();
    }
    else if(clazz.isInterface()){
      throw new InstantiationException();
    }
    else if(clazz.isEnum()){
      Enum<?>[] enumerations = (Enum<?>[])clazz.getEnumConstants();
      for(Enum<?> each : enumerations){
        if(isVoidParameter)
          return (T)each;
        for(Object param : params){
          if(param != null && param.toString().equals(each.name()))
            return (T)each;
        }
      }
      throw new InstantiationException();
    }
    else{
      Constructor constructor = null;
      try{
        constructor = clazz.getConstructor((Class<?>[])typeParameters.toArray());
      }
      catch(NoSuchMethodException nsme){
        constructor = clazz.getDeclaredConstructor((Class<?>[])typeParameters.toArray());
      }
      finally{
        if(constructor == null)
          throw new InstantiationException();
        return (T)constructor.newInstance(params);
      }
    }
  }
}

こんなかんじですかね。generics引数を受け取るクラスは動かないので別建てでメソッドを用意する必要がありますが、大体の印象は伝わりましたかね?実際はエラー処理やら、次回以降でやるAnnotationやProxyを使ってインスタンスも作れるので、色々変わってくるとは思います。

ついでに、実際にどんな感じになるか書いてみましょう。

<?xml>
  <bean id="foo1" class="jp.co.companyname.pjname.Foo" />
  <bean id="foo2" class="jp.co.companyname.pjname.Foo">
    <constructor-args>
      <constructor-arg class="java.lang.String" value="bar" />
    </constructor-args>
  </bean>
  String id = CUstomXmlParser.getId();
  String className = CustomXmlParser.getClassName();
  Object[] params = CustomXmlParser.getConstructorArguments();
  Object bean = ReflectionUtil.newInstance(className, params);
  ...

こんな感じですね。CustomXmlParserは書くとかなり長くなってしまいますので省略しています。
ということで、長くなりましたがインスタンス生成については以上です。時間があるときに最後のユーティリティのところはちょこちょこ直すかも知れません。(動作確認もしてないですし・・・w)