简体   繁体   中英

Java reflection create instance of unknown class and constructor

I've already searched on Google and StackOverflow but I haven't found the answer yet..

I try to instantiate a class by name with constructor without knowing the types of the arguments of the constructor.

  1. I know the package and class name of the class (it is given to the method as string)
  2. I know the arguments of the constructor (they're also given to the method, but as an object array)

It has to be something similar to this:

// ...  Load class using ClassLoader if not in classpath ...

// These are given as arguments to the function:
String pncn = "Some.Package.UnknownClass"; // packagename classname
 Object[] oa =new Object[] { 
     "Test0", 
    new Random() 
};///object array

Class cl = Class.forName(pncn);

Class[] ca = /* ? */;
// (Here I need to get all constructor argument classes by the objects in 'oa'

// Get the constructor with the given arg types of 'ca'
 Constructor  co = cl.getConstructor(ca);

//And then instantiate the class
Object ro = co.newInstance(ca) ;

/*//return it:
return ro;*/;

So basically the issue is how to transform the array of objects of different types ('oa' in the example) into an array of Class?

I plan to use this method to create instances of classes by name and arguments through javascript.

For the simple case with lots of assumptions: You loop over oa , call getClass() on each object and populate ca with the result.

If your routine has to be a bit more robust, you have to consider more cases:

  • You have to check for null . Since null is acceptable for every reference type, you may have to iterate over all possible constructors as well in order to find a match. There is not guaranteed to be a unambiguous match.
  • And then there's primitives, too. If a constructor has a primitive parameter, you have to check for that as well because your object array will only contain reference types and thus getClass() will return the class object for java.lang.Integer instead of Integer.TYPE , the class object for the primitive type int .

In short: You have to re-implement what the compiler already does for the static case.

Try this:

private static Class<?>[] getClasses(Object[] oa) {
    if (oa == null) return new Class[0];
    Class<?>[] ret = new Class[oa.length];
    for (int i = 0; i < oa.length; i++) {
        ret[i] = oa[i].getClass();
    }
    return ret;
}

I haven't tested following extensively, but it seems to find match for args of primitve types and chooses most specific ctor for creation of new object. However, it may have errors (the one I've found is that it has no auto conversion of primitive values for chosen ctor, eg, if chosen ctor has short param and you pass int in object array, it'll fail inside newInstance ). Feel free to improve on this :)

class B {
    @Override
    public String toString() {
        return "B{}";
    }
}
class D extends B {
    @Override
    public String toString() {
        return "D{}";
    }
}
class E extends D {
    @Override
    public String toString() {
        return "E{}";
    }
}

class Test {
    final short primitive;
    final B obj;

    Test() {
        System.out.println("()");
        primitive = 42;
        obj = new D();
    }

    Test(short primitive, B obj) {
        System.out.println("B()");
        this.primitive = primitive;
        this.obj = obj;
    }

    Test(short primitive, D obj) {
        System.out.println("D()");
        this.primitive = primitive;
        this.obj = obj;
    }

    @Override
    public String toString() {
        return "Test{" +
                "primitive=" + primitive +
                ", obj=" + obj +
                '}';
    }
}


class Junk {

    // sorts lists of param classes in order from the most to the least specific one
    private static final Comparator<? super Constructor<?>> CTOR_COMPARATOR = new Comparator<Constructor<?>>() {
        @Override
        public int compare(Constructor<?> ctorA, Constructor<?> ctorB) {
            Class<?>[] params1 = ctorA.getParameterTypes();
            Class<?>[] params2 = ctorB.getParameterTypes();

            if (params1.length != params2.length)
                throw new IllegalArgumentException(ctorA + " can't be compared to " + ctorB);

            for (int i = 0; i < params1.length; i++) {
                Class<?> aClass = params1[i];
                Class<?> bClass = params2[i];
                if (!aClass.equals(bClass)) {
                    if (aClass.isAssignableFrom(bClass)) return 1;
                    if (bClass.isAssignableFrom(aClass)) return -1;
                    throw new IllegalArgumentException(ctorA + " can't be compared to " + ctorB +
                            ": args at pos " + i + " aren't comparable: " + aClass + " vs " + bClass);
                }
            }

            return 0;
        }
    };

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println(tryToCreateBestMatch(Test.class, new Object[]{(short)1, new B()}));
        System.out.println(tryToCreateBestMatch(Test.class, new Object[]{(short)1, new D()}));
        System.out.println(tryToCreateBestMatch(Test.class, new Object[]{null, new B()}));
        System.out.println(tryToCreateBestMatch(Test.class, new Object[]{(short)1, new E()}));
        System.out.println(tryToCreateBestMatch(Test.class, new Object[]{}));
        System.out.println(tryToCreateBestMatch(Test.class, new Object[]{"will fail"}));
    }

    private static <T> T tryToCreateBestMatch(Class<T> aClass, Object[] oa) throws InstantiationException, IllegalAccessException, InvocationTargetException {
        //noinspection unchecked
        Constructor<T>[] declaredConstructors = (Constructor<T>[]) aClass.getDeclaredConstructors();
        Class<?>[] argClasses = getClasses(oa);
        List<Constructor<T>> matchedCtors = new ArrayList<>();
        for (Constructor<T> ctr : declaredConstructors) {
            Class<?>[] parameterTypes = ctr.getParameterTypes();
            if (ctorMatches(parameterTypes, argClasses)) {
                matchedCtors.add(ctr);
            }
        }

        if (matchedCtors.isEmpty()) return null;

        Collections.sort(matchedCtors, CTOR_COMPARATOR);
        return matchedCtors.get(0).newInstance(oa);
    }

    private static boolean ctorMatches(Class<?>[] ctrParamTypes, Class<?>[] argClasses) {
        if (ctrParamTypes.length != argClasses.length) return false;
        for (int i = 0; i < ctrParamTypes.length; i++) {
            Class<?> ctrParamType = ctrParamTypes[i];
            Class<?> argClass = argClasses[i];

            if (!compatible(ctrParamType, argClass)) return false;
        }
        return true;
    }

    private static boolean compatible(Class<?> ctrParamType, Class<?> argClass) {
        if (ctrParamType.isAssignableFrom(argClass)) return true;
        if (ctrParamType.isPrimitive()) return compareAgainstPrimitive(ctrParamType.getName(), argClass);
        return false;
    }

    private static boolean compareAgainstPrimitive(String primitiveType, Class<?> argClass) {
        switch (primitiveType) {
            case "short":case "byte" :case "int":case "long":
                return INTEGER_WRAPPERS.contains(argClass.getName());
            case "float":case "dobule":
                return FP_WRAPPERS.contains(argClass.getName());
        }
        throw new IllegalArgumentException("Unexpected primitive type?!?!: " + primitiveType);
    }

    private static final HashSet<String> INTEGER_WRAPPERS = new HashSet<>(Arrays.asList(
            "java.lang.Integer", "java.lang.Short", "java.lang.Byte", "java.lang.Long"
            ));
    private static final HashSet<String> FP_WRAPPERS = new HashSet<>(Arrays.asList(
            "java.lang.Float", "java.lang.Double"
            ));

    private static Class<?>[] getClasses(Object[] oa) {
        if (oa == null) return new Class[0];
        Class<?>[] ret = new Class[oa.length];
        for (int i = 0; i < oa.length; i++) {
            ret[i] = oa[i] == null ? Object.class : oa[i].getClass();
        }
        return ret;
    }

}

I finally got it to work after 3 hours of researching and trying out different solutions.

I used information from this answer (to a question about primitive types in constructors), @Viktor Sorokin's answer and @musiKk's answer to my question. I'm can create instances of classes by only knowing the class name/package path and constructor arguments (as array of objects), capable of using primitives, objects, arrays of primitives and arrays of objects.

The code is to long to post it here :/

The easy way to do this is with java.beans.Expression. It contains all the same type logic as the compiler. Just execute a string expression of the form "new MyClass" and supply the object array as the parameters. The value of the Expression is the new object. Doing it manually isn quite difficult as you have to take care of primitives and base classes for every parameter.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM