public class Sundew extends Plant implements InsectConsumer { public void consumeInsect(Insect i) { ... } } // ласточка расширяет класс птица public class Swallow extends Bird implements InsectConsumer { public void consumeInsect(Insect i) { ... } } // муравьед расширяет класс млекопитающее public class AntEater extends Mammal implements InsectConsumer { public void consumeInsect(Insect i) { ... } }
В результате в классе, моделирующем служащего зоопарка, можно объявить соответствующий метод:
// служащий, отвечающий за кормление, // расширяет класс служащий class FeedWorker extends Worker { // с помощью этого метода можно накормить // и росянку, и ласточку, и муравьеда public void feedOnInsects(InsectConsumer consumer) { ... consumer.consumeInsect(insect); ... } }
В результате удалось свести работу с одним свойством трех разнородных классов в одно место, сделать код более универсальным. Обратите внимание, что при добавлении еще одного насекомоядного такая модель зоопарка не потребует никаких изменений, чтобы обслуживать новый вид, в отличие от первоначального громоздкого решения. Благодаря введению интерфейса удалось отделить классы, реализующие его (живые организмы) и использующие его (служащий зоопарка). После любых изменений этих классов при условии сохранения интерфейса их взаимодействие не нарушится.
Данный пример иллюстрирует, как интерфейсы предоставляют альтернативный, более строгий и гибкий подход вместо множественного наследования.
Полиморфизм
Ранее были рассмотрены правила объявления классов с учетом их наследования. В этой лекции было введено понятие переопределенного метода. Однако полиморфизм требует более глубокого изучения. При объявлении одноименных полей или методов с совпадающими сигнатурами происходит перекрытие элементов из родительского и наследующего класса. Рассмотрим, как функционируют классы и объекты в таких ситуациях.
Поля
Начнем с полей, которые могут быть статическими или динамическими. Рассмотрим пример:
class Parent { int a=2; } class Child extends Parent { int a=3; }
Прежде всего, нужно сказать, что такое объявление корректно. Наследники могут объявлять поля с любыми именами, даже совпадающими с родительскими. Затем, необходимо понять, как два одноименных поля будут сосуществовать. Действительно, объекты класса Child будут содержать сразу две переменных, а поскольку они могут отличаться не только значением, но и типом (ведь это два независимых поля), именно компилятор будет определять, какое из значений использовать. Компилятор может опираться только на тип ссылки, с помощью которой происходит обращение к полю:
Child c = new Child(); System.out.println(c.a); Parent p = c; System.out.println(p.a);
Обе ссылки указывают на один и тот же объект, порожденный от класса Child, но одна из них имеет такой же тип, а другая – Parent. Отсюда следуют и результаты:
3 2
Объявление поля в классе-наследнике 'скрыло' родительское поле. Данное объявление так и называется – 'скрывающим' (hiding). Это особый случай перекрытия областей видимости, отличный от 'затеняющего' (shadowing) и 'заслоняющего' (obscuring) объявлений. Тем не менее, родительское поле продолжает существовать. К нему можно обратиться и явно:
class Child extends Parent { int a=3; // скрывающее объявление int b=((Parent)this).a; // более громоздкое объявление int c=super.a; // более простое