2

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?

Axel
  • 13,939
  • 5
  • 50
  • 79
  • You are passing `getMeta().getOutputter().output(this)` A type to outputter but, outputter takes `CLS`, why do not you pass `CLS` object? – Elbek Jul 25 '13 at 08:17
  • @elbek, `CLS` is not a class or object, it is a type. Read about generics: http://en.wikipedia.org/wiki/Generics_in_Java – Tim Bender Jul 25 '13 at 08:20
  • sorry, got it, my bad – Elbek Jul 25 '13 at 08:21
  • First problem: A call to getMeta().getOutputter returns an Outputter extends A> (when being called in class A). The actual type of this outputter could be (!) any subtype of A, for example a B, so that it is in fact an Outputter. Calling output(this) - where this is an A - on an Outputter is not allowed. Second problem: I do not know a solution, yet. – Seelenvirtuose Jul 25 '13 at 08:30
  • 1
    Related: [Is there a way to refer to the current type with a type variable?](http://stackoverflow.com/questions/7354740/is-there-a-way-to-refer-to-the-current-type-with-a-type-variable) – Paul Bellora Jul 25 '13 at 14:46
  • @PaulBellora interesting link. But the solution you gave there suffers the same problem that the answer I gave myself here: It's always possible a derived class doesn't fulfill the contract. – Axel Jul 25 '13 at 15:33
  • Outputter is a consumer of A. The only wildcard generalization of Outputter (and by extension, Meta ) that will work is ? super A. – Judge Mental Jul 25 '13 at 16:25

3 Answers3

1

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
    }
Elbek
  • 3,434
  • 6
  • 37
  • 49
1

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.

Chris Bode
  • 1,265
  • 7
  • 16
  • Yes, I understand the problem. But I still don't have a solution. (+1 for the link). – Axel Jul 25 '13 at 11:25
  • The solution is to write it in a different way. If thats the kind of help you want, you ought to give us the complete problem instead of the minimal skeleton of what you think the solution is. – Chris Bode Jul 25 '13 at 21:02
  • It's the minimal skeleton extracted from the current codebase. It's not something I'm starting to implement now. The problem is applying our working code in circumstances where it has to handle inheritance. If it works with the skeleton, I can apply it to our code. Your (and others answers and comments) already helped me a lot, and I think the question is more valueable for others that may have a similar problem later when the code is reduced as much as possible. Even if it turns out it would have been better to have it organized differently, that information will be helpfull to others. – Axel Jul 26 '13 at 06:12
0

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) {
  }
}
Paul Bellora
  • 54,340
  • 18
  • 130
  • 181
Axel
  • 13,939
  • 5
  • 50
  • 79
  • This solution isn't safe. Class `C` could extend `A` and override `getMeta` to return a `Meta`. Make sure to understand warnings before suppressing them. – Paul Bellora Jul 25 '13 at 14:50
  • I do understand the warnings, and I know this *will* go wrong in the case you mentioned. Nonetheless it works if you make sure that every class `X` really returns a `Meta`. But AFAIK there's no way to tell the compiler to enforce this rule (you can just add a Javadoc). But these warnings are not of much use once you decide that you really *want* it like this - so I add a comment and suppress them. That's also why I said I'm still looking for a better solution - just so far there is none. – Axel Jul 25 '13 at 15:25
  • @PaulBellora thanks for the edit. I think that makes it clear. – Axel Jul 25 '13 at 15:57