It’s not clear on what your question is actually founded.
When you have code of the form
Dog d = new Dog();
d.eat();
the static type of d
is Dog
and hence, the compiler will encode an invocation of Dog.eat()
into the class file, after checking that the invocation is correct.
For the invocation, there are several scenarios possible
Dog
might declare a method eat()
that overrides a method with the same signature in its superclass Animal
, like in your example
Dog
might declare a method eat()
that does not override another method
Dog
might not declare a matching method, but inherit a matching method from its superclass or implemented interfaces
Note that it is completely irrelevant, which scenario applies. If the invocation is valid, it will get compiled to an invocation of Dog.eat()
, regardless of which case applied, because the formal static type of d
, on which eat()
is invoked, is Dog
.
Being that agnostic to the actual scenario also implies that at runtime, you might have a different version of the class Dog
, to which another scenario applies, without breaking the compatibility.
It would be a different picture if you had written
Animal a = new Dog();
a.eat();
Now the formal type of a
is Animal
and the compiler will check whether Animal
contains a declaration for eat()
, be it overridden in Dog
or not. This invocation will then be coded as targeting Animal.eat()
in the byte code, even though the compiler could deduce that a
is actually a reference to a Dog
instance. The compiler just follows the formal rules. This implies that this code would not work, if the runtime version of Animal
lacked an eat()
method, even if Dog
has one.
This implies that it would be a dangerous change to remove a method in a base class, but you can always refactor your code adding a more abstract base class and move methods up the class hierarchy, without affecting the compatibility with existing code. This was one of the goal of the Java designers.
So perhaps, you compiled one of the two example above and later, you’re running your code with a newer library version, in which the type hierarchy is Animal
>Carnivore
>Dog
and Dog
hasn’t an implementation of eat()
, because the natural place for the most specific implementation is Carnivore.eat()
. In that environment, your old code will still run and do the right thing, without problems.
Further note that even if you recompile your old code without changes, but using the newer library, it will stay compatible with the old library version, as in your code, you never refer to the new Carnivore
type and the compiler will use the formal types, you use in your code, Animal
or Dog
, not recording the fact that Dog
inherits the method eat()
from Carnivore
into the compiled code, according to the formal rules as explained above. No surprises here.