简体   繁体   中英

Android enum reflection

I have been trying for the past few hours to get the following functionality working.

public class OMG {

    String name;
    HashMap<SETTINGS, String> settings;

    public enum SETTINGS {
        Setting1("OMG setting 1"), Setting2("OMG setting 2");
        private String key;

        @Override
        public String toString() {
            return "SETTINGS: " + key;
        }

        SETTINGS(String key){
            this.key = key;
        }
    }

    public OMG(String name, HashMap<SETTINGS, String> settings) {
        this.name = name;
        this.settings = settings;
    }
}

and

public class Test {

    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("path.to.OMG" + "$" + "SETTINGS");

            System.out.println(Arrays.toString(c.getEnumConstants()));
            HashMap<c,String > values = new HashMap<>();
            OMG omg = new OMG("blah",values);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

I have to create an OMG instance and am been given the path to OMG and the enum NAME. So far I have the code in the Test class. I am able to get all enumValues however I have no idea how to create a HashMap and how to call the constructor with this HashMap to get an instance of the OMG class.

Please help.

Thank you

There is no practical reason for doing what you are trying to do. The way to instantiate an OMG is to do new OMG("foo", new HashMap<SETTINGS, String>());

However, as a purely academic exercise (I like these!), let's see if we can instantiate an OMG only using class names given as strings and without actually typing OMG or SETTINGS anywhere. It can be done!

In Java, generics are a compile-time feature only. At runtime, a HashMap<SETTINGS, String> is no different from a HashMap<Double, Float> or a raw HashMap . Therefore you can instantiate any old HashMap - there is no need to use the object c at all (and you can't use a Class object as a type parameter liken this anyway).

Here is a solution:

public static void main(String[] args) throws Exception {
    Class<?> clazz = Class.forName("path.to.OMG");
    Constructor constructor = clazz.getConstructor(String.class, HashMap.class);
    Object omgInstance = constructor.newInstance("foo", new HashMap<Void, Void>());
    System.out.println(omgInstance.getClass());    // OMG - it worked!
}

From your own answer, I read that you want to use enum entries as keys and default values for the actual configuration that is stored in the map in the class OMG . This configuration should also be written to a file.

Now, you have to make a design decision: When do you want to be able to change which configuration keys can be used and/or the default values? While the program is already running (at run-time) or when the program is recompiled (at compile-time)?

Your description suggests that you want to be able to change them at run-time. Is this correct? Also, is this really needed? Your existing code will probably not use values which are provided for the new keys. Also, a changed default value will probably not have an effect, since the value was already initialized using the old default value.

If you really want to change these information at run-time, then you should not use an enum to store and load them. While this style of programming might work in scripting languages like JavaScript, it does not work well in Java. An enum, like any other code is not supposed to be changed at run-time, but it should be given at compile-time.

Since in Java the code is not supposed to be changed at run-time, the JVM does not even allow to simply reload a class, without doing some class loader magic. You normally want to avoid this whenever possible. For more information about this, see here . It is not impossible to reload a class at run-time, but it is definitely the wrong solution to the problem that you are facing.

So, what is a correct solution? I am still not convinced that you really need to change the information at run-time. If this is correct and you only need to change the information at compile-time, you can simply use your current enum approach. However, you can simplify it to not load the class by name, please see below for more details about a possible implementation.

If you really need to be able to change the information at run-time, you should not store the information in the code, but in a file. This enables you to easily reload the information by re-reading the file. You can use a properties file, which is common in the Java world, see here . If you need a more structured file format, you can use a JSON file. In Java, you can use Gson to work with JSON files, see here .


This works for me:

package test;

import java.util.Arrays;
import java.util.EnumMap;

public class Test {
    public static enum SETTINGS {
        Setting1("OMG setting 1"), Setting2("OMG setting 2");

        private String key;

        @Override
        public String toString() {
            return "SETTINGS: " + key;
        }

        SETTINGS(String key) {
            this.key = key;
        }
    }

    public static class OMG<E extends Enum<E>> {
        String name;
        EnumMap<E, String> settings;

        public OMG(String name, EnumMap<E, String> settings) {
            this.name = name;
            this.settings = settings;
        }

        public static <E extends Enum<E>> OMG<E> create(String name,
                Class<E> enumClass) {
            return new OMG<E>(name, new EnumMap<E, String>(enumClass));
        }
    }

    public static void main(String[] args) {
        try {
            Class c = Class.forName("test.Test$SETTINGS");
            // alternatively you can use:
            // Class<SETTINGS> c = SETTINGS.class;

            System.out.println(Arrays.toString(c.getEnumConstants()));
            OMG.create("foobar", c);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

I assumed that you want to pass any enum type to the class OMG and not only SETTINGS . If it should only be SETTINGS , you might as well just use SETTINGS directly in your code. This is why I moved the SETTINGS enum out of the OMG class.

Next, I added a generic type parameter E to the class OMG , so OMG does not directly depend on SETTINGS . To enable easier initialization, I added a static factory method to OMG , which also uses a generic type parameter E . It only receives the name and the class objects which matches E . You need the class objects, since in Java you cannot directly use the generic type parameters for reflection.

Also, I replaced the HashMap by an EnumMap , since EnumMap is an optimized map implementation when the key is an enum.

Finally, the initialization of the OMG class: If you really need to create the class object from the name of the class, using reflection, you can use the code as is. Note that this will produce a warning. However, most of the time you want to use the class objects directly, like in the commented line of code. If you use this second method, you will not get a warning and you can also omit the try-catch block.

Thanks to @PaulBoddington I solved the issue as follows.

public class Test {

    public static void main(String[] args) {
        try {
            Class<?> c = Class.forName("path.to.OMG" + "$" + "SETTINGS");
            Object[] enumConstants = c.getEnumConstants();
            HashMap<Object, String > configuration = new HashMap<>();
            for (Object enumConstant: enumConstants){
              configuration.put(enumConstant, enumConstant.key);
            }
            OMG omg = new OMG("blah", configuration);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

The use case for people wondering is that OMG is a module in the system which is configurable and also has to allow from a developers point of view new settings to be added easily. This is why settings and their value is kept in a HashMap < SETTING , STRING > and SETTING is an enum containing the key in the configuration file under which the setting should be found as well as other information - default value of example. The OMG class also has a

String getSettingValue(SETTING setting){
    return settingsMap.get(setting);
}

method which returns the value for a setting and is what is exposed by the module.

This means that if a new setting has to be added to the component I just have to create a new instance of the SETTINGS enum and thats it. During creation if the server has not send a value for this setting I can either use the default or crash. On the other hand once the component is instantiated I have immediate access to the new setting without touching the existing code.

Please share your opinion on this architecture couse it is for a school project and I highly doubt the teacher will actually look into it and tell me if it is reasonable or not.

Thank you

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