3

I want to be able to create instances of classes, based on HashMap entries.

E.g. this is what I'd try writing off the top of my head:

public class One implements Interface {
  public void sayName() {
    System.out.println("One");
  }
}

public class Two implements Interface {
  public void sayName() {
    System.out.println("Two");
  }
}

Map<String, Interface> associations = new HashMap<String, Interface>();

associations.put("first", One);
associations.put("second", Two);

Interface instance = new associations.get("first")();

instance.sayName(); // outputs "One"

But I strongly suspect this will not work in Java.


My situation: I want to create a way to associate String names with classes.

A user can create instances of a class by using its "name".

I feel like trying: making a map of names to classes (I don't know how to store classes in a map), and getting the item from the map that matches "name" and then instantiating it.

That won't work.


How can I associate classes with String names and instantiate those classes using the 'name' I've given it?

theonlygusti
  • 11,032
  • 11
  • 64
  • 119
  • You will want to look into the Java reflection API. There's a class.forName method in particular – Kon Apr 06 '18 at 16:25
  • 2
    Make your map `Map` and put `One::new` and `Two::new` instead of `One` or `Two`. Then you can do `associations.get("first").get()`. – lexicore Apr 06 '18 at 16:29
  • 1
    Furthermore, you actually do not want to store classes, but constructors. From a certain point of view, a constructor is just a [Provider](https://docs.oracle.com/javase/9/docs/api/java/util/function/Supplier.html), i.e. a function that generates something when called. As @Kon said, this most certainly will involve some reflection. You may also want to look at the [Factory method-](https://en.wikipedia.org/wiki/Factory_method_pattern) and/or [Builder-Pattern](https://en.wikipedia.org/wiki/Builder_pattern). – Turing85 Apr 06 '18 at 16:29
  • Your map typing is wrong. it should be Map for the Class.forName approach. Or else Map> (and @lexicore has even better suggestion) – Kevin Welker Apr 06 '18 at 16:30

3 Answers3

7

You can use the Supplier functional interface and a method reference to the default constructor:

Map<String, Supplier<Interface>> associations = new HashMap<>();

associations.put("first", One::new);
associations.put("second", Two::new);

To instantiate a new object, call Supplier.get:

Interface foo = associations.get("first").get();

If your constructors require arguments, you'll need to use another functional interface. For one- and two-argument constructors, you can use Function and BiFunction respectively. Any more and you'll need to define your own functional interface. Supposing the constructors both take a string, you could do this:

class One implements Interface
{
    One(String foo){ }

    public void sayName() {
        System.out.println("One");
    }
}

Map<String, Function<String, Interface>> associations = new HashMap<>();
associations.put("first", One::new);

and then use Function.apply to get the instance:

Interface a = associations.get("first").apply("some string");

If your constructors take different number of arguments then you're out of luck.

Michael
  • 41,989
  • 11
  • 82
  • 128
  • I need to make a function that accepts the "constructors" (`One::new` and `Two::new`), what's the type signature of `Class::new`? – theonlygusti Apr 06 '18 at 16:42
  • It doesn't have a type. It will match any functional interface which takes the same number of arguments and returns an `Interface`. – Michael Apr 06 '18 at 16:43
  • I need to make a function that wraps the `associations.put`, this function takes the `String` key and the `Class::new` value. The function will look like `void addAssoc(String name, XXX constructor)`. Is `XXX` going to be `Function` ? – theonlygusti Apr 06 '18 at 16:45
  • If the constructors both take 1 string then yes. – Michael Apr 06 '18 at 16:46
3

I'd highly recommend the use of Supplier in Michael's answer. Alternatively, you should be able to use the following:

var associations = Map.of("first", One.class, "second", Two.class);

var foo = associations.get("first").getConstructor().newInstance();

If your constructors require arguments, just pass the classes to getConstructor and the values to newInstance. For example, if Two takes an int in its constructor:

var foo = associations.get("two").getConstructor(int.class).newInstance(5);

Note: This uses Java 10.

Jacob G.
  • 28,856
  • 5
  • 62
  • 116
  • after getting from the map how do I pass an argument to the constructor? – theonlygusti Apr 06 '18 at 16:35
  • I did see, why are you using the `var` keyword? Is the last `foo` of type `Two`? – theonlygusti Apr 06 '18 at 16:37
  • I've not seen this `Map.of` notation before, how do I add single elements to it at a time? (When it's created I don't know how many elements will be in it) – theonlygusti Apr 06 '18 at 16:38
  • Yes, I used `var` just to shorten the code (as it's a new feature in Java 10). I'm not a fan of having to type `Map>`. `Map#of` was introduced in Java 9. To add elements one-by-one, simply initialize a `HashMap` and `put` entries in. – Jacob G. Apr 06 '18 at 16:39
0

Did you want to store classes and create new instances on demand, or store instances and just use them?

public class ClassInHashMap {
    public static void main(String[] args) {
    Map<String,Class<? extends SomeInterface>> associations = new HashMap<String,Class<? extends SomeInterface>>();
        associations.put("first", One.class);
        associations.put("second", Two.class);

        try {
            Class<? extends SomeInterface> someCls = associations.get("first");
            SomeInterface instance = someCls.getDeclaredConstructor().newInstance();
            instance.sayName(); // outputs "One"
        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
        }
        catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        catch (SecurityException e) {
            e.printStackTrace();
        }
        catch (InstantiationException e) {
            e.printStackTrace();
        }
        catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

interface SomeInterface {
    public void sayName();
}

class One implements SomeInterface {
    public void sayName() {
        System.out.println("One");
    }
}

class Two implements SomeInterface {
    public void sayName() {
        System.out.println("Two");
    }
}
pamcevoy
  • 1,136
  • 11
  • 15