There are a number of different ways to get polymorphism. The one you are most familiar with is inclusion polymorphism (also known as subtype polymorphism), where the programmer explicitly says "X is-a Y" via some sort of extends clause. You see this in Java and C#; both give you the choice of having such an is-a for both representation and API (extends
), or only for API (implements
).
There is also parametric polymorphism, which you have probably seen as generics: defining a family of types Foo<T>
with a single declaration. You see this in Java/C#/Scala (generics), C++ (templates), Haskell (type classes), etc.
Some languages have "duck typing", where, rather than looking at the declaration ("X is-a Y"), they are willing to determine typing structurally. If a contract says "to be an Iterator
, you have to have hasNext()
and next()
methods", then under this interpretation, any class that provides these two methods is an Iterator
, regardless of whether it said so or not. This comports with the case you describe; this was a choice open to the Java designers.
Languages with pattern matching or runtime reflection can exhibit a form of ad-hoc polymorphism (also known as data-driven polymorphism), where you can define polymorphic behavior over unrelated types, such as:
int length(Object o) {
return switch (o) {
case String s -> s.length();
case Object[] os -> os.length;
case Collection c -> c.size();
...
};
}
Here, length
is polymorphic over an ad-hoc set of types.
It is also possible to have an explicit declaration of "X is-a Y" without putting this in the declaration of X. Haskell's type classes do this, where, rather than X declaring "I'm a Y", there's a separate declaration of an instance
that explicitly says "X is a Y (and here is how to map X functionality to Y functionality if it is not obvious to the compiler.)" Such instances are often called witnesses; it is a witness to the Y-hood of X. Clojure's protocols are similar, and Scala's implicit parameters play a similar role ("find me a witness to CanCopyFrom[A,B]
, or fail at compile time").
The point of all this is that there are many ways to get polymorphism, and some languages pick their favorite, others support more than one, etc.
If your question is why did Java choose explicit subtyping rather than duck typing, the answer is fairly clear: Java was a language designed for building large systems (as was C++) out of components, and components want strong checking at their boundaries. A loosey-goosey match because the two sides happen to have methods with the same name is a less reliable means of establishing programmer intent than an explicit declaration. Additionally, one of the core design principles of the Java language is "reading code is more important than writing code." It may be more work to declare "implements Iterator" (but not a lot more), but it makes it much more clear to readers what your design intent was.
So, this is a tradeoff of what we might now call "ceremony" for greater reliability and more clear capture of design intent.