5

With Java reflection, one can get a constructor through getConstructor(klass, args).

However, when we pass as args a derived class of the class specified in the constructor signature, it fails. How to overcome this issue?

For example,

HashSet.class.getConstructor(new Class[]{ HashSet.class });

fails. While

HashSet.class.getConstructor(new Class[]{ Collection.class });

succeeds.

I am looking for something that could easily be used in clojure. Therefore, I would prefer to have something out of the box and not having to add user-defined functions.

Any idea, how to solve this issue?

viebel
  • 19,372
  • 10
  • 49
  • 83

5 Answers5

5

HashSet has no HashSet(HashSet) constructor, so naturally you don't get one when you ask for it. You have to work your way through the assignment-compatible classes (at least loop through the supers, and probably the implemented interfaces and their supers) to find one.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Why can't this be done by the introspection infrastructure ? I really need this functionality for a challenging issue in clojure. – viebel Feb 09 '12 at 17:49
  • It's not available because it's hard. You'd have to replicate all the relevant Java Language Specification rules that are implemented into javac. Clojure's Reflector code omits a large subset of the possible allowed calls (e.g., boxing/unboxing of arguments), hence the need to sometimes provide type-hints/casting when not strictly necessary. – Alex Taggart Feb 12 '12 at 21:49
  • The difficulty of this task should be made apparent by the fact that Sun didn't even include support for it in their own reflection API (hence Clojure's partial support via Reflector). – Alex Taggart Feb 12 '12 at 21:56
5

Here's a fairly simple way of doing this. The getConstructorForArgs -method walks through all the constructors in given class, and checks to see if the parameters of the constructor match the parameters given (note that the given parameters must be in the same order as in the constructor). Implementations of interfaces and sub-classes work also, because the "compatibility" is checked by calling isAssignableFrom for the constructor argument (is the given parameter type assignable to parameter type in constructor).

public class ReflectionTest
{
    public Constructor<?> getConstructorForArgs(Class<?> klass, Class[] args)
    {
        //Get all the constructors from given class
        Constructor<?>[] constructors = klass.getConstructors();

        for(Constructor<?> constructor : constructors)
        {
            //Walk through all the constructors, matching parameter amount and parameter types with given types (args)
            Class<?>[] types = constructor.getParameterTypes();
            if(types.length == args.length)
            {               
                boolean argumentsMatch = true;
                for(int i = 0; i < args.length; i++)
                {
                    //Note that the types in args must be in same order as in the constructor if the checking is done this way
                    if(!types[i].isAssignableFrom(args[i]))
                    {
                        argumentsMatch = false;
                        break;
                    }
                }

                if(argumentsMatch)
                {
                    //We found a matching constructor, return it
                    return constructor;
                }
            }
        }

        //No matching constructor
        return null;
    }

    @Test
    public void testGetConstructorForArgs()
    {
        //There's no constructor in HashSet that takes a String as a parameter
        Assert.assertNull( getConstructorForArgs(HashSet.class, new Class[]{String.class}) );

        //There is a parameterless constructor in HashSet
        Assert.assertNotNull( getConstructorForArgs(HashSet.class, new Class[]{}) );

        //There is a constructor in HashSet that takes int as parameter
        Assert.assertNotNull( getConstructorForArgs(HashSet.class, new Class[]{int.class}) );

        //There is a constructor in HashSet that takes a Collection as it's parameter, test with Collection-interface
        Assert.assertNotNull( getConstructorForArgs(HashSet.class, new Class[]{Collection.class}) );

        //There is a constructor in HashSet that takes a Collection as it's parameter, and HashSet itself is a Collection-implementation
        Assert.assertNotNull( getConstructorForArgs(HashSet.class, new Class[]{HashSet.class}) );

        //There's no constructor in HashSet that takes an Object as a parameter
        Assert.assertNull( getConstructorForArgs(HashSet.class, new Class[]{Object.class}) );

        //There is a constructor in HashSet that takes an int as first parameter and float as second
        Assert.assertNotNull( getConstructorForArgs(HashSet.class, new Class[]{int.class, float.class}) );

        //There's no constructor in HashSet that takes an float as first parameter and int as second
        Assert.assertNull( getConstructorForArgs(HashSet.class, new Class[]{float.class, int.class}) );
    }   
}

Edit: Note that this solution is NOT perfect for all cases: if there are two constructors, that have a parameter which is assignable from a given parameter type, the first one will be chosen, even if the second was a better fit. For example, if SomeClass would have a constructor that takes a HashSet (A Collection-implementation) as a parameter, and a constructor taking a Collection as a parameter, the method could return either one when searching for a constructor accepting a HashSet as parameter, depending on which came first when iterating through the classes. If it needs to work for such cases also, you need to first collect all the possible candidates, that match with isAssignableFrom, and then do some more deeper analysis to the candidates to pick the best suited one.

esaj
  • 15,875
  • 5
  • 38
  • 52
  • Nice. But I am looking for something that could easily be used in `clojure`. Therefore I would prefer to have something out of the box and not having to add user-defined functions. Updated the question. – viebel Feb 11 '12 at 21:35
  • 1
    @YehonathanSharvit I'm not familiar with clojure, but the method itself can be moved to a class of your choosing and declared static, so if you can at least call static methods from clojure, this should work (see the edits about possible pitfalls at the end of my answer, though). – esaj Feb 11 '12 at 22:04
  • Do you understand why this feature is not supported by the reflection infrastructure? – viebel Feb 11 '12 at 22:15
  • 1
    No, I don't really understand why there's no support for this out-of-the-box (AFAIK). My best guess is that the people defining the reflection functionality either overlooked this, or thought it wouldn't be needed. It's doable with custom-code though, including avoiding the pitfall mentioned in my edit (but that is somewhat complex, and could potentially affect performance, if used a lot, because it requires walking up + branching around the inheritance-tree and keeping count of how "far" up the exact parameter matches are). – esaj Feb 11 '12 at 22:29
  • Regardless of how entertaining it may be to speculate as to why, there is no question that this feature is not supported by the reflection API. The Java method resolution algorithm is surprisingly complicated; it was already complicated ten years ago before the introduction of features like varargs and Generics. See http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#20448 for the specification. – user100464 Feb 12 '12 at 00:27
  • @Yehonathan Sharvit, would you be satisfied with a Clojure implementation of the above Java code fragment? – user100464 Feb 12 '12 at 00:30
  • @user100464: I Will be satisfied at 2 conditions: 1. Nobody else finds a better solution. 2. You solve the issue mentioned mentioned by esaj in their answer. In that case, you will get the +50 bounty. – viebel Feb 12 '12 at 08:29
4

Building on the answers of esaj and T.J. Crowder:

The following returns a seq of constructors for the given class which are (1) callable with the specified argument types and (2) optimal in the sense that their declared parameter types are removed by a minimal number of steps up the inheritance ladder from the specified argument types. (Thus an exact match will always be returned alone; if there are two constructors which require casting from some of the specified argument types to their grandparent types, and there is no closer match, they will both be returned; if there are no matching constructors at all, nil will be returned.) Primitive argument types may be specified as symbols or keywords (i.e. 'int / :int). Finally, primitive types are considered equivalent to their boxed counterparts.

Example:

user> (find-best-constructors java.util.HashSet [:int :float])
(#<Constructor public java.util.HashSet(int,float)>)
user> (find-best-constructors java.util.HashSet [java.util.HashSet])
(#<Constructor public java.util.HashSet(java.util.Collection)>)
user> (find-best-constructors java.util.HashSet [Integer])
(#<Constructor public java.util.HashSet(int)>)

One might want to permit widening numeric conversions; that could be done e.g. by adding Integer -> Long etc. mappings to convm and tweaking the if condition in count-steps below.

Here's the code:

(defn find-best-constructors [klass args]
        (let [keym {:boolean Boolean/TYPE
                    :byte    Byte/TYPE
                    :double  Double/TYPE
                    :float   Float/TYPE
                    :int     Integer/TYPE
                    :long    Long/TYPE
                    :short   Short/TYPE}
              args (->> args
                        (map #(if (class? %) % (keyword %)))
                        (map #(keym % %)))
              prims (map keym [:boolean :byte :double :float :int :long :short])
              boxed [Boolean Byte Double Float Integer Long Short]
              convm (zipmap (concat prims boxed) (concat boxed prims))
              ctors (->> (.getConstructors klass)
                         (filter #(== (count args) (count (.getParameterTypes %))))
                         (filter #(every? (fn [[pt a]]
                                            (or (.isAssignableFrom pt a)
                                                (if-let [pt* (convm pt)]
                                                  (.isAssignableFrom pt* a))))
                                          (zipmap (.getParameterTypes %) args))))]
          (when (seq ctors)
            (let [count-steps (fn count-steps [pt a]
                                (loop [ks #{a} cnt 0]
                                  (if (or (ks pt) (ks (convm pt)))
                                    cnt
                                    (recur (set (mapcat parents ks)) (inc cnt)))))
                  steps (map (fn [ctor]
                               (map count-steps (.getParameterTypes ctor) args))
                             ctors)
                  m (zipmap steps ctors)
                  min-steps (->> steps
                                 (apply min-key (partial apply max))
                                 (apply max))]
              (->> m
                   (filter (comp #{min-steps} (partial apply max) key))
                   vals)))))
Community
  • 1
  • 1
Michał Marczyk
  • 83,634
  • 13
  • 201
  • 212
  • Wow!! Quite a bit of code. Could you please provide a runnable clj file that demonstrates how to call `find-best-constructors` with `HashSet` for instance. – viebel Feb 13 '12 at 09:15
  • Well, there are three examples in the answer. You can copy the expressions to the right of `user>` into your own REPL / file and verify that the values returned are as advertised. – Michał Marczyk Feb 13 '12 at 17:27
  • Somehow, I get an error when importing HashSet. You can access my file at: [find-constructors.clj](https://github.com/viebel/Learning/blob/master/clojure/find-constructors.clj). – viebel Feb 13 '12 at 18:11
  • 1
    Your `ns` syntax is off: it should be `(ns test (:import java.util.HashSet))` or `(ns test (:import (java.util HashSet)))`. Also, the names of your namespace and the file it is defined it should agree, unless you're only planning to run this file as a script and never to require it from the classpath; plus the filename should use `_` in place of `-`. Additionally, single-segment namespace names cause problems. So you might want to call the namespace `find-constructors.test` and place it in a file `find_constructors/test.clj`, where `find_constructors` is a directory on the classpath. – Michał Marczyk Feb 13 '12 at 19:44
  • See e.g. [my answer](http://stackoverflow.com/a/2459611/232707) to an old SO question on Clojure's expectations w.r.t. the classpath and the source tree layout for more details. Better yet, use [Leiningen](https://github.com/technomancy/leiningen) to manage your project and follow its conventions. It is very well documented and is the most popular project automation tool in the Clojure community, so learning about it is a good use of time for anyone interested in doing anything serious with Clojure. – Michał Marczyk Feb 13 '12 at 19:54
  • Ok thanks. Now, I have 2 questions: 1. How could I write an assertion in clojure to check the find-best-constructore? I tried `(= (find-best-constructors java.util.HashSet [:int :float]) (#)` but it caused an exception. 2. How could I call one of the constructors from clojure? – viebel Feb 14 '12 at 07:04
  • @Yehonathan Sharvit Perhaps you could post a new question for that. – user100464 Feb 14 '12 at 15:58
  • @YehonathanSharvit: 1. `#` is not a readable representation of a `Constructor` object; in fact, `#<...>` is the conventional unreadable representation. If you want to make sure that you got the correct constructor e.g. in a test, take the `Constructor` object, call its `.getDeclaringClass` and `.getParameterTypes` methods and check that they are `identical?` to the appropriate class objects (e.g. `Integer/TYPE` for `int`). 2. See the `construct` function in [mikera's answer](http://stackoverflow.com/a/9186889/232707) to the question you linked to above -- it does exactly this. – Michał Marczyk Feb 14 '12 at 23:24
0

Do not confuse polymorphic behavior here. Because, you are passing Collection as concrete value not param type in (new Class[]{Collection}).

yadab
  • 2,063
  • 1
  • 16
  • 24
0

I think, you can get the parent class and a list of all implemented Interfaces --> so you can check for the constructor of Hashset first. If nothing is found, you can do that recursively for all parent classes and interfaces until you find some matching one.