viebel
viebel

Reputation: 20700

Using Java Reflection, how to get the constructor of a class specifying a derived class of the constructor args?

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?

Upvotes: 5

Views: 3124

Answers (5)

Michał Marczyk
Michał Marczyk

Reputation: 84379

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)))))

Upvotes: 4

user1025189
user1025189

Reputation: 71

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.

Upvotes: 0

esaj
esaj

Reputation: 16035

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.

Upvotes: 5

yadab
yadab

Reputation: 2143

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

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1075587

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.

Upvotes: 5

Related Questions