1. Но и это еще не все

Предположим, в классе Cow есть метод printAll(), который вызывает два других метода. Тогда код будет работать так:

Код Описание
class Cow
{
   public void printAll()
   {
      printColor();
      printName();
   }

   public void printColor ()
   {
      System.out.println("Я — белая");
   }

   public void printName()
   {
      System.out.println("Я — корова");
   }
}

class Whale extends Cow
{
   public void printName()
   {
      System.out.println("Я — кит");
   }
}
public static void main(String[] args)
{
   Whale whale = new Whale ();
   whale.printAll();
}
На экран будет выведена надпись:
Я — белая
Я — кит

Обратите внимание: когда вызывается метод printAll(), написанный в классе Cow, у объекта типа Whale, используется метод printName класса Whale, а не Cow.

Главное — не в каком классе написан метод, а какой тип (класс) объекта, у которого этот метод вызван.

Наследовать и переопределять можно только нестатические методы. Статические методы не наследуются и, следовательно, не переопределяются.

Вот как выглядит класс Whale после применения наследования и переопределения методов:

class Whale
{
   public void printAll()
   {
      printColor();
      printName();
   }

   public void printColor()
   {
      System.out.println("Я - белая");
   }

   public void printName()
   {
      System.out.println("Я - кит");
   }
}
Вот как выглядит класс Whale после применения наследования и переопределения метода. Ни о каком старом методе printName мы и не знаем.

2. Приведение типов

Тут есть еще более интересный момент. Т.к. класс при наследовании получает все методы и данные класса-родителя, объект этого класса разрешается сохранять (присваивать) в переменные класса-родителя (и родителя родителя, и т.д., вплоть до Object). Пример:

Код Описание
public static void main(String[] args)
{
   Whale whale = new Whale();
   whale.printColor();
}
На экран будет выведена надпись:
Я — белая
public static void main(String[] args)
{
   Cow cow = new Whale();
   cow.printColor();
}
На экран будет выведена надпись:
Я — белая
public static void main(String[] args)
{
   Object o = new Whale();
   System.out.println(o.toString());
}
На экран будет выведена надпись:
Whale@da435a.

Метод toString() унаследован от класса Object

Это очень ценное свойство: немного позже вы поймете, как это использовать на практике.


3. Вызов метода объекта

При вызове метода у переменной, реальный метод вызывается у объекта. Этот механизм называется динамической диспетчеризацией методов.

Вот как это выглядит:

Код Описание
public static void main(String[] args)
{
   Whale whale = new Whale();
   whale.printName();
}
На экран будет выведена надпись:
Я — кит
public static void main(String[] args)
{
   Cow cow = new Whale();
   cow.printName();
}
На экран будет выведена надпись:
Я — кит

Обратите внимание, что на то, какой именно метод printName() вызовется, от класса Cow или Whale, влияет не тип переменной, а тип объекта, на который она ссылается.

В переменной типа Cow сохранена ссылка на объект типа Whale, и будет вызван метод printName(), описанный в классе Whale.

Это не очень очевидно. Запомните главное правило:

Набор методов, которые можно вызвать у переменной, определяется типом переменной. А какой именно метод/какая реализация вызовется, определяется типом/классом объекта, ссылку на который хранит переменная.

Вы будете постоянно сталкиваться с этим, так что чем раньше запомните, тем лучше.