Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enforcing Multiple Generic Bounds in Java Return Type

Tags:

java

generics

This isn't a JavaFX question, but I'm trying to write an interface in JavaFX that declares a class to be Viewable. Viewable classes are meant to have a view() method that returns a Node object representing that Viewable. Simple so far, but here's where it gets complicated. The returned Node should be guaranteed to have a getViewable() method that returns the Viewable object it represents. How do I accomplish this? My first instinct was to try something like this:

interface Viewable<V extends Viewable<V>>{
    <N extends Node&View<V>>N view();
}
interface View<V extends Viewable<V>>{
    V getViewable();
}

Which at first appears sound, and allows classes like the following:

class ViewableObject implements Viewable<ViewableObject>{
    @Override public ObjectView view(){
        return new ObjectView();
    }
    class ObjectView extends Pane implements View<ViewableObject>{
        @Override public ViewableObject getViewable(){
            return ViewableObject.this;
        }
    }
}

However, for some reason, this class also compiles:

class ViewableObject implements Viewable<ViewableObject>{
    @Override public Pane view(){
        return new Pane();
    }
}

Pane is a Node, but it does not implement View, so why does this class compile? I would think this is violating the contract of the view() method. Even stranger, the same class fails to compile when Pane is replaced with Object:

class ViewableObject implements Viewable<ViewableObject>{
    @Override public Object view(){//complains this is not an @Override
        return new Object();
    }
}

What's going on here? Is there a flaw in my understanding of generics? How can I get this to work as intended?

like image 980
Ryan Hilbert Avatar asked Oct 31 '22 20:10

Ryan Hilbert


1 Answers

You don't want to use a generic method in this case, since your goal is to fix the type of view()'s return value. A generic method lets the caller determine the concrete types. So really you're doing the exact opposite of enforcing.

I think you'd want to build the type parameter for the Node's type into the interface definitions, which will enforce that view() returns the correct type. Maybe something like this:

interface Viewable<V extends Viewable<V, N>, N extends Node & View<V, N>> {
    N view();
}

interface View<V extends Viewable<V, TNode>, TNode extends Node & View<V, TNode>> {
    V getViewable();
}

class ViewableObject implements Viewable<ViewableObject, ViewableObject.ObjectView> {
    @Override
    public ObjectView view() {
        return new ObjectView();
    }

    class ObjectView extends Pane implements View<ViewableObject, ObjectView> {
        @Override
        public ViewableObject getViewable() {
            return ViewableObject.this;
        }
    }
}

If you take a look at the byte code for the declaration of Viewable.view(), you'll see that the compiler selects the first type bound to specify as the actual return type for the method. Here are the relevant lines of the output from the IntelliJ byte code viewer:

// declaration: N view<N extends org.cumberlw.viewtest.Node, org.cumberlw.viewtest.View<V>>()
public abstract view()Lorg/cumberlw/viewtest/Node;

So when overriding you can specify any type that is covariant with the first type only and the compiler will accept it. If you switch the order of the type bounds, this is what you'll see in the byte code viewer:

// declaration: N view<N extends org.cumberlw.viewtest.View<V>, org.cumberlw.viewtest.Node>()
public abstract view()Lorg/cumberlw/viewtest/View;

Notice that the byte code says the return value is View now. So now your second example won't compile because Pane is not a subclass of View. Neither order of the parameters will let the third example compile because Object is not a subclass of Node or View.


Overriding a method with a generic return type with multiple bounds can easily produce runtime errors too. The compiler only enforces that return types be covariant with the first type bound, so you can return a type that does not conform to the second type bound. For example this compiles fine, but crashes at runtime:

interface DogLike {
    void bark();
}

interface CatLike {
    void meow();
}

class Dog implements DogLike {
    @Override
    public void bark() {
        System.out.println("Woof");
    }
}

interface MoreauMachine {
    <H extends DogLike & CatLike > H createHybrid();
}

class MalfunctioningDogCatFactory implements MoreauMachine {

    @Override
    public DogLike createHybrid() {
        //Compile with -Xlint:unchecked to see a warning here:
        //Warning:(84, 20) java: createHybrid() in org.cumberlw.viewtest.MalfunctioningDogCatFactory implements <H>createHybrid() in org.cumberlw.viewtest.MoreauMachine
        //return type requires unchecked conversion from org.cumberlw.viewtest.DogLike to H
        return new Dog();
    }

    public static void main(String[] args) {
        MoreauMachine factory = new MalfunctioningDogCatFactory();

        //crashes!
        //Exception in thread "main" java.lang.ClassCastException: org.cumberlw.viewtest.Dog cannot be cast to org.cumberlw.viewtest.CatLike
        factory.createHybrid().meow();
    }
}
like image 123
Wes Cumberland Avatar answered Nov 02 '22 09:11

Wes Cumberland