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.
- 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.
- We can subtype this function in the following 2 ways:
- 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
.
- 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!
- See that
Prime
is a subclass of Integer
: you can still add, multiply and invert them, but you can also use them for cryptography.
- 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
.
- 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.