メソッドオーバーロードの動的束縛とか

Javaのメソッドは名前が似てるオーバーライドとオーバーロードという二つの概念があります。オーバーライドはサブクラスでメソッドを再定義するもので、オーバーロードは同じ名前で引数が異なるメソッドを複数定義する仕組みです。

で、今回はJavaのメソッドオーバーロードで無茶して、動的束縛を実現する例。

Javaのメソッドオーバーロードは、コンパイル時に静的に解決されます。つまり、

public class StaticDispatch {
    public static void main(String[] args) {
        StaticDispatch obj = new StaticDispatch();
        Root hoge = new Hoge();
        obj.method(hoge);
    }
    
    public void method(Object o) {
        System.out.println("Object");
    }
    public void method(Hoge o) {
        System.out.println("Hoge");
    }
    public void method(Foo o) {
        System.out.println("Foo");
    }
    
    interface Root {}
    static class Hoge implements Root {}
    static class Foo implements Root {}
    static class Bar extends Hoge {}
}

というプログラムは"Object"と出力されます。
引数に渡した型の本当の値はHoge型ですが、オーバーロードコンパイル時の型だけで解決しようとします*1。そのため、Root型のargを渡しているメソッドの呼び出しではmethod(Hoge)が選択されることはありません。

コレを何とかしてmethod(Hoge)を呼んでみようというのが趣旨。

パワーで解決

一番簡単で、一番ありがちなのがコレ。

public class HandDispatch {
    public static void main(String[] args) {
        HandDispatch obj = new HandDispatch();
        Root arg = new Hoge();
        // obj.method(arg) の代わりに、振り分けするメソッドを呼ぶ
        obj.dispatch(arg); 
    }
    
    // 引数の型検査をしつつ手動でがんばる
    private void dispatch(Object arg) {
        if (arg instanceof Hoge) {
            method((Hoge) arg); // Bar (extends Hoge)も自動的に
        }
        else if (arg instanceof Foo) {
            method((Foo) arg);
        }
        else {
            method(/*(Object)*/ arg);
        }
    }
    interface Root {}
    static class Hoge implements Root {}
    static class Foo implements Root {}
    static class Bar extends Hoge {}
}

dispatchメソッドの中では、argをinstanceofで検査して、適切な型にキャストした上でオーバーロードを行っています。キャストすればコンパイル時の型も変わるので、静的に正しく解決されてmethod(Hoge)が呼び出されるようになります。

ただ、このタイプはメソッド数が5個を越えた辺りから残念な気分になります。

Visitorで解決

引数のデータのほうにacceptメソッドを定義して、Visitor内のオーバーロードされたメソッドを呼ぶパターンでも解決できます。これは、acceptメソッドをそれぞれのデータ型でオーバーライドすることで、この部分だけは型ごとに定義されたacceptメソッドを動的束縛で呼び出すことができます。
で、acceptの中で静的にオーバーロードを解決してやれば、あたかもオーバーロードメソッドを動的束縛してるように見せかけられます。

こんな感じ。

public class VisitorDispatch {
    public static void main(String[] args) {
        VisitorDispatch obj = new VisitorDispatch();
        Root arg = new Hoge();
        arg.accept(obj); // Visitorパターンでmethod(..)を呼び戻してもらう
    }

    public void method(Object o) {
        System.out.println("Object");
    }
    public void method(Hoge o) {
        System.out.println("Hoge");
    }
    public void method(Foo o) {
        System.out.println("Foo");
    }
    
    interface Root {
        // acceptメソッドを基底型に宣言しておく
        void accept(VisitorDispatch callback);
    }
    // それぞれの型でacceptメソッドを実装する
    static class Hoge implements Root {
        public void accept(VisitorDispatch callback) {
            // 中身はオーバーロードメソッドの呼び出しだけ。
            // thisの型がHogeと静的にわかるため、オーバーロードが正しく動作する
            callback.method(this);
        }
    }
    static class Foo implements Root {
        public void accept(VisitorDispatch callback) {
            callback.method(this);
        }
    }
    static class Bar extends Hoge {
        @Override public void accept(VisitorDispatch callback) {
            callback.method(this);
        }
    }
}

こいつの問題は、それぞれのクラスでacceptメソッドを実装してやらなきゃならないことです。そのため、外部のライブラリなど、自分で変更ができないものについてはこのパターンを適用できません。また、引数が2個あったりすると、acceptメソッドの組み合わせが爆発するのでアウトです。
このやり方は、パワーで解決するやり方よりも再利用性は高くなるのですが、acceptを全部に実装する点が面倒です。自動生成系以外ではやりたくない部類に入る感じ。

なお、最近やたらVisitorについて言及してるのは、このパターンが好きなわけではなくて、オーバーロードの動的束縛が使えると便利な場面が多いってだけです。ちなみに、実行時に引数の情報を元にメソッドを選択する仕組みを、多重ディスパッチと呼ぶそうです。

リフレクションで解決(失敗版)

リフレクションを使ってメソッドを決定すれば、Visitor書かなくても汎用的に動的束縛してくれるんじゃねとか思ってました。

public class ReflectionDispatch {
    public static void main(String[] args) throws Exception {
        ReflectionDispatch obj = new ReflectionDispatch();
        Root arg = new Bar(); // 今回はBarで。
        obj.dispatch(arg);
    }
    
    // 引数の型検査をしつつリフレクション機構にがんばってもらう
    private void dispatch(Object arg) throws Exception {
        // argの引数を元にメソッドを探す
        Method method = getClass().getMethod("method", arg.getClass());
        method.invoke(this, arg);
    }

    public void method(Object o) {
        System.out.println("Object");
    }
    public void method(Hoge o) {
        System.out.println("Hoge");
    }
    public void method(Foo o) {
        System.out.println("Foo");
    }

    // public void method(Bar o) はあえて定義しない

    interface Root {}
    static class Hoge implements Root {}
    static class Foo implements Root {}
    static class Bar extends Hoge {}
}

if (arg instanceof ..) のチェインの代わりにリフレクションAPIを使っている感じです。これを実行してみると、

Exception in thread "main" java.lang.NoSuchMethodException:
    ...ReflectionDispatch.method(...Bar)
  at java.lang.Class.getMethod(Class.java:1605)
  at ...dispatch(ReflectionDispatch.java:15)
  at ...main(ReflectionDispatch.java:9)

というエラーメッセージ(整形済み)が…。この場合はちゃんと「getClass().getMethod("method", Hoge.class)」と書いてやらないと、method(Hoge)を見つけてくれないようです。Bar extends Hogeですが、サブタイプとか完全に無視されてる模様。

リフレクションで解決(改良版)

オーバーロードメソッドをコンパイル時に解決するのは意外と面倒で、こんなことやってます*2

  1. 利用可能なすべてのメソッド(継承したやつも含む)をクラスから取得する
  2. メソッドから適用可能なものの一覧を抽出する
    • メソッド名が呼び出し対象の名前と一致する
    • すべての実引数がメソッドの仮引数に適用できる
  3. 適用可能なメソッドから、最も限定的なものを抽出する
    • [a(Object), a(CharSequence), a(String)] だったら a(String)になる
  4. 最後に残ったのがひとつなら、それが呼び出し先のメソッド
    • それ以外は呼び出し先が存在しない/あいまいなので失敗

今日の帰りの電車で、上記をちょっと端折りつつ実装してみました。150行くらいあります。速度はまったく考えてないので、多分ダメなくらい遅いはず。

public class Dispatcher {

    public static Object invokevirtual(
            Object obj, String name, Object...args) throws Exception {
        
        // 引数の型一覧
        Class<?>[] argTypes = toTypes(args);
        
        // メソッド名が一致して、さらに実引数が適用できるもの一覧
        List<Method> applicables =
            findApplicables(obj.getClass(), name, argTypes);
        
        // もっとも限定的な引数一覧を持つメソッドを探す
        List<Method> mostApplicables = findMostApplicables(applicables);

        // メソッドが一意に決まれば完了。そうじゃないとエラー
        if (mostApplicables.isEmpty()) {
            throw new IllegalArgumentException("ひとつもない");
        }
        if (mostApplicables.size() >= 2) {
            throw new IllegalArgumentException("あいまい: " + mostApplicables);
        }
        
        return mostApplicables.get(0).invoke(obj, args);
    }

    // 型の一覧を列挙。ただし、nullの型はnullで表現することに
    private static Class<?>[] toTypes(Object... args) {
        Class<?>[] argTypes = new Class<?>[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] != null) {
                argTypes[i] = args[i].getClass();
            }
        }
        return argTypes;
    }
    
    // クラスから適用可能なすべてのメソッドを探してくる
    private static List<Method> findApplicables(
            Class<?> type, String name, Class<?>[] argTypes) {
        List<Method> results = new ArrayList<Method>();
        for (Method m : type.getMethods()) {
            // まず名前が一致しないとアウト
            if (m.getName().equals(name) == false) {
                continue;
            }
            // 実引数を仮引数に適用できないとアウト
            Class<?>[] paramTypes = m.getParameterTypes();
            if (isApplicable(argTypes, paramTypes) == false) {
                continue;
            }
            // 引数を適用できるメソッドがひとつとは限らないので全部列挙
            results.add(m);
        }
        return results;
    }

    // 実引数を仮引数に適用できる?
    private static boolean isApplicable(Class<?>[] from, Class<?>[] to) {
        // 引数の個数が違えばアウト
        if (from.length != to.length) {
            return false;
        }
        // ひとつでも適用できない引数があればアウト
        for (int i = 0; i < from.length; i++) {
            if (isApplicable(from[i], to[i]) == false) {
                return false;
            }
        }
        // 全部適用できればOK
        return true;
    }
    
    // from型の値をto型の引数に適用できる?
    private static boolean isApplicable(Class<?> from, Class<?> to) {
        // SomeType v = null; は常にOK
        if (from == null) {
            return true;
        }
        else if (to == null) {
            return false;
        }
        // Class.isAssignbleFrom(Class) で簡易検査
        return to.isAssignableFrom(from);
    }
    
    // 候補の中から最も限定的なものを探し出す
    // a(Object), a(CharSequence), a(String) だったら a(String)になる
    private static List<Method> findMostApplicables(List<Method> apps) {
        Method[] ms = apps.toArray(new Method[apps.size()]);
        for (int i = 0; i < ms.length; i++) {
            if (ms[i] == null) {
                continue; // less applicableですでに候補から外れてる
            }
            // ms[i]のほうがms[j]よりもmore applicableなら、
            // ms[j]を候補から消す
            for (int j = 0; j < ms.length; j++) {
                if (i == j) {
                    continue; // 自分自身はスルー
                }
                if (ms[j] == null) {
                    continue; // less applicableですでに候補から外れてる
                }
                if (isMoreApplicable(ms[i], ms[j])) {
                    ms[j] = null; // less applicable は除去
                }
            }
        }
        
        // こうして残ったのを拾い集める
        List<Method> results = new ArrayList<Method>();
        for (int i = 0; i < ms.length; i++) {
            if (ms[i] != null) {
                results.add(ms[i]);
            }
        }
        return results;
    }
    
    // aの引数一覧がbの引数一覧よりも厳しい?
    private static boolean isMoreApplicable(Method a, Method b) {
        Class<?>[] as = a.getParameterTypes();
        Class<?>[] bs = b.getParameterTypes();
        assert as.length == bs.length;
        boolean moreApplicable = false;
        for (int i = 0; i < as.length; i++) {
            // 同じ型なら次の引数で判定
            if (as[i] == bs[i]) {
                // next..
            }
            // as[i]とbs[i]が違う型で、as[i]をbs[i]に適用できると
            // aのほうが限定的になる可能性がある
            else if (isApplicable(as[i], bs[i])) {
                moreApplicable = true;
            }
            // それ以外では、aとbは関係ない型なので、
            // aはbよりも限定的とはいえない
            else {
                return false;
            }
        }
        // ひとつでも限定的なのがあればOK。ただし、関係ない型があるとダメ
        return moreApplicable;
    }
}

これを使うと、パワーのようにif文を書かず、Visitorのようにacceptを書かず、さらに汎用的に動的にオーバーロードメソッドを選択できるようになります。

こんな感じで。

public class Reflection2Dispatch {
    public static void main(String[] args) throws Exception {
        Reflection2Dispatch obj = new Reflection2Dispatch();
        // どうせだからいろいろと呼んでみる
        Object[] values = {new Hoge(), new Foo(), new Bar(), new Object()};
        for (Object arg : values) {
            String type = arg.getClass().getSimpleName();
            // 普通のオーバーロード機構で実行する
            System.out.println("=== Built-in Overload: " + type);
            obj.method(arg);
            // さっき作った処理で実行する
            System.out.println("=== Dynamic Overload: " + type);
            Dispatcher.invokevirtual(obj, "method", arg);
        }
    }

    public void method(Object o) {
        System.out.println("Object");
    }
    public void method(Hoge o) {
        System.out.println("Hoge");
    }
    public void method(Foo o) {
        System.out.println("Foo");
    }
    
    interface Root {}
    static class Hoge implements Root {}
    static class Foo implements Root {}
    static class Bar extends Hoge {}
}

結果はこうでます(整形済み)。

=== Built-in Overload: ...Hoge@69b332
Object
=== Dynamic Overload: ...Hoge@69b332
Hoge
=== Built-in Overload: ...Foo@530daa
Object
=== Dynamic Overload: ...Foo@530daa
Foo
=== Built-in Overload: ...Bar@a62fc3
Object
=== Dynamic Overload: ...Bar@a62fc3
Hoge
=== Built-in Overload: Hello, world!
Object
=== Dynamic Overload: Hello, world!
Object

だいたいよさげな感じ。

ちなみに、Dispatcher.invokevirtual(obj, "method", null) というように引数にnullを渡すとmethod(Hoge)とmethod(Foo)があいまいで見つけられないエラーが出ます。これはmethod(null)って直接書いたときも同じようにあいまいになります。

まとめ

  • パワーで解決
    • 一番かたいし、読みやすい
    • if文を書くので間違いやすく面倒
    • 処理を切り出すのが比較的難しい
  • Visitorで解決
    • 慣れてれば読みやすい
    • 記述量が多くなる
    • 引数の型にacceptメソッドを実装するので、それができない状況だと無理
  • リフレクションで解決
    • 特に制約はない
    • パフォーマンスがひどいことになる
    • 静的型検査がまったく効かなくなる

どれも一長一短。

*1:Javaオーバーロードは動的束縛を行いません

*2:Java5系からジェネリクス型推論の導入により、これはさらに面倒なことになっています(http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.12)。