简体   繁体   中英

Java: Creating objects with variable type and calling methods on an object with variable name

I have a hashmap with keys of type String and values of type ArrayList.

It looks something like this:

key: ".avi", value: {new VideoResource(var), "videoResources"}

I want to do the following things:

1. Create a new variable with the same type as that of the one found in the hashmap. This time I would want to create a new VideoResource, but it could be anything (AudioResource, ImageResource...) depending on what's inside the hashmap. The types always take the same parameter "var", however.

So, I want to do:

SomeResource resource = new SomeResource(var);

where SomeResource is decided by the type of some object.

2. Call a method on an object, that is previously instantiated, with the name given by

String objectName = hashMap.get(key).get(1);

The object will always have the method and the method being called will always have the same name.

So, I want to do:

objectName.methodName();

where objectName is decided by some string.

How can I achieve this?

EDIT

The context is this:

For an assignment I've been tasked to refactor a bit of code in the open-source project FreeCol. The method I am refactoring is createResource shown below. The problem with it is, that in addition to the very repetitive if-statements, it violates the open/closed principle. We want it to be open for extension, ie adding a new extension type (.avi etc.), but closed for modification, ie you shouldn't have to modify a whole bunch of code to do so.

The method looks like this:

public static void createResource(URI uri, ResourceSink output) {
if(findResource(uri, output))
    return;

try {
    if ("urn".equals(uri.getScheme())) {
        if (uri.getSchemeSpecificPart().startsWith(ColorResource.SCHEME)) {
            ColorResource cr = new ColorResource(uri);
            output.add(cr);
            colorResources.put(uri, new WeakReference<>(cr));
        } else if (uri.getSchemeSpecificPart().startsWith(FontResource.SCHEME)) {
            FontResource fr = new FontResource(uri);
            output.add(fr);
            fontResources.put(uri, new WeakReference<>(fr));
        }
    } else if (uri.getPath().endsWith("\"")
            && uri.getPath().lastIndexOf('"',
                    uri.getPath().length()-1) >= 0) {
        StringResource sr = new StringResource(uri);
        output.add(sr);
        stringResources.put(uri, new WeakReference<>(sr));
    } else if (uri.getPath().endsWith(".faf")) {
        FAFileResource far = new FAFileResource(uri);
        output.add(far);
        fafResources.put(uri, new WeakReference<>(far));
    } else if (uri.getPath().endsWith(".sza")) {
        SZAResource szr = new SZAResource(uri);
        output.add(szr);
        szaResources.put(uri, new WeakReference<>(szr));
    } else if (uri.getPath().endsWith(".ttf")) {
        FontResource fr = new FontResource(uri);
        output.add(fr);
        fontResources.put(uri, new WeakReference<>(fr));
    } else if (uri.getPath().endsWith(".wav")) {
        AudioResource ar = new AudioResource(uri);
        output.add(ar);
        audioResources.put(uri, new WeakReference<>(ar));
    } else if (uri.getPath().endsWith(".ogg")) {
        if (uri.getPath().endsWith(".video.ogg")) {
            VideoResource vr = new VideoResource(uri);
            output.add(vr);
            videoResources.put(uri, new WeakReference<>(vr));
        } else {
            AudioResource ar = new AudioResource(uri);
            output.add(ar);
            audioResources.put(uri, new WeakReference<>(ar));
        }
    } else {
        ImageResource ir = new ImageResource(uri);
        output.add(ir);
        imageResources.put(uri, new WeakReference<>(ir));
    }
    catch (Exception e) {
        logger.log(Level.WARNING, "Failed to create resource with URI: " + uri, e);
    }
}

So what I want to do, is to get rid of all the else ifs that handle the file extensions, and replace them with a single call to a method assignResource that creates the right resource

SomeResource resource = new SomeResource(uri);

adds it to the output

output.add(resource);

and puts it in the right WeakHashMap

someResource.put(uri, new WeakReference<>(resource));

These hashmaps are declared as following:

private static final Map<URI, WeakReference<ColorResource>> colorResources = new WeakHashMap<>();
private static final Map<URI, WeakReference<FontResource>> fontResources = new WeakHashMap<>();
private static final Map<URI, WeakReference<StringResource>> stringResources = new WeakHashMap<>();
private static final Map<URI, WeakReference<FAFileResource>> fafResources = new WeakHashMap<>();
private static final Map<URI, WeakReference<SZAResource>> szaResources = new WeakHashMap<>();
private static final Map<URI, WeakReference<VideoResource>> videoResources = new WeakHashMap<>();
private static final Map<URI, WeakReference<ImageResource>> imageResources = new WeakHashMap<>();

I have code that extracts the file extension from the URI, and puts it as a key, along with the associated object (ie VideoResource()) and the name of the object upon with we want to invoke the put() method (ie "videoResources"), as values.

The idea is that if you would add a new extension type, you just have to do one function call to my method addResource:

addResource(resourceMap, ".ogg", new VideoResource(uri), "videoResources");

that adds these parameters to the map of file extensions it can handle.

Instead of all the else if statements, a single call to a method assignResource

assignResource(uri, resourceMap);

would be made.

So the problem I faced was how to create a new object of a type that matches that of the one found in my hashmap, and then invoking method put() on the right WeakHashMap (videoResources etc.).

EDIT

More questions.

The line output.add(resource); gives an error because resource is a Resource and not one of the subtypes. The next line, resources.add(uri, resource); complains about type safety, that references should be parameterized. I changed the interface to your second suggested generic one. An example of what a Resources implementation now looks like is this:

class StringResources implements Resources<StringResource> {

    private final Map<URI, WeakReference<Resource>> resources = new WeakHashMap<>();
    @Override
    public boolean matches(URI uri) {
        return uri.getPath().endsWith("\"")
            && uri.getPath().lastIndexOf('"', uri.getPath().length() - 1) >= 0;
    }

    @Override
    public StringResource from(URI uri) {
        return new StringResource(uri);
    }

    @Override
    public void add(URI uri, StringResource resource) {
        resources.put(uri, new WeakReference<>(resource));
    }  
}

Is this how you meant they should look? In that case, how should we change the lines

Resource resource = resources.from(uri);
output.add(resource);
resources.add(uri, resource);

so that the resource is of the right subtype when we call output.add?

I'll give you the answer for your question but what you're trying to do is most likely not the best solution for whatever problem you have. Providing more context could result in better approach.

class VideoResource {

    public VideoResource(Object var) {
    }

    public void videoResources() {
        System.out.println("It was called");
    }
}

class Example {

    public static void main(String[] args) {
        String key = ".avi";

        Map<String, List<Object>> map = prepareExample(key);

        Class<?> unknownClass = map.get(key).get(0).getClass();

        try {
            // getConstructor accepts class of parameter type
            Constructor<?> constructor = unknownClass.getConstructor(Object.class);
            Object newInstance = constructor.newInstance("abc");
            Method method = newInstance.getClass().getMethod((String) map.get(key).get(1));
            method.invoke(newInstance);
        } catch (NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private static Map<String, List<Object>> prepareExample(String key) {
        Map<String, List<Object>> map = new HashMap<>();
        List<Object> list = new ArrayList<>();
        list.add(new VideoResource(null));
        list.add("videoResources");
        map.put(key, list);
        return map;
    }
}

Edit:

Alright. According to presented context I have a proposition of solution. I would start with creating two interfaces:

interface Resource {
}

interface Resources {
    boolean matches(URI uri);
    Resource from(URI uri);
    void add(URI uri, Resource resource);
}

I believe there must be some common interface for those resources as ResourceSink class accepts any of them. I didn't create a full solution but you should be able to do it yourself after a while of explanation. Example implementations of both interfaces are presented below for two resources existing in this project:

class ColorResource implements Resource {
    ColorResource(URI uri) {
    }
}

class ColorResources implements Resources {

    private final String COLOR_RESOURCE_SCHEMA = "";

    private final Map<URI, WeakReference<Resource>> resources = new WeakHashMap<>();

    @Override
    public boolean matches(URI uri) {
        return uri.getSchemeSpecificPart().startsWith(COLOR_RESOURCE_SCHEMA);
    }

    @Override
    public Resource from(URI uri) {
        return new ColorResource(uri);
    }

    @Override
    public void add(URI uri, Resource resource) {
        resources.put(uri, new WeakReference<>(resource));
    }
}

class StringResource implements Resource {
    StringResource(URI uri) {
    }
}

class StringResources implements Resources {

    private final Map<URI, WeakReference<Resource>> resources = new WeakHashMap<>();
    @Override
    public boolean matches(URI uri) {
        return uri.getPath().endsWith("\"")
            && uri.getPath().lastIndexOf('"', uri.getPath().length() - 1) >= 0;
    }

    @Override
    public Resource from(URI uri) {
        return new StringResource(uri);
    }

    @Override
    public void add(URI uri, Resource resource) {
        resources.put(uri, new WeakReference<>(resource));
    }
}

As you can see Resources interfaces is able to tell if it should create resource from given uri (matches), create it (from) and store internally (add). It has those WeakHashMap inside. Last thing is changing your main method:

class Stack464 {

    private static Logger logger = Logger.getLogger(Stack464.class.toString());

    private static final Resources colorResources = new ColorResources();
    private static final Resources stringResources = new StringResources();

    public static void createResource(URI uri, ResourceSink output) {
        try {
            Stream.of(colorResources,
                      stringResources)
                .filter(resources -> resources.matches(uri))
                .findFirst()
                .ifPresent(resources -> {
                    Resource resource = resources.from(uri);
                    output.add(resource);
                    resources.add(uri, resource);
                });
        } catch(Exception e){
            logger.log(Level.WARNING, "Failed to create resource with URI: " + uri, e);
        }
    }
}

As you can see I replaced every map field with field of type Resources. Then in the main method I create a stream from the fields. Then I'm trying to find first match among them - I'm looking for resources class able to create a resource from given uri. Then I take first of them, creating a resource, adding it to Sink and to internal resources map.

Now to add new resource type you need to implement two interfaces, add new field and modify fields inside Stream#of.

There are two things I need to to mention:

  1. in your code you could use the resources stored in map and they had proper value type like ColorResources etc. In My solutions internal maps must have WeakReference value because they produce object of class Resource. So to actually use it you would have to cast it (but you can safely do it inside concrete Resources implementation).
  2. The order of fields in Stream#of matters. In your code you create VideResource when the suffix of uri is ".video.ogg" and AudioResource if it is ".ogg". If you would put AudioResources above VideoResources in Stream#of then even if it would be a video resource the code would create audio resource as suffix ".ogg" occured. Placing VideoResources above AudioResources solves the problem. So you must order fields in Stream#from from more concrete to more general matching patterns.

The first problem could be avoided changing interface Resources to

interface Resources<T extends Resource> {
    boolean matches(URI uri);
    T from(URI uri);
    void add(URI uri, T resource);
}

Then class ColorResources implements Resources<ColorResource> could have inner WeakHashMap with references to ColorResource. It would result in calling Resources#add on raw Resources type as in ifPresent method we don't know which implementation handles it.

Edit:

To use overloaded method add of ResourceSink there is only one solution I have on my mind. This is method used in visitor pattern. First of all modify your Resource interface:

interface Resource {
    void putIn(ResourceSink resourceSink);
}

And now example implementation looks like:

class ColorResource implements Resource {
    ColorResource(URI uri) {
    }

    @Override
    public void putIn(ResourceSink resourceSink) {
        resourceSink.add(this);
    }
}

Inside the ColorResource this has ColorResource type so expected ResourceSink#add method is invoked. Then u change main method to utilize this new function.

Resource resource = resources.from(uri);
resource.putIn(output);
resources.add(uri, resource);

Now about this unsafe add call on Resources class. If you really need References to concrete classes in hash map then I don't know better approach. If u don't then not using generics solves the problem.

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