Why does class.getConstructor(parameters) not allow child objects as parameters?

Viewed 170

Lets say I have a parent class and a child class which extends my parent and I have the following code.

public class SomeClass {
  private Parent myParent;

  public SomeClass(Parent myParent) {
    this.myParent = myParent;
  }
}

Why is this allowed

Class<SomeClass> clazz = SomeClass.class;
SomeClass someClass = clazz.getConstructor(Parent.class).getInstance(Child);

and this isnt't

Class<SomeClass> clazz = SomeClass.class;
SomeClass someClass = clazz.getConstructor(Child.class).getInstance(Child);

The second one throws an NoSuchMethodeException. Why isn't there dynamic binding in this case but when using the normal Constructor dynamic binding works just fine? And is there a way to workaround this problem?

Edit: I'm trying to load jar files at runtime. At this moment I have the classes I need, loaded with an URLClassLoader . Next I want to create a new instance of one of the loaded classes. To create the instance i call urlClassLoader.loadClass(nameOfClass).getConstructor(parameterType).newInstance(initArguments); The parameterTypewould be child.class in this case.

2 Answers

The method getConstructorAcceptingSupertype below finds a constructor that accepts the given parameter type or one of its supertypes.

import java.lang.reflect.Constructor;

public class ConstructorTester {
    
    public ConstructorTester(Object o) {
        System.out.println("Object constructor");
    }
    
    public ConstructorTester(CharSequence o) {
        System.out.println("CharSequence constructor");
    }
    
    public static void main(String[] args) throws Throwable {
        Object[] arg = {null};
        getConstructorAcceptingSupertype(ConstructorTester.class, Object.class).newInstance(arg); //Object constructor
        getConstructorAcceptingSupertype(ConstructorTester.class, CharSequence.class).newInstance(arg); //CharSeq constructor
        getConstructorAcceptingSupertype(ConstructorTester.class, Integer.class).newInstance(arg); //Object constructor
        getConstructorAcceptingSupertype(ConstructorTester.class, String.class).newInstance(arg); //CharSeq constructor
        getConstructorAcceptingSupertype(ConstructorTester.class, StringBuilder.class).newInstance(arg); //CharSeq constructor
    }
    
    /**
     * Returns a one-arg {@link Constructor} from {@code clazz} that accepts the given {@code parameterType}. This
     * method guarantees that there is no other one-arg constructor in {@code clazz} that accepts a superclass or
     * superinterface of the type that the returned constructor accepts. 
     */
    private static Constructor<?> getConstructorAcceptingSupertype(Class<?> clazz, Class<?> parameterType) {
        Constructor<?> correctConstructor = null;
        for(Constructor<?> constructor : clazz.getConstructors()) {
            if( constructor.getParameterCount() == 1 &&
                constructor.getParameters()[0].getType().isAssignableFrom(parameterType)) {
                if(correctConstructor == null) {
                    correctConstructor = constructor;
                }
                else { //see if this constructor is more specific than the current correctConstructor.
                    Class<?> currentType = correctConstructor.getParameters()[0].getType();
                    Class<?> newType = constructor.getParameters()[0].getType();
                    if(currentType.isAssignableFrom(newType))
                        correctConstructor = constructor;
                }
            }
        }
        if(correctConstructor == null)
            throw new IllegalArgumentException("No one-arg constructor exists that accepts the given parameter type");
        return correctConstructor;
    }
    
}

Note that there is no way to avoid an ambiguous situation where an interface, say C, extends two other interfaces A and B, and your class has two constructors (SomeClass(A) and SomeClass(B)). In such a scenario, even an explicit constructor invocation such as new SomeClass(instanceOfC) would fail at compile-time. In such a scenario, My method above will return an arbitrary constructor (either SomeClass(A) or SomeClass(B)), but does not guarantee which.

The problem is w.l.o.g. not solvable since we can create examples that are ambiguous. Consider the following example:

interface Foo {}

class Bar {
  public Bar() {}

  public Bar(Bar other) {}

  public Bar(Foo other) {}
}

class Baz extends Bar implements Foo {}

If we now call new Bar(new Baz()), the program will not compile.

Ideone demo

Hence, Class.getConstructor(Class<?>... parameterTypes) interprets the types as formal parameters, not actual parameters.


I would strongly advice to not implement a solution that, given a situation like above, "chooses" a constructor for us. Instead, I would opt to:

  • either throw some Exception, signaling that no singular constructor can be determined, or
  • return a List<Constructor<?>>.

For the second approach (returning a List<...>), we can use the following code:

public static List<Constructor<?>> getConstructorsFromTypeFittingActualParameters(
    Class<?> type,
    Class<?>... actualParameters) {
  final Class<?>[] actualParametersNotNull = 
      Optional.ofNullable(actualParameters).orElseGet(() -> new Class<?>[0]);
  return Arrays.stream(type.getConstructors())
      .filter(constructor -> constructor.getParameterCount() == actualParametersNotNull.length)
      .filter(constructor -> formalParameterTypesAcceptActualParameterTypes(
          constructor.getParameterTypes(),
          actualParametersNotNull))
      .collect(Collectors.toList());
}

private static boolean formalParameterTypesAcceptActualParameterTypes(
    Class<?>[] formalParameterTypes,
    Class<?>[] actualParameterTypes) {
  Objects.requireNonNull(formalParameterTypes);
  Objects.requireNonNull(actualParameterTypes);
  if (formalParameterTypes.length != actualParameterTypes.length) {
    throw new IllegalArgumentException();
  }

  for (int index = 0; index < formalParameterTypes.length; ++index) {
    if (!formalParameterTypes[index].isAssignableFrom(actualParameterTypes[index])) {
      return false;
    }
  }
  return true;
}

Given the above example, this implementation will return two constructors.

Ideone example

Related