Skip to main content

04. Наследование

Теория

Наследование - это механизм языка, позволяющий описать новый класс на основе уже существующего (родительского, базового) класса. Класс-потомок может добавить собственные методы и поля, а также обращаться к родительским методами и полям. Это позволяет строить иерархии классов. Родительские классы в данном случае принято называть суперклассами.

Некоторые объекты являются частным случаем более общей категории объектов.

Например, авторучка, фломастер и простой карандаш являются частными случаями категории «письменные принадлежности». С одной стороны, каждый из объектов обладает отдельными характеристиками. С другой стороны, они имеют общие свойства (например, цвет).

Хотелось бы иметь возможность рассматривать их как объект общей категории «письменные принадлежности», т.к. для всех этих объектов применимы общие действия (например, «писáть»), но при этом уметь хранить для каждого в отдельности его особенные свойства и использовать присущие каждому специфические методы.

Например, для авторучки хотелось бы различать ее тип (шариковая, гелевая, роллер) и уметь менять стержень. Для фломастера - знать перманентный он или нет. А для карандаша - знать, простой он или цветной и уметь его точить.

Используя технологию наследования, говорят, что классы «фломастер», авторучка и карандаш - наследники класса «письменная принадлежность». Объект «письменная принадлежность» хранит информацию о цвете и к нему может быть применена операция «писáть».

Еще принято говорить, что класс «письменной принадлежности» является родительским по отношению к классам «фломастер», «ручка» и «карандаш». Каждый из классов: «фломастер», «авторучка» и «карандаш» наследуют от родительского класса свойство "цвет" и метод «писáть». И при этом для каждого из них мы добавляем свои дополнительные свойства и методы.

Грубо говоря, язык программирования за нас копирует всё, что мы напишем в родительском классе, в дочерний (унаследованный от родительского) каждый раз при компиляции, поэтому даже если мы изменим родительский класс, дочерний изменится автоматически.

Наследование в Java

В Java можно наследовать класс только от одного родителя.

Пример

Рассмотрим пример из жизни. Существуют различные предметы быта, на которых сидят люди: стул, кресло, табуретка, диван и так далее. Объединим их по функции, для которой они предназначены (чтобы на них сидеть), и выделим их в единый суперкласс(родительский) Sittings.

Sittings – это класс, который имеет общие свойства и методы для всех сидячих мест: число сидячих мест в одном объекте класса, его габариты, вес и так далее. Таким образом мы в Sittings включили только те свойства, которые относятся ко всем без исключения предметам быта, на которых можно сидеть.

Теперь перечислим те классы, которые могут наследоваться от класса Sittings: ArmChair (кресло), Chair (стул), Stool (табуретка), Sofa (диван). Функционально эти три объекта предназначены для того, чтобы на них сидеть. Следовательно, они являются наследниками класса Sittings.

В связи с тем, что они наследуются от Sittings в них помимо их собственных свойств включаются общие свойства от предка: число сидячих мест, габариты, вес и так далее.

Помимо общих свойств, которые получены от суперкласса, у наследников есть так же и их индивидуальные свойства. У табуретки, например, есть свойство – число ножек (3, 4 или даже 5).

Чтобы унаследовать класс в java, достаточно после имени класса дописать extends Имя_Суперкласса. В роли суперкласса у нас будет выступать класс Sittings.

public class Main {
public static void main(String[] args) {
Sittings sitting = new Sittings();
System.out.println(sitting);
Chair chair = new Chair(4);
System.out.println(chair.weight);
Sofa sofa = new Sofa();
System.out.println(sofa);

Chair chair = new Chair();
Sittings sittings = new Sittings();
sofa = new Sofa(4,4,1,2,"Ткань");

// выводим текстовые представления объектов
System.out.println("toString: "+chair);
System.out.println("toString: "+sittings);
System.out.println("toString: "+sofa);
}
}

На консоль будет выведено:

сидение на 1 мест
100
Sofa: Кожа
toString: сидение на 1 мест
toString: сидение на 1 мест
toString: Sofa: Ткань (сидение на 1 мест)

Переопределение конструктора

С переопределением конструкторов есть проблема: если мы напишем такое же имя метода, как и в родителе, то по отношению к потомку такой метод не будет конструктором.

Обратите внимание

В Sofa.toString() мы вызываем реализацию метода родителя. В общем виде, чтобы использовать реализацию метода, определённую в предке, нужно написать super.название_метода().

Поэтому при переопределении конструкторов мы определяем новый конструктор в потомке и вызываем из его тела конструктор суперкласса при помощи метода super(параметры). Аннотацию @Override писать не нужно. Причём благодаря полиморфизму будет вызван конструктор, который соответствует указанному набору параметров. Если в конструкторе предполагается вызов конструктора суперкласса по умолчанию, т.е. без параметров, то его можно не вызывать, т.к. java сама подставит конструктор по умолчанию, если не будет вызван ни один из конструкторов. Ничто не мешает Вам вызвать конструктор по умолчанию суперкласса super() без параметров, но это бесполезная команда.

Также существует ещё одно важное правило: метод super() должен вызываться первой командой в теле конструктора дочернего класса.

Полиморфизм

Т.к. при объявлении объекта класса компилятор выделяет память в соответствии с описанием класса, то мы можем объявить переменную родительского класса, а выделить память под дочерний. Правда, тогда мы сможем использовать только методы и переменные, объявленные в родительском классе.

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

Используя полиморфизм, мы можем не думать, объект какого класса перед нами, мы просто обращаемся с ним как с объектом родительского класса. При этом методы работают именно так, как указано в реализации того или иного класса. Интерфейс получается одинаковым, а реализация заявленной совокупности может быть разной у разных объектов.

Из этого следует, что если метод определён только в потомке, то вызывать его от переменной родительского класса нельзя, т.к. он отсутствует в его определении(спецификации).

        Sittings sitting = new Sittings();
System.out.println(sitting);
Chair chair = new Chair(4);
System.out.println(chair.weight);
Sittings sofa = new Sofa();
System.out.println(sofa);
Sittings chair = new Chair();
Sittings sittings = new Sittings();
sofa = new Sofa(4,4,1,2,"Ткань");

// выводим текстовые представления объектов
System.out.println("toString: "+chair);
System.out.println("toString: "+sittings);
System.out.println("toString: "+sofa);

Это может показаться малополезным, но теперь объекты, на которые ссылаются переменные sofa, chair и sitting имеют интерфейс класса Sittings. Тогда мы можем объединить их в один массив:

public class Main {
public static void main(String[] args) {
Sittings sitting = new Sittings();
System.out.println(sitting);
Chair chair = new Chair(4);
System.out.println(chair.weight);
Sofa sofa = new Sofa();
System.out.println(sofa);
Sittings [] sArr = {
new Chair(),
new Sittings(),
new Sofa(4,4,1,2,"Ткань")
};
// выводим текстовые представления объектов
for (Sittings s: sArr){
System.out.println("toString: "+s);
}
// используем сидение
for (Sittings s: sArr){
System.out.print("use: ");
s.use();
}
}
}

На консоль будет выведено:

сидение на 1 мест
100
Sofa: Кожа
toString: сидение на 1 мест
toString: сидение на 1 мест
toString: Sofa: Ткань (сидение на 1 мест)
use: используем стул
use: используем сидение
use: используем диван

Инкапсуляция

Сокрытие внутренней логики работы той или иной сущности называется инкапусляцией. При работе с переменными базового типа от нас скрывается логика работы процессорных регистров. Точно также следует скрывать логику работы внутренней структуры класса.

Другим объектам должны быть доступны минимальное количество полей и методов.

Например, объект реального мира - микроволновая печь. Для того, чтобы ею пользоваться, нужно только складывать в нее блюда и кнопками задавать время разогрева. Пользователю не нужно знать, как внутренняя тарелка должна вращаться, какой ток и какое напряжение нужно подавать на элементы внутренней электроники.

Если пользователю позволить менять эти параметры напрямую, он может задать такую их комбинацию, после которой микроволновая просто сломается.

С другой стороны, если изготовитель микроволновой печи пришлет мастера, который изменит принцип ее внутренней работы, пользователь этого не почувствует - в его распоряжении все также останется дверца и кнопки, задающие время разогрева.

Микроволновая печь в этом примере (как и инкапсулированный объект) - это «черный ящик» для пользователя. Таким образом ООП дает возможность пользователю пользоваться объектами, не задумываясь об их внутреннем устройстве. (Например, классы String и StringBuilder)

Чтобы разграничить доступность того или иного элемента класса (метода или поля), используются четыре модификатора доступа:

  • public - публичный, доступен отовсюду
  • default - по умолчанию, доступен всем внутри одной папки(пакета), если не указать модификатор доступа, он будет модификатором доступа по умолчанию
  • protected - защищённый, доступен только внутри класса и его потомкам
  • private - закрытый, доступен только внутри класса

Считается хорошим тоном делать все поля закрытыми, задавать их значения с помощью специальных методов-геттеров, а задавать значения с помощью сеттеров.

У геттера поля названиеПоля должно быть строго определённое название вида: getНазваниеПоля, тип возвращаемого значения должен совпадать с типом рассматриваемого поля, а аргументы отсутствовать.

У геттера поля названиеПоля название тоже должно быть строго определённо и иметь вид: setНазваниеПоля, тип возвращаемого значения должен быть null. У сеттера должен быть ровно один аргумент типа, совпадающего с типом рассматриваемого поля.

Перепишем класс сотрудника из прошлого примера в усечённом виде:

package com.company;

import java.util.Random;

public class Worker {
// фамилия и имя
private String name;
// возраст
private int age;
// стаж
private int exp;
// продуктивность
private double kpi;

// конструктор по умолчанию
public Worker() {
System.out.println("Конструктор сотрудника по умолчанию");
}

// получить стаж
public int getExp() {
return exp;
}
// задать стаж
public void setExp(int exp) {
this.exp = exp;
}
// получить имя и фамилию
public String getName() {
return name;
}
// задать имя и фамилию
public void setName(String name) {
this.name = name;
}
// получить возраст
public int getAge() {
return age;
}
// задать возраст
public void setAge(int age) {
this.age = age;
}
// получить продуктивность
public double getKpi() {
return kpi;
}
// задать продуктивность
public void setKpi(double kpi) {
this.kpi = kpi;
}

// строковое представление
@Override
public String toString() {
return "Worker{" + name + ": " + exp + "/" + String.format("%.2f", kpi) + "}";
}
}

Теперь из метода main() можно получить любое из закрытых полей объекта сотрудника или задать их значение:

package com.company;

public class Main {

public static void main(String[] args) {
Worker worker = new Worker();
worker.setExp(10);
worker.setName("Павлов Сергей");
worker.setKpi(8.012);
worker.setAge(39);
System.out.println(
worker.getExp() + " " + worker.getAge() + " " +
worker.getKpi() + " " + worker.getName()
);
}
}

На консоль будет выведено:

Конструктор сотрудника по умолчанию
10 39 8.012 Павлов Сергей

При этом если мы теперь попробуем напрямую обратиться к полю, компилятор выдаст нам ошибку:

xor

Задание

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

Прочитайте внимательно, что именно нужно отправить в тот или иной тест.

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

xor

Ссылка на контест