I am talking specifically about the state of affairs in Java. We may try to do the same trick in Scala; my point here though was to have a real-life, although contrived, example of how subclassing is different from subtyping, and Java is a easy target.
First, definitions (informal, as well as the rest of this post).
Types in Java.
Java has the following kinds of types:
- primitive (e.g. char)
- null
- interfaces (declared with interface keyword)
- classes (declared with class keyword)
- array (declared with [square brackets])
- type variables (used in generics)
Subclassing: declaring one class to be a subclass of another (class ... extends ...) - this allows a subclass to inherit functionality from its superclass
Subtyping: A is a subtype of B if an instance of A can be legally placed in a context where an instance of B is required. The understanding of "legally" may vary.
Many sources state firmly that in Java, a subclass is always a subtype. Opinions that a subclass is not always a subtype are also widespread; and that's obviously true for some languages: Self type is a good source of subclass not being a subtype confusion; we do not have Self type in Java.
In Java 5 a covariant return type inheritance was added: if class A has a method that returns X, and its subclass B declares a method with the same name and parameter list (meaning, parameter types), and returning a subclass of X, then this method overrides the method in A.
In addition to this, arrays are also covariant: if A is a subtype of B, then A[], in Java, is a subtype of B[].
This last feature allows us to create an example showing that no! Subclasses in Java are not always subtypes! See:
public class Subtyping
{
interface A
{
A[] m();
void accept(A a);
}
interface B extends A // subtype of A
{}
public static void main(String[] args) {
A a = new AImpl(); // see AImpl below
B b = new BImpl(); // see BImpl below
// now note that B is a subtype of A.
a.accept(a); // this works; substitution is trivial
b.accept(a); // this works too (substitution is trivial too)
a.accept(b); // oops, this fails! b, being a subtype of a, is not accepted at runtime
}
static class AImpl implements A {
public A[] m()
{
return new A[]{this};
}
public void accept(A a)
{
a.m()[0] = this;
}
}
static class BImpl extends AImpl implements B{
public B[] m()
{
return new B[]{this};
}
}
}
So there, this code demonstrates a subclass that is not legally a subtype. By 'legally' here I mean that the code always throws a Java runtime exception without adding any client precondition predicates.
Here's a good discussion of this issue:
public boolean lspHolds(Object o) {
return o.toString().contains("@");
}