われプログラミングする、ゆえにバグあり

私だって価値を創造してみたいのです

interfaceにNewというstaticメソッドを定義してみた

java8からはinterfaceにstaticメソッドを定義できて便利です。


こういうの面白いかなと思って、New という static メソッドを持つ interface 定義してみました。そしたら、interface だけでも結構いけるんじゃね?って思ったので、記事を書いてみました。
ちょっとしたネタです。

ここでは Map のインスタンスに interface をラップして、(その interface で定義した)getHoge() メソッドの呼び出しで Map#get("hoge") を呼ぶような委譲処理をやってみます。
(interface の実装では Proxy.newProxyInstance を使っています。)

1.ベースとなる interface(と実装)の定義

これ(1.)は読み飛ばして、次(2.)だけ読むといいです。

interface Bean {

    static <T extends Bean> T __new(Class<? extends T> clazz) {
        return __new(null, clazz);
    }

    static <T extends Bean> T __new(Map<String, Object> store, Class<? extends T> clazz) {
        return __new(null, store, clazz);
    }

    static <T extends Bean> T __new(T obj, Map<String, Object> store) {
        List<Class<?>> classes = new ArrayList<>();
        for (Class<?> clazz : obj.getClass().getInterfaces()) {
            if (Bean.class.isAssignableFrom(clazz)) {
                classes.add(clazz);
            }
        }
        return (T) __new(obj, store, (Class<? extends Bean>[]) classes.toArray(new Class<?>[classes.size()]));
    }

    // 実装部分(インスタンス生成とメソッドの実装)
    static <T extends Bean> T __new(T obj, Map<String, Object> store, Class<? extends Bean>... classes) {
        Map<String, Object> _store = store == null ? new HashMap<>() : store;
        return (T) Proxy.newProxyInstance(Bean.class.getClassLoader(), classes, (proxy, method, args) -> {
            String name = method.getName();
            if (name.startsWith("get") && ((args == null) || (args.length == 0))) {
                if (name.equals("get")) {
                    return _store;
                }
                String key = name.substring(3);
                if (!key.isEmpty()) {
                    key = key.substring(0, 1).toLowerCase() + key.substring(1);
                }
                Object ret = _store.get(key);
                Class<?> retType = method.getReturnType();
                if (retType.isPrimitive() && (ret == null)) {
                    if (retType == boolean.class) { return false; }
                    if (retType == byte.class)    { return (byte) 0; }
                    if (retType == short.class)   { return (short) 0; }
                    if (retType == char.class)    { return (char) 0; }
                    if (retType == int.class)     { return 0; }
                    if (retType == long.class)    { return 0L; }
                    if (retType == float.class)   { return 0f; }
                    if (retType == double.class)  { return 0d; }
                }
                return ret;
            } else if (name.startsWith("set") && ((args != null) && (args.length == 1))) {
                String key = name.substring(3);
                if (!key.isEmpty()) {
                    key = key.substring(0, 1).toLowerCase() + key.substring(1);
                }
                _store.put(key, args[0]);
                return null;
            }
            return method.invoke(obj == null ? _store : obj, args);
        });
    }

    // 以下、外部から呼び出すメソッド

    static <T extends Bean> T alias(T obj) {
        return (T) __new(obj, obj.get());
    }

    static <S extends Bean, T extends Bean> S alias(T obj, Class<S> clazz) {
        return (S) __new(obj, obj.get(), clazz);
    }

    static <T extends Bean> T copy(T obj) {
        return (T) __new(obj, new HashMap<>(obj.get()));
    }

    static <S extends Bean, T extends Bean> S copy(T obj, Class<S> clazz) {
        return (S) __new(obj, new HashMap<>(obj.get()), clazz);
    }

    Map<String, Object> get();
}

※戻り値の型のチェックは厳密ではありません。
(例えば、String型の値をsetしてInteger型でgetしようとするとClassCastExceptionが投げられます。)
実装方法については、メソッドのマッチ方法とその場合の処理を指定するといった感じにすると汎用的になるでしょう。

 

2.作成した interface を使ってみる

こちらのコードが(2.)です

interface Name extends Bean {

    static Name New() {
        return Bean.__new(Name.class);
    }

    static Name accessor(Map<String, Object> map) {
        return Bean.__new(map, Name.class);
    }

    String getName();

    void setName(String name);
}

interface Name2 extends Bean {

    static Name2 New() {
        return Bean.__new(Name2.class);
    }

    static Name2 accessor(Map<String, Object> map) {
        return Bean.__new(map, Name2.class);
    }

    String getName();

    void setName(String name);
}


// 以下が実行例です。

Map<String, Object> map = new HashMap<>();
map.put("name", "brigen");

// MapをNameインターフェースでラップするインスタンスを生成
Name n_1 = Name.accessor(map);
System.out.println("n_1.getName() => " + n_1.getName()); // => brigen

// 新しく内部にMapを持つNameインターフェースのインスタンスを生成
Name n_2 = Name.New();
System.out.println("n_2.getName() => " + n_2.getName()); // => null
n_2.setName("hoge");
System.out.println("n_2.getName() => " + n_2.getName()); // => hoge
System.out.println("n_1.getName() => " + n_1.getName()); // => brigen

// 同じMapをラップするNameインターフェースを生成
Name n_3 = Bean.alias(n_1);
System.out.println("n_3.getName() => " + n_3.getName()); // => brigen

// 複製したMapをNameインターフェースでラップするインスタンスを生成
Name n_4 = Bean.copy(n_1);
System.out.println("n_4.getName() => " + n_4.getName()); // => brigen
n_4.setName("foo");
System.out.println("n_4.getName() => " + n_4.getName()); // => foo
System.out.println("n_1.getName() => " + n_1.getName()); // => brigen

// 同じMapをName2インターフェースでラップするインスタンスを生成
Name2 n2_1 = Bean.alias(n_1, Name2.class);
System.out.println("n2_1.getName() => " + n2_1.getName()); // => brigen

// 複製したMapをName2インターフェースでラップするインスタンスを生成
Name2 n2_2 = Bean.copy(n_1, Name2.class);
System.out.println("n2_2.getName() => " + n2_2.getName()); // => brigen
n2_2.setName("bar");
System.out.println("n2_2.getName() => " + n2_2.getName()); // => bar
System.out.println("n_1.getName() => " + n_1.getName());   // => brigen

java8でメソッド参照の機能が追加されたので、Name.New() は場合によっては Name::New と使えそうです。

 

まとめ

lombok(Project Lombok)には処理を委譲させる機能(@Delegateアノテーション)もあります。ですが、同じシグニチャのメソッドを呼ぶ以上の使い方ができなさそうというのもあって、今回の記事を書いてみました。

java8のメソッド参照では、コンストラクタの呼び出しも他のメソッドの呼び出しと同じように使えます。なので、インスタンスの生成のためのstaticメソッドを、コンストラクタのように使う。といった機会が増えそうです(?)