简体   繁体   中英

How to return generic interface reference as output of factory method

I have interface called InputCreator. This is a generic interface while its concrete implementation does know the related type.

interface InputCreator<T> { 
  public T createInput(String str) throws Exception; 
}

Its concrete implementation is XMLcreator.

class XMLCreator implements InputCreator<String> { 
  public String createInput(String str) throws Exception() {
     // some code here
     return xmlString;
  }
}

Since XMLCreator implements InputCreator with type parameter String the method signature gets checked at compile time.

Now there could be multiple concrete implementations, so I have created static factory method with signature:

public static <T> InputCreator<T> createInputInstance(Type type) {
    if (type == Type.XML)
         return new XMLCreator();
    else if (type == Type.SQL)
         return new SQLCreator(); }

From this method I am creating XMLCreator object based on type enum. But while returning this instance I am getting compiler error since it is not able to convert XMLCreator to InputCreator<T>

This is how I call my createInputInstance method. Parameter "inputType" is of type enum. It has possible values like XML, SQL and few other custom input types. This is not same as type parameter T, since for XML I use String class. So sending class<T> as parameter is not helping me out.

InputCreator<String> iCreator = CreatorFactory.createInputInstance(inputType);
String input = iCreator.createInput(inputString);

I understand compiler is not able to resolve value of T.
[Compiler message - TypeMismatch : Cannot convert from XMLCreator to InputCreator<T>]

How do I solve this ?

I hope that you do not have any special reason to use Type as an argument of your factory method. Use Class instead. The difference is that Class is parameterized iteself, so if you define your method as following:

public static <T> InputCreator<T> createInputInstance(Class<T> clazz)

you can then use it:

InputCreator<String> iCreator = CreatorFactory.createInputInstance(String.class);

without neither compilation error nor warning.

The problem is, think how the caller will assign the InputCreator you are returning from factory,

InputCreator<Integer> creator = CreatorFactory.createInstance("XML");

Now here the user expects to get a InputCreator<Integer> because of type inference, but you are returning XMLCreator which is InputCreator<String> , which is not what user thought would be.

So Java gives you error.

The types you are talking about XML, SQL are all runtime checks and compile time inference won't work like that.

I'm not sure there is a nice way to solve this problem. A "fix" is to add a cast (and a warning suppression) to the factory method:

public static <T> InputCreator<T> createInputInstance(Type t) {
  switch (t) {
  case XML:
    return (InputCreator<T>) new XMLCreator();
  // ...
  }    
}

This is the most obvious trap someone can fall into when dealing with Java Generics. Remember that java generics are implemented by type erasure . This means that all the information about generic parameters (<T>) are going to be lost at compile time. They are only a tool for compile time type safety. Imagine that your interface at run-time is transformed into the following and then you have a problem with the return type.

Compile time code:

interface InputCreator<T> { 
  public T createInput(String str) throws Exception; 
}

Converted to run time as:

interface InputCreator { 
  public ?? createInput(String str) throws Exception; 
}

Hence the problem.

A similar but more obvious problem is with wanting to instantiate an object of the parameterized type as shown in the code below at compile time:

class Foo<T> {
  String bar() {
    String s = (String) new T();
    return s;
  }
}

Converted to run time as:

class Foo {
  String bar() {
    String s = (String) new ??();
    return s;
  }
}

Bottom line:
Do not forget that in Java the Generics are implemented with type erasure (for backward compatibility) and hence this is the price that you have to pay. You can not use them for return type or for newing either.

In other languages (like C++) you do not pay this price.

A nice article about Java Generic Gotchas can be viewed at : http://www.ibm.com/developerworks/library/j-jtp01255/index.html

You simply can't mix enums with generics without losing type safety.

But if you're willing to drop your type safety requirements, the actual creator class could stay in your Type enum, you don't need a static factory method for that (but you're going to need an explicit cast, since you can't have generic enum instances):

enum Type {
    XML(XMLCreator.class),
    SQL(SQLCreator.class);

    private final Class<? extends InputCreator<?> creatorType; 

    private Type(Class<? extends InputCreator<?> creatorType) {
        this.creatorType = creatorType;
    }

    public <T> InputCreator<T> instantiateInputCreator() {
        return (InputCreator<T>) creatorType.newInstance();
    }
}

Then you can use it like this:

InputCreator<String> inputCreator = Type.XML.instantiateInputCreator();

However, you can also do:

InputCreator<Integer> inputCreator = Type.XML.instantiateInputCreator();

and you'll get a ClassCastException if you try to use that newly instantiated InputCreator.

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