Reputation: 47
I have a problem with generics and inheritance in java.
I would like to instantiate an 'ObjectManager' class from the 'createManager' method that can create an instance from a given type. This 'ObjectManager' have some utility methods (ex: 'getDescription').
This method allows me to instantiate an 'ObjectManager' from a type and then get, for example, its description (the hash here).
I would also like to specialize 'ObjectManager' for 'FooManager' if the input type is assignable to type 'Foo', so that I can override the 'getDescription' method. I therefore test the type of the input in the 'createManager' method to know if I create an 'ObjectManager' or a 'FooManager'.
The code works, but I don't know the generic type for the output corresponding to 'new FooManager(type)'. In my opinion, and for Eclipse Quick fix, the type should be <T> but this leads to an error:
Bound mismatch: The type T is not a valid substitute for the bounded parameter <T extends Foo> of the type FooManager<T>
My code:
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
public class GenericBug {
private GenericBug() {}
public static void main(String[] args) {
ObjectManager<Foo> objectManager1 = createManager(Foo.class);
System.out.println(objectManager1.getDescription());
ObjectManager<Object> objectManager2 = createManager(Object.class);
System.out.println(objectManager2.getDescription());
}
private static <T> ObjectManager<T> createManager(Class<T> type) {
if(Foo.class.isAssignableFrom(type))
return new FooManager<T>(type); //Error: Bound mismatch
return new ObjectManager<T>(type);
}
}
class ObjectManager<T>{
public T val;
public ObjectManager(Class<T> type) {
try {
val = type.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
}
public String getDescription() {
return "Hash: " + Objects.hash(val);
}
}
class FooManager<T extends Foo> extends ObjectManager<T>{
public FooManager(Class<T> type) {
super(type);
}
@Override
public String getDescription() {
return super.getDescription() + " value: " + val.getVal();
}
}
class Foo{
private double val = Math.random();
public Foo() {}
public double getVal() {
return val;
}
}
So my question is: why this error? how to fix it without having a warning? and, as a bonus, why Eclipse provides a solution that doesn't compile? is this an Eclipse error?
Thank you in advance for your answer.
Upvotes: 2
Views: 542
Reputation: 1964
„…why this error?…“
The error that javac
reports on your original code gives you this reason:
...GenericBug.java:14: error: type argument T#1 is not within bounds of type-variable T#2
return new FooManager<T>(type); //Error: Bound mismatch
^
where T#1,T#2 are type-variables:
T#1 extends Object declared in method <T#1>createFooManager(Class<T#1>)
T#2 extends Foo declared in class FooManager
1 error
T#1 is not within bounds of type-variable T#2
because:
<T> ObjectManager<T> createFooManager(...)
means you defined T
(aka T#1
) as having no bounds.T
(akaT#2
) of FooManager<T extends Foo>
is defined with the bounds extends Foo
. And the generic method's T
does not extend Foo
as the generic class requires. So it's out of bounds.„…how to fix it…“
Your original Foo.class.isAssignableFrom(type)
approach strikes me as not very object-oriented. So basically, I've fixed it this way.
...
private static <T> ObjectManager<T> createObjectManager(Class<T> type) {
return new ObjectManager<>(type);
}
private static <S extends Foo> ObjectManager<S> createFooManager(Class<S> type) {
return new FooManager<>(type);
}
...
It isn't obvious what your IRL use case is. So I had to make some assumptions. Like I assumed you would more than likely be using this with more specialized implementations of Foo
. So I introduced a FooJr
to demonstrate how the solution would deal with that:
ObjectManager<? extends Foo> objectManager1 = createFooManager(Foo.class);
System.out.println(objectManager1.getDescription());
ObjectManager<?> objectManager2 = createObjectManager(Object.class);
System.out.println(objectManager2.getDescription());
objectManager1 = createFooManager(FooJr.class);
System.out.println(objectManager1.getDescription());
objectManager2 = createObjectManager(FooJr.class);
System.out.println(objectManager2.getDescription());
To say one solution is „better“ or „worse“ than another one is subjective; a matter of personal taste maybe. But it can be said with objectivity (pun intended) that one solution can be more object-oriented than another one. Some solutions are definitely more type safe than others.
„…without having a warning?…“
This solution is provably more type safe. Run it with -Xlint:unchecked
. The compiler does not report any unchecked warnings. And that is without the need for any @SuppressWarnings(...)
.
„…why Eclipse provides a solution that doesn't compile?…“
Eclipse was only presenting you with its best guess at what it thought the fix for the problem was. It's in the nature of IDEs to give some things a PASS that javac
or the JLS would absolutely not allow.
Upvotes: 1
Reputation: 456
That error occurs because you have no compile time guarantee that T is an instance of Foo. You have only the runtime guarantee that Foo is assignable from T, which the compiler does not care about. You can literally make this compile without warnings by suppressing them:
@SuppressWarnings({ "unchecked", "rawtypes" })
private static <T> ObjectManager<T> createFooManager(Class<T> type) {
if(Foo.class.isAssignableFrom(type))
return new FooManager(type); //Error: Bound mismatch
return new ObjectManager<T>(type);
}
That is probably not the answer you were looking for, but I don't think you can get rid of the warnings without suppressing them. What I would infer from the inevitability of those warnings is that the initial approach is incorrect. Why is it that you need to pass in a class as parameter? If the class parameter is known at compile time, you could simply call different methods for different classes. If the class is not known at compile time, you should never be in a position to benefit from that generic parameter at all.
Upvotes: 1
Reputation: 121088
The problem is that you are mixing compile time safety of generics and runtime checks via isAssignableFrom
.
I don't know of a better way to do it:
private static <T> ObjectManager<T> createFooManager(Class<T> type) {
if (Foo.class.isAssignableFrom(type)) {
return (ObjectManager<T>) getIt((Class<? extends Foo>) type);
}
return new ObjectManager<>(type);
}
private static <R extends Foo> ObjectManager<R> getIt(Class<R> cls) {
return new FooManager<>(cls);
}
This will raise an unchecked warning
, though.
Upvotes: 1