Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Self-type - requiring a type within a type bound

Tags:

java

generics

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.

like image 788
Rafael Winterhalter Avatar asked Oct 08 '20 09:10

Rafael Winterhalter


1 Answers

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;
    }
}
like image 166
John Vasileff Avatar answered Oct 24 '22 07:10

John Vasileff