Reputation: 20700
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
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
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
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
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
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