Java でインスタンスメソッドを静的にコールする方法はない? 2


ここで「静的に」というのは、“狙いを定めて…”とか“継承関係による優先順位をすっとばして…”とか“(可能なら)型安全性を無視して…”というような意味で使っています。わかりにくくてごめんなさい。 そう…。この場合、“メソッドディスパッチを介さずに直接…”とか“非仮想的に”と書くのがより的確だったかもしれません。(odz さん、ありがとうございます)


id:sumim:20061207:p1 について、keisuken さんにリフレクションを使えば可能…という言及追記:上記表現が悪くて、誤解を生じさせてしまったようです。スミマセン。>keisuken さん)をいただいたのでさっそく試してみたのですが、いかんせん自らの Java 力不足の前にみごとに失敗したので晒しておきます(^_^;)。

class B {
   public void method() { System.out.println("B#mehtod()"); }
}

class D extends B {
   public void method() { System.out.println("D#method()"); }
}

public class Main {
   public static void main(String[] args) throws Exception {
      new D().method();                                    //=> "D#method()"
      B b = new D();
      b.method();                                          //=> "D#method()"
      B.class.getMethod("method", null).invoke(b, null);   //=> "D#method()"
   }
}

私のメンタルモデルでは、最後の invoke は "B#method()" を返してきてくれると期待していたわけです。orz


SqueakSmalltalk だと、こんな感じでしょうか。

Object subclass: #B

B >> method
  ^ 'B >> #method'
B subclass: #D

D >> method
  ^ 'D >> #method'
(D new) method                                               " => 'D >> #method' "
(B >> #method) valueWithReceiver: (D new) arguments: #()     " => 'B >> #method' "


追記: ちなみに両者は継承関係にある必要すらありません。Smalltalk、恐るべし…。w

Object subclass: #C1

C1 >> method
  ^ 'C1 >> #method'
Object subclass: #C2

C2 >> method
  ^ 'C2 >> #method'
(C2 new) method                                              " => 'C2 >> #method' "
(C1 >> #method) valueWithReceiver: (C2 new) arguments: #()   " => 'C1 >> #method' "

Java の getMethod & invoke の振る舞いが、Smalltalk だと“#perform:シリーズ”に近いというのは、発見…というよりは軽い衝撃(^_^;)でした。

(D new) perform: #method withArguments: #()                  " => 'D >> #method' "

他方で、こんなことをして遊んでみたりもしましたが、予想を超えた不思議な振る舞いを目の当たりにしてしまいました。

import java.lang.reflect.*;

interface I {
  public void method();
}

class C1 implements I {
  public void method() { System.out.println("C1#method()"); }
}

class C2 implements I {
  public void method() { System.out.println("C2#mehtod()"); }
}

public class Main {
  public static void main(String[] args) throws Exception {
    new C2().method();                                   //=> "C2#mehtod()"
    I ic2 = new C2();
    ic2.method();                                        //=> "C2#mehtod()"
    Method m1 = C1.class.getMethod("method", null);
    // m1.invoke(ic2, null);                             //=> error
    Field f = m1.getClass().getDeclaredField("clazz");
    f.setAccessible(true);
    f.set(m1, C2.class);
    m1.invoke(ic2, null);                                //=> "C2#mehtod()" !?
  }
}

その後、Javassist というライブラリを見つけました。この Javassist が提供する機能が、当初やろうとしていた「メソッドをクラスから引っこ抜いてきて叩く」というイメージに近いのかな…と思いました。

import javassist.*;

class C1 {
   public void method() { System.out.println("C1#method()"); }
}

class C2 {
}

public class Main {
   public static void main(String[] args) throws Exception {
      ClassPool pool = ClassPool.getDefault();
      CtClass cc1 = pool.get("C1");
      CtClass cc2 = pool.get("C2");
      CtMethod m1 = cc1.getMethod("method", "()V");
      cc2.addMethod(CtNewMethod.copy(m1, cc2, null));
      cc2.writeFile();
      
      C2.class.getMethod("method", null).invoke(new C2());   //=> "C1#method()"
   }
}

odz さんから、もろもろの情報に添えてアクセス修飾に着目すればいいというヒントをいただいたので、Javassist との組み合わせでトライしてみました。アクセス修飾の変更や、メソッド差し替えの方法は、 プログラマメモ - privateなメソッドにアクセスするもうひとつの方法 を参考にさせていただきました。

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.AccessFlag;

class B {
   public void method() { System.out.println("B#method()"); }
}

class D extends B {
   public void method() { System.out.println("D#method()"); }
}

class DD extends D {
   public void method() { System.out.println("DD#method()"); }
}

class Main {
   public static void main(String[] args) throws Exception {

      CtClass cc;
      CtMethod m;
      String [] l = {"B", "D", "DD"};
      for (String name : l) {
         cc = ClassPool.getDefault().get(name);
         m = cc.getDeclaredMethod("method");
         cc.removeMethod(m);
         m.getMethodInfo2().setAccessFlags(AccessFlag.PRIVATE); 
         cc.addMethod(m);
         cc.writeFile();
      }
      
      Object obj = DD.class.newInstance();
      ((B)obj).method();                   //=> "B#method()"
      ((D)obj).method();                   //=> "D#method()"
      ((DD)obj).method();                  //=> "DD#method()"
      obj = D.class.newInstance();
      ((B)obj).method();                   //=> "B#method()"
      ((D)obj).method();                   //=> "D#method()"
      obj = B.class.newInstance();
      ((B)obj).method();                   //=> "B#method()"
   }
}


一瞬、できたっ!?…と思ったのですが、なんだかちょっと違うような(^_^;)。ともあれ、デフォではできないということが分かったのと、いろいろと教えていただけたので良かったです。


おまけ。newInstance() しなくてもいい版。つか、ファイルを別にしただけ。

DD.java
package pkg;

class B {
   public void method() { System.out.println("B#method()"); }
}

class D extends B {
   public void method() { System.out.println("D#method()"); }
}

public class DD extends D {
   public void method() { System.out.println("DD#method()"); }

   public static void main(String[] args) {

                                //   -- intact --    -- modified --
      B b = new DD();
      D d = new DD();
      b.method();               //=> "DD#method()"    "B#method()"
      d.method();               //=> "DD#method()"    "D#method()"
      new DD().method();        //=> "DD#method()"    "DD#method()"
      
      b = new D();
      b.method();               //=> "D#method()"     "B#method()"
      new D().method();         //=> "D#method()"     "D#method()"
      
      new B().method();         //=> "B#method()"     "B#method()"
   }
}
Modifier.java
package pkg;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.AccessFlag;

public class Modifier {
   public static void main(String[] args) throws Exception {
      ClassPool cp = ClassPool.getDefault();
      CtClass cc;
      CtMethod cm;
      String [] l = {"B", "D", "DD"};
      for (String name : l) {
         cc = cp.get("pkg." + name);
         cm = cc.getDeclaredMethod("method");
         cc.removeMethod(cm);
         cm.getMethodInfo2().setAccessFlags(AccessFlag.PRIVATE); 
         cc.addMethod(cm);
         cc.writeFile();
      }
      
      DD.main(args);
   }
}

die さんからも別解を頂きました。SqueakSmalltalk で意訳すると、こんなかんじになりましょうか。

Object subclass: #C1

C1 >> method
  ^ 'C1 >> #method'
Object subclass: #C2

C2 >> method
  ^ 'C2 >> #method'
(C1 new) method            " => 'C1 >> #method' "
(C2 new) method            " => 'C2 >> #method' "
((C2 new) as: C1) method   " => 'C1 >> #method' "


しかし、仮とはいえ“become”を名乗る以上は、このくらいの凶悪さがないとちょっと物足りないかも…。w

| aC2 bC2 cC2 |
aC2 := C2 new.
bC2 := cC2 := C2 new.
bC2 become: (bC2 as: C1).

^ {aC2 method. bC2 method. cC2 method}
=> #('C2 >> #method' 'C1 >> #method' 'C1 >> #method')