Одно из основных понятий ООП — это наследование. Наследование — естественная абстракция для большинства людей, потому что с наследованием мы сталкиваемся в жизни, и возможность перенести принципы наследования в разработку программного обеспечения позволяет значительно упростить декомпозицию сложных систем и создавать такой уровень абстракции, который будет доступен человеку
Наследование — это отношение между классами, при котором класс использует структуру или поведение другого класса.
Наследование — принцип объектно-ориентированного программирования, в рамках которого возможно описание новых классов на основе уже существующих таким образом, что свойства (в Java — поля) и методы родителя будут присутствовать и в наследниках. Родительский класс в данном случае принято называть суперклассом.
Имеется класс User, описывающий отдельного человека:
class User {
private String name;
public String getName(){
return name;
}
public User(String name){
this.name=name;
}
public void display(){
System.out.println("Name: " + name);
}
}
В дальнейшем мы захотим добавить еще один класс, который описывает сотрудника предприятия - класс Employee. Так как этот класс реализует тот же функционал, что и класс User, поскольку сотрудник - это также и человек, то было бы рационально сделать класс Employee производным (наследником, подклассом) от класса User, который, в свою очередь, называется базовым
классом, родителем или суперклассом:
class Employee extends Person{}
Чтобы объявить один класс наследником от другого, надо использовать после имени класса-наследника ключевое слово extends, после которого идет имя базового класса.
Для класса Employee базовым является User, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе User.
Использование классов:
public class Main {
public static void main(String[] args) {
User alex = new User("Alex");
alex.display();
Employee mike = new Employee("Mike");
mike.display();
}
}
class User {
private String name;
public String getName(){ return name; }
public User (String name){
this.name = name;
}
public void display(){
System.out.println("Name: " + name);
}
}
class Employee extends User {
}
Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private.
При этом производный класс также может добавлять свои поля и методы:
public class Main {
public static void main(String[] args) {
Employee mike = new Employee("Mile", "Yandex");
mike.display();
mike.work();
}
}
class User {
private String name;
public String getName(){ return name; }
public User(String name){
this.name=name;
}
public void display(){
System.out.println("Name: " + name);
}
}
class Employee extends User {
private String company;
public Employee(String name, String company) {
super(name);
this.company=company;
}
public void work(){
System.out.printf("%s works in %s \n", getName(), company);
}
}
В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work().
Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super.
Класс User имеет конструктор, который принимает один параметр. Поэтому в классе Employee в конструкторе нужно вызвать конструктор класса User
После слова super в скобках идет перечисление передаваемых аргументов. Таким образом, установка имени сотрудника делегируется конструктору базового класса.
При этом вызов конструктора базового класса должен идти в самом начале в конструкторе производного класса.
Переопределение методов
Производный класс может определять свои методы, а может переопределять методы, которые унаследованы от базового класса. Например, переопределим в классе Employee метод display():
public class Main {
public static void main(String[] args) {
Employee mike = new Employee("Mike", "Yandex");
mike.display();
}
}
class User {
private String name;
public String getName(){ return name; }
public User(String name){
this.name=name;
}
public void display(){
System.out.println("Name: " + name);
}
}
class Employee extends User {
private String company;
public Employee(String name, String company) {
super(name);
this.company=company;
}
@Override
public void display(){
System.out.printf("Name: %s \n", getName());
System.out.printf("Works in %s \n", company);
}
}
Перед переопределяемым методом указывается аннотация @Override. Данная аннотация говорит компилятору, что мы будем переопределять метод и в принципе необязательна
При переопределении метода он должен иметь уровень доступа не меньше, чем уровень доступа в базовом класса.
Например, если в базовом классе метод имеет модификатор public, то и в производном классе метод должен иметь модификатор public.
Однако в данном случае мы видим, что часть метода display в Employee повторяет действия из метода display базового класса. Поэтому мы можем сократить класс Employee:
class Employee extends User {
private String company;
public Employee(String name, String company) {
super(name);
this.company=company;
}
@Override
public void display(){
super.display();
System.out.printf("Works in %s \n", company);
}
}
С помощью ключевого слова super мы также можем обратиться к реализации методов базового класса.
Запрет наследования
Наследование - эффективный механизм, но в некоторых ситуациях его применение может быть нежелательным. И в этом случае можно запретить наследование с помощью ключевого слова final.
Например:
public final class User {}
Если бы класс User был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:
class Employee extends User {}
Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод display(), запретим его переопределение:
public class User {
//................
public final void display(){
System.out.println("Имя: " + name);
}
}
В этом случае класс Employee не сможет переопределить метод display()
Абстрактные классы
Кроме обычных классов в программе могут содержаться абстрактные классы
Абстрактными называются классы, которые имеют абстрактные методы
Абстрактными называются те методы, которые не имеют реализации в классе. После круглых скобок следует не блок описания метода, а точка с запятой. Перед именем метода указывается при этом ключевое слово abstract.
Абстрактные классы призваны предоставлять базовый функционал для классов-наследников. А производные классы уже реализуют этот функционал
Отличным примером реализации абстрактного класса является система геометрических фигур.
В реальности есть общее понятие геометрической фигуры, но общее представление отсутствует, есть круг, прямоугольник, квадрат, но просто фигуры нет. Однако же и круг, и прямоугольник имеют что-то общее и являются фигурами.
// абстрактный класс фигуры
abstract class Figure{
float x; // x-координата точки
float y; // y-координата точки
Figure(float x, float y){
this.x=x;
this.y=y;
}
// абстрактный метод для получения периметра
public abstract float getPerimeter();
// абстрактный метод для получения площади
public abstract float getArea();
}
// производный класс прямоугольника
class Rectangle extends Figure
{
private float width;
private float height;
// конструктор с обращением к конструктору класса Figure
Rectangle(float x, float y, float width, float height){
super(x,y);
this.width = width;
this.height = height;
}
public float getPerimeter(){
return width * 2 + height * 2;
}
public float getArea(){
return width * height;
}
}