Rafael Winterhalter
Rafael Winterhalter

Reputation: 44032

Self-type - requiring a type within a type bound

I am wondering if it is possible to restrain a method declared on an interface to require a type within a type bound. In effect, I'd like to offer a method for casting a type that is somewhat type-safe in the absense of being able to provide real type-safety.

As an example, consider a hierarchy with a base type Base which is inherited via an intermediate interface. Typically, one knows about a type being of interface FirstInterface but not what specific class is implementing it. I'd like a method that allows casting to either implementation of an interface without allowing to cast to other implementations of Base as demonstrated in the following example:

interface Base<TYPE extends Base<TYPE>> {
  default <CAST extends TYPE> CAST as(Class<? extends CAST> type) {
    if (type.isInstance(this)) {
      return type.cast(this);
    } else {
      throw new IllegalArgumentException();
    }
  }
}

interface FirstIface<TYPE extends FirstIface<TYPE>> extends Base<TYPE> { }
class FirstClassA implements FirstIface<FirstClassA> { }
class FirstClassB implements FirstIface<FirstClassB> { }

interface SecondIface<TYPE extends SecondIface<TYPE>> extends Base<TYPE> { }
class SecondClassA implements SecondIface<SecondClassA> { }
class SecondClassB implements SecondIface<SecondClassB> { }

interface ThirdIface<TYPE extends ThirdIface<TYPE>> extends FirstIface<TYPE>, SecondIface<TYPE> { }
class ThirdClassA implements ThirdIface<ThirdClassA> { }
class ThirdClassB implements ThirdIface<ThirdClassB> { }

I'd hope to being able to make the following code compile in Java:

FirstIface<?> i = new FirstClassA();
FirstClassA a = i.as(FirstClassA.class); // desired: compiles, now: compiler error
FirstClassB b = i.as(FirstClassB.class); // desired: runtime exception, now: compiler error

The same should work for the hierarchy of ThirdIFace, whereas the following code should render a compiler error:

SecondIface<?> i = new SecondClassA();
SecondClassA a = i.as(SecondClassA.class); // now and desired: compiler error
SecondClassB b = i.as(SecondClassB.class); // now and desired: compiler error

Is there any way to declare Base.as to withhold this requirement? The code is auto-generated, so it would also be possible to provide an override in the interfaces which are auto-generated (as are the classes). When overrides are used, a scenario of SecondIface extends FirstIface.

Upvotes: 15

Views: 615

Answers (3)

John Vasileff
John Vasileff

Reputation: 5478

If your goal is to only allow calls that are likely to succeed, with likely to succeed meaning that we know (statically) that SUB is a subtype of SUPER when attempting to cast an instance of type SUPER to type SUB, I'm not sure that is possible.

A problem, even after overcoming the issue of not having a type variable for, let's say, the intermediate self type (FirstIface<?> in your example), is that the compiler will infer SUPER==Object (or SUPER==Base<?>) if necessary to satisfy SUB extends SUPER.

The code is auto-generated, so it would also be possible to provide an override in the interfaces which are auto-generated

I don't think that would help. The goal would be for methods in sub-interfaces to have more restrictive parameter types than what is declared in the supertype, but parameter types are not covariant (covariant parameter types would violate the Liskov substitution principle)

But since the wildcard could be any subtype of FirstIface, it does not allow for any subtype of FirstIFace

Yeah. We only have 1) the self type variable TYPE which is for the final implementing type, and 2) a type for the Base interface. We don't have a way to write down the type for the intermediate type information that is known at the call site.

I suspect the closest approximation to your goal would be to:

  1. Use a static method for the cast, and
  2. Avoid use of type inference at the call site, since javac will happily infer Object for the supertype to make the code compile.

Of course, this isn't a good solution, since avoiding type inference is impractical. Here is a full example:

public class Hello {
    interface Base<TYPE extends Base<TYPE>> {}

    interface FirstIface<TYPE extends FirstIface<TYPE>> extends Base<TYPE> {}

    static final class FirstClassA implements FirstIface<FirstClassA> { }
    static final class FirstClassB implements FirstIface<FirstClassB> { }

    interface SecondIface<TYPE extends SecondIface<TYPE>> extends Base<TYPE> { }

    static final class SecondClassA implements SecondIface<SecondClassA> { }
    static final class SecondClassB implements SecondIface<SecondClassB> { }

    public static void main(String[] args) {
        FirstIface<?> i = new FirstClassA();

        FirstClassA a = Hello.<FirstIface<?>, FirstClassA>as(i, FirstClassA.class); // works
        FirstClassB b = Hello.<FirstIface<?>, FirstClassB>as(i, FirstClassB.class); // runtime error

        SecondClassA c = Hello.<FirstIface<?>, SecondClassA>as(i, SecondClassA.class); // compile error
        SecondClassB d = Hello.<FirstIface<?>, SecondClassB>as(i, SecondClassB.class); // compile error
    }

    static <SUPER, SUB extends SUPER> SUB as(SUPER obj, Class<? extends SUB> c) {
        return (SUB) obj;
    }
}

Upvotes: 1

František Hartman
František Hartman

Reputation: 15086

If you know the FirstIface when generating the Base interface without all the generics madness:

interface Base {
    default <CAST extends FirstIface> CAST as(Class<CAST> type) {
        if (type.isInstance(this)) {
            return type.cast(this);
        } else {
            throw new IllegalArgumentException();
        }
    }
}

interface FirstIface extends Base { }
class FirstClassA implements FirstIface { }
class FirstClassB implements FirstIface { }

interface SecondIface extends Base { }
class SecondClassA implements SecondIface { }
class SecondClassB implements SecondIface { }

public class Generics {

    public static void main(String[] args) {
        FirstIface i = new FirstClassA();
        FirstClassA a = i.as(FirstClassA.class);
        FirstClassB b = i.as(FirstClassB.class); // runtime exception

        SecondIface j = new SecondClassA();
        SecondClassA c = j.as(SecondClassA.class); // compiler error
        SecondClassB d = j.as(SecondClassB.class); // compiler error
    }
}

Upvotes: 0

Xavier Dury
Xavier Dury

Reputation: 1571

The best solution I could come up with is:

    interface Base<TYPE extends Base<TYPE, BASE>, BASE extends Base<?, ?>> {

        default <CAST extends BASE> CAST as(Class<CAST> type) {
            if (type.isInstance(this)) {
                return type.cast(this);
            } else {
                throw new IllegalArgumentException();
            }
        }
    }

    interface FirstIface<TYPE extends FirstIface<TYPE>> extends Base<TYPE, FirstIface<?>> {}
    static class FirstClassA implements FirstIface<FirstClassA> {}
    static class FirstClassB implements FirstIface<FirstClassB> {}

    interface SecondIface<TYPE extends SecondIface<TYPE>> extends Base<TYPE, SecondIface<?>> {}
    static class SecondClassA implements SecondIface<SecondClassA> {}
    static class SecondClassB implements SecondIface<SecondClassB> {}

    public static void main(String... args) {
        {
            FirstIface<?> i = new FirstClassA();
            FirstClassA a = i.as(FirstClassA.class);
            FirstClassB b = i.as(FirstClassB.class); // runtime exception
            SecondClassA x = i.as(SecondClassA.class); // compile exception
            SecondClassB y = i.as(SecondClassB.class); // compile exception
        }
        {
            SecondIface<?> i = new SecondClassA();
            FirstClassA a = i.as(FirstClassA.class); // compile exception
            FirstClassB b = i.as(FirstClassB.class); // compile exception
            SecondClassA x = i.as(SecondClassA.class);
            SecondClassB y = i.as(SecondClassB.class); // runtime exception
        }
        new FirstClassA().as(FirstClassB.class); // unfortunately, this compiles fine :-(
    }

Upvotes: 0

Related Questions