Generic method return type - compilation error [duplicate]

蹲街弑〆低调 提交于 2021-02-10 16:00:27

问题


Given this code sample:

class A {

}

public class TestGenerics {

    private static <T extends A> T failsToCompile() {
        return new A();
    }

    private static <T extends A> T compiles(T param) {
        return param;
    }

}

how come the first method doesn't compile, but the second one does?

The error is:

Incompatible types. Required: T. Found: A

Essentially what I'm trying to achieve is to return a concrete instance of a sub-type (for example of class B, that extends A). Sort of a factory method.

Edit: OK, for those who downvoted, I decided to elaborate a bit further. See the updated example, which no longer uses String (yes I'm aware that String is final, no need to state the obvious)

Edit 2: alternative version of the question:

How would you implement this method so that it compiles (without unchecked casts and warnings)?

abstract <T extends A> T failsToCompile();

Edit 3: Here's a code sample closer to the problem I'm trying to solve:

class Foo { }
class Bar extends Foo { }
class A<T extends Foo> { }
class B extends A<Bar> { }
public class TestGenerics {

    private static <T extends Foo> A<T> failsToCompile() {
        return new B();
    }

}

Here, the method return type is A<T>. Considering that T is defined as a Foo or its subclass, and the definition of B is as follows: B extends A<Bar>, why can't I just return new B()? I guess the problem comes down to the fact that you can't assign List<Animal> dList = new ArrayList<Dog>(); and I understand why. But is there an elegant solution to this?


回答1:


Ignoring the fact that String is final (so you can't have a sub-class of String), you can pass the generic class Class<T> into the method for the generic case. Like,

private static <T extends String> T get(Class<T> cls) 
            throws InstantiationException, IllegalAccessException {
    return cls.newInstance();
}



回答2:


I'm trying to elaborate my comment a bit more into an answer to the "why".

What you're showing in your question are generic methods (and not generic types), see e.g. Oracles "The Java™ Tutorials", Generic Methods.

Reasoning

Let's consider the explicit call for the working method: TestGenerics.<A>compiles(new A());

This instantiates the generic type T of your method call to class A. Remember that type erasure removes all these funky generic types when compiling, that the JVM doesn't see generic types and that the compiler handles generics. The compiler knows now, that you want to call the method with the generic type T "set" to class A. From that, it can deduct that the returned type is also of class A (as T is returned according to the methods code and as T is instantiated to A). That's all you gain here from using generics.

You can remove the <A> from your method call and make it look like an "ordinary" method call, as the compiler can deduct that T has to be A: TestGenerics.compiles(new A());

Let's look at TestGenerics.<B>compiles(new B()); with class B extends A {}. As above, the compiler knows that this method call will return an object of class B.

Now imagine the (not compiling) code TestGenerics.<B>compiles(new A());. The compiler will throw an error as the object passed to the method is not of type B. Otherwise the method would return an object of class A - but the generic type asserts that the method returns here an object of typeB. That's actually equivalent to the example (B b = failsToCompile()) Andreas gave in his comment.

Until you instantiate a generic type - even if you set bounds (like T extends A) - it doesn't have a class type. You therefore can't return a concrete class, as this would not satisfy the generic type parameter.

Pizza Example

Just for fun, let's try to make a real world example for above reasoning: Pizza. For the sake of the example, let's assume that every Pizza is a subtype of the Margherita, i.e. you add ingredients to a Margherita to get your favorite other pizza.

class PizzaMargherita {};

class PizzaProsciutto {
  PizzaProsciutto() {
    super(); 
    addIngredient(new Prosciutto());
  }
}

Your compiles() method now bakes the pizza:

public static <P extends Margherita> P bake(P pizza) {
  heat(pizza); 
  return pizza;
}

Bake a Margherita and get a (baked) Margherita out of your oven, bake a Prosciutto and get a Prosciutto.

Now think of this:

public static <P extends Margherita> P doesntBake(P pizza) {
  return new PizzaMargherita();
}

An oven always returning a Margherita, independent of what you put into it ?!? The compiler doesn't approve that.

-> You need the concrete pizza to bake it or you need a workaround, like the type token:

public static <P extends Margherita> P bakePizza(Ingredients<P> ingredient) {
  P pizza = buildPizza(ingredients);
  return bake(pizza);
}

Workaround type token

You have to use a runtime-type token as @Elliott Frisch shows in his answer:

private static <T extends A> T failedToCompile(Class<T> c)
        throws InstantiationException, IllegalAccessException {
    return c.newInstance();
}

You can't instantiate a generic type - new T() doesn't work, as the JVM doesn't know anything about the generic type T. What you're looking for in edit 2 therefore doesn't work. But TestGenerics.<A>failedToCompile(A.class); works, as A.class is part of the java byte code.

Workaround type token & generic class

Depending on your specific requirements, a generic factory class might help you:

class Factory<T extends A> {
  private final Class<T> typeToken;
  public Factory(Class<T> typeToken) {
    this.typeToken = typeToken;
  }

  public T build()
        throws InstantiationException, IllegalAccessException {
    return this.typeToken.newInstance();
  }
}

You will still need some form of Map to get the correct factory to build the class you need, but you can now use whatever is available at the point where you need to create the Object of type T.

Map<String, Factory<?>> factories = new HashMap<>();
Map<DAO, Factory<?>> factories = new HashMap<>();
Map<ValueObject, Factory<?>> factories = new HashMap<>();
factories.get(valueObjectTypeA);



回答3:


Others have clearly explained why the failsToCompile method doesn't compile. Andreas has even shown an example...

As to how to circumvect this, I think you should consider using a Supplier, which is just the factory you need:

private static <T extends A> T nowItCompilesFine(Supplier<? extends T> factory) {
    return factory.get();
}

And you call it:

A a = nowItCompilesFine(() -> new A());

Or using a reference to a constructor:

A a = nowItCompilesFine(A::new);

Same with any descendant of A:

class Child extends A { }

List<Supplier<? extends A>> factories = Arrays.asList(A::new, Child::new);

Supplier<? extends A> factoryA = factories.get(0);

A a1 = nowItCompilesFine(factoryA);

Supplier<? extends A> factoryChild = factories.get(1);

A a2 = nowItCompilesFine(factoryChild);

System.out.println(a2.getClass().getSimpleName()); // Child


来源:https://stackoverflow.com/questions/44792032/generic-method-return-type-compilation-error

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!