Reputation: 14159
I have a (generic) class that holds meta data for other classes. The meta data is used in several ways (writing and reading XML data, database, output as text, etc). So far this works. But I have come across a problem when using all of this for classes that inherited from other classes.
Please have a look at the following code (I have tried to produce a minimal example that is compilabe except the line marked below):
class A {
public Meta<? extends A> getMeta() {
return new Meta<A>();
}
public void output() {
/*
* Error shown in eclipse for the next line:
* The method output(capture#1-of ? extends A) in the type
* Outputter<capture#1-of ? extends A> is not applicable for the arguments
* (A)
*/
getMeta().getOutputter().output(this);
}
}
class B extends A {
@Override
public Meta<? extends B> getMeta() {
return new Meta<B>();
}
}
class Meta<CLS> {
public Outputter<CLS> getOutputter() {
return null;
}
}
class Outputter<CLS> {
public void output(CLS obj) {
}
}
I can change A.getMeta()
to return Meta<A>
to make above line compilabe, but then I cannot override it as Meta<B> getMeta()
in class B.
Any ideas on how to solve this?
Upvotes: 2
Views: 240
Reputation: 14159
I will answer this myself since I found a working solution.
Although this solution is not type-safe, it works and requires the least changes to my existing codebase. If anyone comes up with something that works and doesn't require the @SuppressWarnings
, I will accept that answer.
class A {
Meta<?> getMeta() {
return new Meta<A>();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public void output() {
Outputter out = getMeta().getOutputter();
out.output(this);
}
}
class B extends A {
@Override
public Meta<?> getMeta() {
return new Meta<B>();
}
}
class Meta<CLS> {
public Outputter<CLS> getOutputter() {
return null;
}
}
class Outputter<CLS> {
public void output(CLS obj) {
}
}
Upvotes: 0
Reputation: 1265
The reason you cannot override public Meta<A> getMeta()
with public Meta<B> getMeta()
is that instances of B will be castable to A, and such a casted instance would need to return a Meta<A>
. While it may be that a Meta<B>
can serve as a Meta<A>
, the compiler doesn't know that.
Imagine instead that you are returning List<A>
and a List<B>
. It is allowable to put instances of A and B into a List<B>
, but it is not allowable to put instances of B into a List<A>
, so the List<B>
that is actually being returned by B can not serve as a List<A>
.
Changing List<A>
to List<? extends A>
allows the code to compile, because List<B>
is technically a subclass of List<? extends A>
, but it will not allow you to do everything you may expect.
B b = new B();
A casted = (A)b;
casted.getList().add(new A());
The compiler will accept the first and second line without issue, but it will take issue with the third:
The method add(capture#1-of ? extends A) in the type List<capture#1-of ? extends A> is not applicable for the arguments (A)
If you investigate a bit, you'll find that this casted variable will accept neither elements of A nor B. The compiler has remembered that the object was casted and may not actually be able to accept anything that extends A.
I'm trying to hunt down documentation for this behavior, but I'm failing. Eclipse tooltips are suggesting that I should give it an element of type null
, which is obviously nonsense. I'll update if I find anything on it.
EDIT: The behavior described is a product of "Capture Conversion" as described here. Capture Conversion allows wildcards to be more useful by changing the bounds of type arguments over the course of assignments and casts. What happens in our code is simply that the bounds are constricted to the null
type.
Upvotes: 1
Reputation: 3484
What if you do this? It requires one more class, but it seems it is going to work:
class T{
//put common methods here, generic methods are not common, so they will not be here
}
class A extends T{
public Meta<A> getMeta() {
return new Meta<A>();
}
public void output() {
/*
* Error shown in eclipse for the next line:
* The method output(capture#1-of ? extends A) in the type
* Outputter<capture#1-of ? extends A> is not applicable for the arguments
* (A)
*/
getMeta().getOutputter().output(this);
}
}
class B extends T {
public Meta<B> getMeta() {
return new Meta<B>();
}
}
class Meta<CLS> {
public Outputter<CLS> getOutputter() {
return null;
}
}
class Outputter<CLS> {
public void output(CLS obj) {
}
}
if you do not want to create another method you can use composite. There are many good discussions about compositions over inheritance.
Everything will be the same except A
and B
classes:
class A{
public Meta<A> getMeta() {
return new Meta<A>();
}
...
}
class B {
private class A a;
public Meta<B> getMeta() {
return new Meta<B>();
}
//use a here if you need, a is composed into B
}
Upvotes: 1