Everyone likes to give inheritance a hard time these days. Compose, don't inherit, so they say. Well, time for me to jump onto the bandwagon. Here is reason #6701 why inheritance is a problem.

Inheritance in traditional OOP introduces implicit single-dispatch polymorphism at every call site. When you call .myMethod() on SomeClass someObject, you may think you know what behaviour you're eliciting, but you actually don't. Because if SomeChildClass extends SomeClass, and later you or someone else decides to @override myMethod(), then the caller of .myMethod() no longer has any control over what happens.

Contrast this with generics-based polymorphism. When I call a generic myProc<T>(), at the call site, I have full control over where control flow goes. I know at compile time exactly what will be executed. This makes myProc<T>() nice and usable from the caller's perspective.

Same thing for ad hoc polymorphism. When I call myProc(float x), I know that it's not myProc(String x) that will get run.

(Not all inheritance has this problem, you specifically need dynamic dispatch, i.e.: virtual methods. So this isn't as much of a problem in C++ for example, since you know at the call site that unpredictable runtime polymorphism might occur because you know can see the method is virtual.)

Of course, the fact that you don't know where control flow is the whole point of OOP, right? Hide the implementation. Yes! Absolutely. Information hiding is awesome. Modularity all the way. But, as has been pointed out by many before me, the object just never seems to be the right scale at which to create an interface-implementation split. Modules are.

This ends up incentivising code which is really hard to understand, where you have to jump around between classes a lot. Because let's be honest: most methods or procedures just don't have the kind of in-depth documentation of exactly what it's supposed to do and which post-conditions need to be maintained, information which is critical for a person extending your class in the future. So TheirChildClass ends up violating that implicit post-condition which callees of the method are quietly depending on.

This reminds me of nothing less than the dreaded goto. goto is bad because it creates an unstable relationship between the gotoer and the gotoee. The gotoer gets no guarantees about if, when, nor how execution will return to it. This is implicit control flow, because the goto could send you anywhere compared to the current statement, before or after, creating the potential for complicated unexpected dependencies.

With inheritance, you have implicit polymorphism. Sure, you're still doing structured programming, so at least you know the method will return. So there's no uncertainty in the caller control flow. But there is unpredictability in the behaviour of the method call, which leaks upwards to make the caller unpredictable as well.