15

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.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • Which JDK version are you planning to use? – SnowmanXL Oct 08 '20 at 09:55
  • This needs to compile on Java 11+. – Rafael Winterhalter Oct 08 '20 at 10:00
  • 2
    Maybe I'm blind, but I don't see the structural difference between the `FirstIface` and the `SecondIface` snippet that might render the second one illegal at compile-time while the first one isn't. – Ralf Kleberhoff Oct 08 '20 at 10:08
  • 1
    If you want to allow to cast only to `FirstIface`, but not other, why don't you declare it such by `default `? Or, as others point out, how else do you decide which is actually `FirstIface`, which you allow and which is `SecondIface`, which you forbid. – František Hartman Oct 08 '20 at 11:10
  • I want to allow providing any class as an argument that actually implements `FirstIface`, which are `FirstClassA` and `FirstClassB`. I want to forbid classes that do not implement `FirstIface` such as `SecondClassA` and `SecondClassB`. But since the wildcard could be any subtype of `FirstIface`, it does not allow for any subtype of `FirstIFace`. I'd like to permit any subtype of `IFirstIface` but not any other type. – Rafael Winterhalter Oct 08 '20 at 11:12
  • So, do you want `FirstIface` to be "hardcoded" into the `as()` method, such that it only compiles with `FirstIface`-implementing classes, and not with others? – Ralf Kleberhoff Oct 08 '20 at 12:50
  • I think I got it now: you want the `as()` method to place two restrictions on its argument. A `Base` should only accept types that are both subclasses of `X` and implementations of `FirstIface`. Is that your problem? – Ralf Kleberhoff Oct 08 '20 at 13:03
  • Yes, they should be subclasses of the raw interface, in a way. – Rafael Winterhalter Oct 08 '20 at 13:57
  • Maybe this answer will help you: https://stackoverflow.com/a/7355094 – Carlos Melo Oct 08 '20 at 19:35

3 Answers3

1

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;
    }
}
John Vasileff
  • 5,292
  • 2
  • 22
  • 16
  • This is elegant, but it defeats the convenience I am aiming for. I'd like to make the compiler help out in a large, autogenerated type hierarchy. If you'd need to explicitly define the arguments every time, I doubt people would use it. – Rafael Winterhalter Oct 09 '20 at 11:27
  • Right, but the key point is that what you are trying to do requires covariant method parameter types (parameter types that are *more* restrictive in subtypes), and that is unsound and not possible in Java. If covariant parameter types were allowed, you would be able to write totally type-safe looking code that crashes with a type error at runtime. – John Vasileff Oct 09 '20 at 12:45
  • I was afraid so, but it would not be the first time that some mindtrick can accomplish this, especially now that the code already is generated. My impression was however also that it's not possible. – Rafael Winterhalter Oct 09 '20 at 13:13
0

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 :-(
    }
Xavier Dury
  • 1,530
  • 1
  • 16
  • 23
  • Unfortunately, this makes it impossible to create hierarchies. I added this scenario to my question by `ThirdIface`, other then that, this is an elegant approach. – Rafael Winterhalter Oct 09 '20 at 11:28
0

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
    }
}
František Hartman
  • 14,436
  • 2
  • 40
  • 60