When you call an overridden method on a parent-type variable, Java doesn't use the parent's version. It looks at the actual object at runtime and calls that object's version. This is called dynamic method dispatch.
Animal a = new Dog();
a.speak();
The compiler sees a as Animal and confirms that Animal has speak(). At runtime, Java sees that a holds a Dog, so it calls Dog's speak() and prints Woof!.
This decision happens every time the method is called. If you reassign a = new Cat() and call a.speak() again, Java dispatches to Cat's version.
Dynamic dispatch only works with instance methods. Static methods are resolved at compile time based on the declared type, not the actual object.