「ClojureでJavaクラスのコンストラクタをapplyする」をリフレクションで

元ネタは1年前の記事。

ClojureからJavaコンストラクタを呼ぶ場合,

(new Hoge foo bar)

または

(Hoge. foo bar)

のように書く。ところが,これらの構文ではクラス名が静的に決まっている必要があるため,たとえば

(let [c Hoge]
  (new c foo bar))

(let [c Hoge]
  (c. foo bar))

というようには書けない。また,applyを使って

(apply Hoge. [foo bar])

とすることもできない。

インスタンスを生成するクラスを動的に変更したい場合というのはちょくちょくあって,そういう場合には今まで,オブジェクトを生成する関数を引数に渡すようにしてお茶を濁していた。

リンク先の記事では,最終的にevalを使って解決していたけど,それだけのためにevalを呼ぶのはいかにも効率が悪そう。そこで,最近ちょっとずつ弄って遊んでいたJavaのリフレクションの機能を使えば同じことが実現できそうだと思い立って書き直してみた。

ソースコード

apply_ctor.clj

(ns apply-ctor
  (import [java.lang.reflect Constructor]))

(defn- acceptable-types? [ptypes atypes]
  (and (= (count ptypes) (count atypes))
       (every? (fn [[ptype atype]]
                 (or (= ptype atype)
                     ((ancestors atype) ptype)))
               (map vector ptypes atypes))))

(defn apply-ctor [^Class klass args]
  (let [atypes (into-array Class (map class args))
        ctors (for [^Constructor ctor (.getConstructors klass)
                    :let [ptypes (.getParameterTypes ctor)]
                    :when (acceptable-types? ptypes atypes)]
                ctor)]
    (when (empty? ctors)
      (throw (IllegalArgumentException.
              (str "No matching ctor found for " klass))))
    (let [^"[Ljava.lang.Object;" args (into-array Object args)]
      (.newInstance ^Constructor (first ctors) args))))

やってることは単純で,リフレクションを使ってクラスからすべてのコンストラクタを取得して,実引数の型からコンストラクタを選別,コンストラクタを呼ぶ,ということをやっている。無駄なリフレクションが起きないように,極力 type hint をつけるようにして書いている。Object配列のtype hintはややトリッキー?

実行例

eval版と速度を比較してみると,当然リフレクション版の方が断然速い。

user=> (defn f1 [c args] (eval `(new ~c ~@args)))
#'user/f1
user=> (defn f2 [c args] (apply-ctor c args))
#'user/f2
user=> (time (dotimes [i 10000] (f1 String [(str "hoge" i)])))
"Elapsed time: 9086.514 msecs"
nil
user=> (time (dotimes [i 10000] (f2 String [(str "hoge" i)])))
"Elapsed time: 146.569 msecs"
nil
user=>