Evan Belcher
Evan Belcher

Reputation: 43

Incompatible type with inheriting class

I'm running into the following issue which feels like an inconsistency in the way inheritance is handled:

import java.util.*;

interface Greeting {
    
}

class Hello implements Greeting {
    public Hello(){}
}

public class HelloWorld{
    
    // Compiles fine
    public static Greeting getGreeting() {
        return new Hello();
    }
    
    // incompatible types: ArrayList<Hello> cannot be converted to ArrayList<Greeting>
    public static ArrayList<Greeting> getGreetings() {
        return new ArrayList<Hello>();
    }
    
    // Hello cannot be converted to O
    // where O is a type-variable:
    // O extends Greeting declared in method <O>getGenericGreeting()
    public static <O extends Greeting> O getGenericGreeting() {
        return new Hello();
    }

     public static void main(String []args){}
}

Can anyone explain why this works this way, and if there are alternatives to the latter two methods which don't compile?

Upvotes: 0

Views: 53

Answers (1)

rzwitserloot
rzwitserloot

Reputation: 102872

Reality, basically. We start with an axiom: The "point" of saying Hello implements Greeting is that Hello is a kind of greeting. If a greeting is required, then an instance of Hello fits the bill, so to speak.

Thus, Greeting g = new Hello(); compiles and works fine.

You'd think: Great, so, List<Greeting> g = new ArrayList<Hello>(); is fine as well, right?

No. Not how it works. Not 'that is not how java works', no, 'that is not how that model works - that is not how reality works'. It's inherent in what lists are.

Imagine it DID work. Then you can do this:

interface Greeting {}
class Hello implements Greeting {}
class Goodbye implements Greeting {}

List<Hello> hellos = new ArrayList<Hello>();
List<Greeting> greets = hellos;
greets.add(new Goodbye());
Hello hello = hellos.get(0);

Consider the entirety of that code: Hopefully you realize that it is broken.

This is why java works the way it does: To avoid this scenario. If you try to compile the above code, the List<Greeting> greets = hellos; won't compile.

This is called variance.

A covariant type system lets a subtype 'fit' for any of its supertypes. Basic java is covariant. This is a covariant assign: Greeting g = new Hello().

There are other variances: invariant means only the type itself is allowed, not any subtype and not any supertype. Basic generics is invariant. It is invariant because that's how reality works out with this stuff - see the snippet above.

There's also contravariant. This would be a contravariant assign: Hello h = new Greeting(); - a supertype can stand in for a subtype. Not how basic java works, and seems ridiculous, but just wait.

Finally there's 'legacy / raw' variance - anything goes, do whatever you want, it's all fine, let's pray runtime exceptions happen if we make mistakes.

Generics supports all 4 of these:

List<? extends Greeting> g; // This is a covariant list
List<? super Hello> g; // This is a contravariant list
List<Greeting> g; // This is an invariant list
List g; // This is a raw/legacy variance list.

Let's try the exact same snippet again, this time using covariance:

List<Hello> hellos = new ArrayList<Hello>();
List<? extends Greeting> g = hellos;
g.add(new Goodbye());
Hello h = hellos.get(0);

This time, line 3 fails to compile - you can't add anything to a covariant list. And that's because that's how it's supposed to work!

This is good news though! The code as written makes no sense at all, so it's great that it doesn't compile. But if you just want it to compile (knowing that at runtime something has to budge), legacy will do it:

List<Hello> hellos = new ArrayList<Hello>();
List raw = hellos;
raw.add(new Goodbye());
Hello h = hellos.get(0);

The above compiles, but it will emit a warning. If you run it, the last line will cause a ClassCastException to be thrown, even though line 4 contains no cast. That's what the warning is effectively telling you (oookay, but, if your assertions are wrong here, you get ClassCastExceptions in weird places. Your funeral if you continue).

List<Greeting> is a list of greeting objects, and as basic java is covariant, it could be a Hello or a Goodbye instance (it could never be an instance of just Greeting itself, as that is an interface!)

List<? extends Greeting> is a list of some unknown type, but I can guarantee you that it is at least greeting. Not quite the same. Because we don't know what it actually, is you can't add, addAll, etc. (Well, except the ridiculous academic case of list.add(null) which compiles and runs fine, but only because null is a valid value for absolutely any reference type.

And now let's get back to contravariance: It IS useful! With contravariance, you can't call get() at all (well, you can, and you get Object back, only because all ref types are neccessarily Objects, no matter what happens), but you CAN call add! List<? super Hello> is List<Hello> or List<Greeting> or List<Object>, and invoking .add(new Hello()) on any of those is just fine.

Here's a handy table:

code type name compatible types get? add? safe?
List invariant only Greeting Yes Yes Yes
List<? extends Greeting> covariant Greeting or any subtype Yes No! Yes
List<? super Greeting> contravariant Greeting or Object No! Yes Yes
List raw / legacy anything you want Yes Yes No!

Your snippets, but fixed.

public static ArrayList<Greeting> getGreetings() {
  return new ArrayList<Hello>();
}

This does not work, as you can add a Goodbye to a List<Greeting>, but not to a List<Hello>. To fix:

public static ArrayList<? extends Greeting> getGreetings() {
  return new ArrayList<Hello>();
}

public static <O extends Greeting> O getGenericGreeting() {
  return new Hello();
}

This code says: There is some unknown type. All we know about it, is that it extends Greeting. This method is supposed to return an expression of that type.

Clearly, impossible, unless you either [A] never return (just throws or endless loop inside), or [B] you get something handed to you of the right type, e.g. you have a param O thingie or List<O> thingie, or [C] you return null or [D] you add some ugly, compiler-warning-generating assertions.

More generally generics are figments of javac's imagination, so they 'link' things: If you declare a type var and use it zero or one times, it's either useless or you're doing some language hacking. In other words, as a general rule, if you use a type var once or zero times, you messed up.

At any rate, the compiler has no idea what O might be. It could be Goodbye so new Hello() is not a valid return here.

To fix it:

public static Hello getGenericGreeting() {
  return new Hello();
}

or perhaps:

public static <O extends Greeting> O getGenericGreeting(Class<O> type) {
  return type.getConstructor().newInstance();
}

Upvotes: 2

Related Questions