-1

Consider this code:

public interface I { I f(I x); }

public class C1 implements I { public I  f (I  x) { return null; } }

public class C2 implements I { public C2 f (I  x) { return null; } }

public class C3 implements I { public I  f (C3 x) { return null; } }

public class C4 implements I { public C4 f (C4 x) { return null; } }

public class C5 implements I { public C1 f (I  x) { return null; } }

If you try to compile, you will see that C3 and C4 fail:

C3 is not abstract and does not override abstract method f(I) in I

C4 is not abstract and does not override abstract method f(I) in I

So I am allowed to specialize the return type, but the argument must remain abstract. In other words, I am allowed to specify the return type of the method (it may even be C2 in the definition of C1 — I can mix), but I cannot restrict the arguments, so f is always polymorphic on the input.

  • What is the logic behind this situation?
  • Why is it not allowed to specialize the argument?
  • Why is it allowed to specialize the return type to the type of this or keep it abstract?
  • Why is it allowed to specialize the return type to another implementation of the same interface? (Like in C5.)

 


P.S. That other question about covariant return obviously does not cover all the points above, if any.

  • I am not asking "what", I am here asking "why".
  • Yet again, my question here is about relation of a class to an interface, not subclassing in general (even if we do consider implementing an interface a case of subclassing, which is an open, though philosophical, question in itself).
  • And yet once more, that question is general, while my question is specific to Java.
  • Finally, the wording is completely different. I do not even know where this "covariant" comes from. Like in "vector" covariant? "Functor" covariant? If there is deep analogy, it has got to be explained.

So please kindly reopen.

Ignat Insarov
  • 4,444
  • 13
  • 34
  • If you want academic references, then you'd probably have a better chance on [stackexchange.cs](https://cs.stackexchange.com/). – Kayaman Oct 10 '19 at 10:32
  • @Kayaman I welcome you to check out my own answer, since you were instrumental to it being written. – Ignat Insarov Oct 10 '19 at 14:07
  • I don't have much input. I still think this question is more fitting on CS, since it's about how Java works vs. "what if things were different". – Kayaman Oct 11 '19 at 07:54
  • @Kayaman I am ambiguous about asking such stuff on CS since it is very specific to Java. I can fork out a separate question around, say, type checking decidability in the presence of argument subtyping. But a major part of the question is how things work in Java. For instance, whether in Java inheritance is compatible with subtyping decides whether we can speak of function type variance at all. And then there is the implicit `this` argument that seemingly shatters the whole function subtyping picture. If you have suggestions on how best to approach my little research here, I welcome an advice. – Ignat Insarov Oct 11 '19 at 08:25

2 Answers2

1

Let's see.

public interface I { I f(I x); }
I[] all = { new C1(), new C2(), new C3(), new C4(), new C5() };
I argument = availableArguments[Random.nextInt(all.length)];
I c1 = new C1();
I c2 = new C2();
I c3 = new C3();
I c4 = new C4();
I c5 = new C5();

public class C1 implements I { public I  f (I  x) { return null; } }
I result = c1.f(argument); // ok; argument is an I, an I is returned

public class C2 implements I { public C2 f (I  x) { return null; } }
I result = c2.f(argument); // ok; argument is an I, returned C2 can be assigned to I

public class C3 implements I { public I  f (C3 x) { return null; } }
I result = c3.f(argument); // would fail: argument is not of type C3 as required

public class C4 implements I { public C4 f (C4 x) { return null; } }
I result = c4.f(argument); // would fail: argument is not of type C4 as required

public class C5 implements I { public C1 f (I  x) { return null; } }
I result = c5.f(argument); // ok; argument is an I, returned C1 can be assigned to I
daniu
  • 12,131
  • 3
  • 23
  • 46
  • Surely we must require that the argument type is correct, and we can do that at compile time. Nothing breaks, no one is hurt. Why not? – Ignat Insarov Oct 10 '19 at 11:13
  • @IgnatInsarov How can we ensure that at compile time? The `argument` in my example is of type `I` which is all the compiler knows. We don't know how it was created. I adjusted the code to make this more clear. – daniu Oct 10 '19 at 11:17
  • The compiler might, for instance, attach a _compile time_ tag to each identifier. If the tag on the argument says that the argument is a fitting subtype of I, then it accepts the invocation of `f` as well typed. If the tag says that the argument is in fact a distinct subtype, or even an unknown subtype _(which is to say, the variable is of abstract class, also known as interface)_, then the compiler rejects the code. **...Or there may be some other way.** Indeed, your elaborate construction with factories is completely superfluous here, so it is healthy to reject this particular program. – Ignat Insarov Oct 10 '19 at 11:35
  • @IgnatInsarov Yeah the "tagging" thing is what generics are for. I don't see how my factory is either elaborate or superfluous, but reject away. – daniu Oct 10 '19 at 11:47
  • I do not see how generics fit in this picture. If I reject your factory edit, then we are back at my comment №1: the compiler knows quite well how the argument was created. I am not at all insisting that you are wrong in some way, but there are holes in your reasoning, so you may want to amend your answer with more details. – Ignat Insarov Oct 10 '19 at 11:52
  • @IgnatInsarov I implemented you a `Supplier`. Good luck attaching a compile-time tag. – daniu Oct 10 '19 at 12:06
  • On the one hand, the compiler can now safely sort the correct invocations of `C2.f` from the possibly faillible invocations of `C3.f` and forbid only the latter. On the other hand, that I can or cannot think of a way to do something in a few minutes' time has nothing to do with the general impossibility of doing it, which fact I stated in bold a few comments above. If you have a point, be making it. If you think there is no way to rule out incorrect invocations of `C3.f` at compile time, then state it in your answer and give some arguments for your belief, if not a complete proof. – Ignat Insarov Oct 10 '19 at 12:51
  • The compiler forbids the definition, not the invocation. – Ignat Insarov Oct 10 '19 at 13:56
0

The story goes some way here.

Interfaces are a kind of classes.

It is not immediately obvious, but another fitting name for an "interface" of Java would be an "abstract class". That interfaces are in fact a subset of classes is of course questionable, so we should question it separately. If we find out that it is false, we should revisit this answer as well. But for now, let us assume that it is true.

We also have to assume that Java "classes" are a kind of types. This can, again, be questioned in itself, elsewhere. If we assume this, we can draw some insight from the theory of types. What we need specifically is the notion of subtyping. In short, that C is a subtype of I means that when you have a variable of type I in your code, you can replace it with that of type C and some part of your code will still work.

Side notes:

  • Of course, you can never actually create a value of type I, since I is an abstract class — it has no definition; but you may consider a value of which it is known only that it is of some class that implements I.
  • Which part of your code will work afterwards depends on the exact definition of subtyping, of which there are some — read on to find out which definition Java is using.

Argument and return types are fundamentally distinct with respect to subtyping.

This is where covariance and contravariance enter. These terms have come to computer science from physics and through category theory. That seems far fetched, but take a look at how subtyping plays with functions and you are sure to see how it all aligns.

Here is an example. It is a bit involved, so take a pen, some paper and check it for yourself.

  1. Take a function f: Integer -> Boolean. It takes a number and returns a truth value. For instance, let it tell us whether the number is even.
  2. We can subtype this function in the following 2 ways:
    1. Surely, if it works on any number, it will work on primes, so we can use the same function like this: f -- specialize argument --> f1: Prime -> Boolean. (3 still gives False, 2 gives True. All is good.) Where the latter works, the former would work too. So, f1 is a subclass of f.
    2. It is also true that we can use the same function like this: f -- generalize result --> f2 = Integer -> Byte. (We would simply convert True to 0x01 and False to 0x00.) Where f2 is needed, f will do. So, f2 is a subclass of f too!
  3. See that Prime is a subclass of Integer: you can still add, multiply and invert them, but you can also use them for cryptography.
  4. See that Bool is a subclass of Byte: you can still store it in memory, but you can also store an array of Bool packed, in 8 times smaller memory than an array of Byte.
  5. Now observe: when we move the argument of f in the direction of subtyping (specialize it), the function also moves in that direction, while the result type has to be moved in the opposite direction (get generalized) for a function to be subtyped. The reader may actually draw this and see how it aligns with the definition of vector variance in physics.

This is of course not a real proof, just some hand waving. I have no real proof. Do question!

Subclassing in Java aligns with a certain "subtyping" partial order on types.

The famous Barbara Liskov's subtype requirement is one way to define the subtyping relation. That particular definition is called "behavioural" subtyping, and it opens a deep rabbit hole of undecidability which may or may not be of a concern for us. (So again we have a question.) It is here we have to take it on faith that Java is faithful in its rendition of behavioural subtyping. (Habitually question it. One possible way to drive a wedge is look attentively at the relationship between subtyping and inheritance.)

So let us suppose that inheritance is compatible with subtyping in Java. What does it give us?

A function with subtyped argument is not a subtype of the original.

That is, a method of a subtyped item is not a subtype of the original.

So suppose you have this function that performs RSA encryption and it just needs one prime, f: Prime -> X. If you put this function in situation where a non-prime argument can be given to it, then it will likely create a message that is encrypted very weakly. You introduced a bug. So, while you can always put a prime to a place where a number is expected, you cannot put a method invocation that requres a prime to a place where a mere number will be given to it as an argument.

But the object the method is invoked from is itself an implicit argument, you may ask, so subclassing should be forbidden altogether! Indeed, suppose I define a method of Integer that computes a possibly weak RSA key and subclass Prime from this Integer. Now the same method, with the same evident types, computes a strong RSA key, because its implicit argument has been subtyped — and, if I want strong keys, I cannot blindly replace Integer with Prime.

This is the first big question of our fantasy adventure.

(I am very fuzzy at this point.)

Java tries to make sure an implementation of a method is always a subtype of its definition.

One way of ensuring that is to only allow subtyping the result. (Is it the only way? Right way? Is it actually safe?)

For some reason, Java does not try to give type errors at use site, but simply forbids the definition.

What is the reason for that is our big question number two. Possibly because it wants to align subtyping and inheritance.

Ignat Insarov
  • 4,444
  • 13
  • 34
  • This answer is a work in progress. It is actually sewn out of questions itself. So I will be revisiting it at a later time, when I get some _sub_-answers. – Ignat Insarov Oct 10 '19 at 13:58
  • One problem with this answer is that in Java, [interfaces and abstract classes are two different things](https://stackoverflow.com/questions/761194/interface-vs-abstract-class-general-oo), so the terms will need to be adjusted. – Ignat Insarov Oct 10 '19 at 14:46