How can I instantiate a generic type in Java?

后端 未结 6 853
遥遥无期
遥遥无期 2020-12-17 16:26

I\'ve added a human-readable configuration file to my app using java.util.Properties and am trying to add a wrapper around it to make type conversions easier. Specifically,

相关标签:
6条回答
  • 2020-12-17 16:53

    Generics are implemented using type erasure in Java. In English terms, most generic information are lost at compile time, and you can't know the actual value of T at runtime. This means you simply can't instanciate generic types.

    An alternate solution is to provide your class with the type at runtime:

    class Test<T> {
    
        Class<T> klass;
    
        Test(Class<T> klass) {
            this.klass = klass;
        }
    
        public void test() {
            klass.newInstance(); // With proper error handling
        }
    
    }
    

    Edit: New example closer to your case

    static <T> T getProperty(String key, T fallback, Class<T> klass) {
        // ...
    
        if (value == null) {
            return fallback;
        }
        return (T) klass.newInstance(); // With proper error handling
    }
    
    0 讨论(0)
  • 2020-12-17 16:55

    Try this:

    protected <T> T getProperty(String key, T fallback) {
        String value = properties.getProperty(key);
    
        if (value == null) {
            return fallback;
        } else {
            Class FallbackType = fallback.getClass();
            return (T)FallbackType.cast(value);
        }
    }
    
    0 讨论(0)
  • 2020-12-17 16:57

    The following uses functional interfaces.

    You could change the method signature to use a provided "parser":

    protected <T> T getProperty(String key, T fallback, Function<String, ? extends T> parser) {
        String value = properties.getProperty(key);
    
        if (value == null) {
            return fallback;
        } else {
            return parser.apply(value);
        }
    }
    

    Also, for efficiency you might want to replace T fallback with Supplier<? extends T> fallbackSupplier to prevent having to create fallback values when they are not needed:

    protected <T> T getProperty(String key, Supplier<? extends T> fallbackSupplier, Function<String, ? extends T> parser) {
        String value = properties.getProperty(key);
    
        if (value == null) {
            return fallbackSupplier.get();
        } else {
            return parser.apply(value);
        }
    }
    

    Then you can use method references and lambda expressions as parser and fallback supplier, e.g.:

    protected void func2() {
        foobar = getProperty("foobar", () -> 210, Integer::valueOf);
        // Better create own parsing method which properly handles 
        // invalid boolean strings instead of using Boolean#valueOf
        foobaz = getProperty("foobaz", () -> true, Boolean::valueOf);
    
        // Imagine creation of `ExpensiveObject` is time-wise or 
        // computational expensive
        bar = getProperty("bar", ExpensiveObject::new, ExpensiveObject::parse);
    }
    

    The advantage of this approach is that it is not restricted to a (potentially non-existent) constructor anymore.

    0 讨论(0)
  • 2020-12-17 17:05

    If you want to keep your existing method signature do it this way.

    import java.lang.reflect.InvocationTargetException;
    import java.util.Properties;
    
    public class Main
    {
        private final Properties properties;
    
        public Main()
        {
            this.properties  = new Properties();
            this.properties.setProperty("int", "1");
            this.properties.setProperty("double", "1.1");
        }
    
        public <T> T getProperty(final String key, final T fallback)
        {
            final String value = this.properties.getProperty(key);
            if (value == null)
            {
                return fallback;
            }
            else
            {
                try
                {
                    return (T) fallback.getClass().getConstructor(new Class<?>[] { String.class } ).newInstance(value);
                }
                catch (final InstantiationException e)
                {
                    throw new RuntimeException(e);
                }
                catch (final IllegalAccessException e)
                {
                    throw new RuntimeException(e);
                }
                catch (final InvocationTargetException e)
                {
                    throw new RuntimeException(e);
                }
                catch (final NoSuchMethodException e)
                {
                    throw new RuntimeException(e);
                }
            }
        }
    
    
        public static void main(final String[] args)
        {
            final Main m = new Main();
            final Integer i = m.getProperty("int", new Integer("0"));
            final Double d = m.getProperty("double", new Double("0"));
            System.out.println(i);
            System.out.println(d);
        }
    }
    
    0 讨论(0)
  • 2020-12-17 17:12

    This is something that you cannot do.

    Because of type erasure, the type T, while known at compile time, is not available to the JVM at run time.

    For your specific problem, I think the most reasonable solution is to manually write the code for each different type:

    protected String getProperty(String key, String fallback) { ... return new String(value); }
    protected Double getProperty(String key, Double fallback) { ... return new Double(value); }
    protected Boolean getProperty(String key, Boolean fallback) { ... return new Boolean(value); }
    protected Integer getProperty(String key, Integer fallback) { ... return new Integer(value); }
    

    Notes:

    • In the Java standard API, you will find many places where there is a set of related methods that only differ by the input types.
    • In C++, your probably could probably be solved by templates. But C++ introduces many other problems...
    0 讨论(0)
  • 2020-12-17 17:17

    Due to type erasure, you can't instantiate generic objects. Normally you could keep a reference to the Class object representing that type and use it to call newInstance(). However, this only works for the default constructor. Since you want to use a constructor with parameters, you'll need to look up the Constructor object and use it for the instantiation:

    protected <T> T getProperty(String key, T fallback, Class<T> clazz) {
        String value = properties.getProperty(key);
    
        if (value == null) {
            return fallback;
        } else {
    
            //try getting Constructor
            Constructor<T> constructor;
            try {
                constructor = clazz.getConstructor(new Class<?>[] { String.class });
            }
            catch (NoSuchMethodException nsme) {
                //handle constructor not being found
            }
    
            //try instantiating and returning
            try {
                return constructor.newInstance(value);
            }
            catch (InstantiationException ie) {
                //handle InstantiationException
            }
            catch (IllegalAccessException iae) {
                //handle IllegalAccessException
            }
            catch (InvocationTargetException ite) {
                //handle InvocationTargetException
            }
        }
    }
    

    However, seeing how much trouble it is to achieve this, including the performance cost of using reflection, it's worth looking into other approaches first.

    If you absolutely need to take this route, and if T is limited to a distinct set of types known at compile time, a compromise would be to keep a static Map of Constructors, which is loaded at startup - that way you don't have to dynamically look them up at every call to this method. For example a Map<String, Constructor<?>> or Map<Class<?>, Constructor<?>>, which is populated using a static block.

    0 讨论(0)
提交回复
热议问题