Привет! Поговорим об абстрактных классах в Java.
Почему классы называют «абстрактными»
Ты наверняка помнишь, что такое «абстракция» — мы это уже проходили :) Если вдруг подзабыл — не страшно, вспомним: это принцип ООП, согласно которому при проектировании классов и создании объектов необходимо выделять только главные свойства сущности, и отбрасывать второстепенные. Например, если будем проектировать классSchoolTeacher
— школьный учитель — вряд ли понадобится характеристика «рост». Действительно: для преподавателя эта характеристика не важна. Но вот если мы будем создавать в программе класс BasketballPlayer
— игрок в баскетбол — рост станет одной из главных характеристик.
Так вот, абстрактный класс — это максимально абстрактная, о-о-о-чень приблизительная «заготовка» для группы будущих классов. Эту заготовку нельзя использовать в готовом виде — слишком «сырая». Но она описывает некое общее состояние и поведение, которым будут обладать будущие классы — наследники абстрактного класса.
Примеры абстрактных классов Java
Рассмотрим простой пример с машинами:
public abstract class Car {
private String model;
private String color;
private int maxSpeed;
public abstract void gas();
public abstract void brake();
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
}
Вот так выглядит самый простой абстрактный класс. Как видишь, ничего особенного :)
Для чего он может нам понадобиться?
Прежде всего, он максимально абстрактно описывает нужную нам сущность — автомобиль. Слово abstract находится здесь недаром. В мире не существует «просто машин». Есть грузовики, гоночные автомобили, седаны, купе, внедорожники.
Наш абстрактный класс — это просто «чертеж», по которому мы позже будем создавать классы-автомобили.
public class Sedan extends Car {
@Override
public void gas() {
System.out.println("Седан газует!");
}
@Override
public void brake() {
System.out.println("Седан тормозит!");
}
}
Это во многом похоже на то, о чем мы говорили в лекциях про наследование. Только там у нас класс Car
и его методы не были абстрактными. Но у такого решения есть целый ряд минусов, которые в абстрактных классах исправлены.
Первое и главное — экземпляр абстрактного класса создать нельзя:
public class Main {
public static void main(String[] args) {
Car car = new Car(); // Ошибка! Класс Car является абстрактным!
}
}
Эта «фишка» была реализована создателями Java специально. Еще раз, для запоминания: абстрактный класс — это просто чертеж для будущих «нормальных» классов. Тебе же не нужны экземпляры чертежа, правильно? Вот и экземпляры абстрактного класса создавать не надо :)
А если бы класс Car
не был абстрактным, мы легко могли бы создавать его объекты:
public class Car {
private String model;
private String color;
private int maxSpeed;
public void gas() {
// какая-то логика
}
public void brake() {
// какая-то логика
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car(); // Все ок, машина создалась
}
}
Теперь у нас в программе появилась какая-то непонятная машина — не грузовик, не гоночная, не седан, а вообще непонятно что. Та самая «просто машина», которых в природе не существует.
Тот же пример можно привести с животными. Представь, если бы в твоей программе появились объекты Animal
— «просто животное». Какого оно вида, к какому семейству относится, какие у него характеристики — непонятно. Было бы странно увидеть его в программе. Никаких «просто животных» в природе не существует. Только собаки, кошки, лисы, кроты и другие.
Абстрактные классы избавляют нас от «просто объектов». Они дают нам базовое состояние и поведение. Например, у всех машин должна быть модель, цвет и максимальная скорость, а еще они должны уметь газовать и тормозить. Вот и все. Это — общая абстрактная схема, дальше ты уже сам проектируешь нужные тебе классы.
Обрати внимание: два метода в абстрактном классе тоже обозначены как abstract, и они вообще не реализованы. Причина та же: абстрактные классы не создают «поведения по умолчанию» для «просто машин». Они просто говорят, что должны уметь делать все машины.
Впрочем, если поведение по умолчанию тебе все-таки нужно, методы в абстрактном классе можно реализовать. Java этого не запрещает:
public abstract class Car {
private String model;
private String color;
private int maxSpeed;
public void gas() {
System.out.println("Газуем!");
}
public abstract void brake();
//геттеры и сеттеры
}
public class Sedan extends Car {
@Override
public void brake() {
System.out.println("Седан тормозит!");
}
}
public class Main {
public static void main(String[] args) {
Sedan sedan = new Sedan();
sedan.gas();
}
}
Вывод в консоль:
“Газуем!”
Как видишь, мы реализовали в абстрактном классе один метод, а второй не стали.
В итоге поведение нашего класса Sedan
разделилось на две части: если вызвать у него метод gas()
, он «подтянется» из родительского абстрактного класса Car
, а метод brake()
мы переопределили в классе Sedan
. Получилось очень удобно и гибко.
Но теперь наш класс не такой уж и абстрактный? Ведь у него, по факту, половина методов реализована.
На самом деле — и это очень важная особенность — класс является абстрактным, если хотя бы один из его методов является абстрактным. Хоть один из двух, хоть один из тысячи методов — без разницы.
Мы можем даже реализовать все методы и не оставить ни одного абстрактного. Будет абстрактный класс без абстрактных методов. В принципе, это возможно, и компилятор не выдаст ошибок, но лучше так не делать: слово abstract потеряет смысл, а твои коллеги-программисты сильно удивятся, увидев такое :/
При этом, если метод помечен словом abstract, каждый класс-наследник должен его реализовать или быть объявленным как абстрактный. Иначе компилятор выбросит ошибку.
Разумеется, каждый класс может наследоваться только от одного абстрактного класса, так что в плане наследования разницы между абстрактными и обычными классами нет. Неважно, наследуемся мы от абстрактного класса или от обычного, класс-родитель может быть только один.
Почему в Java нет множественного наследования классов
Мы уже говорили, что в Java нет множественного наследования, но так толком и не разобрались почему. Давай попробуем сделать это сейчас. Дело в том, что если бы в Java было множественное наследование, дочерние классы не могли бы определиться, какое именно поведение им выбрать. Допустим, у нас есть два класса —Toster
и NuclearBomb
:
public class Toster {
public void on() {
System.out.println("Тостер включен, тост готовится!");
}
public void off() {
System.out.println("Тостер выключен!");
}
}
public class NuclearBomb {
public void on() {
System.out.println("Взрыв!");
}
}
Как видишь, у обоих есть метод on()
. В случае с тостером он запускает приготовление тоста, а в случае с ядерной бомбой — устраивает взрыв.
Ой :/
А теперь представь, что ты решил (уж не знаю, с чего вдруг!) создать что-то среднее между ними. И вот он твой класс — MysteriousDevice
!
Этот код, разумеется, нерабочий, и мы приводим его просто в качестве примера «а как оно могло бы быть»:
public class MysteriousDevice extends Toster, NuclearBomb {
public static void main(String[] args) {
MysteriousDevice mysteriousDevice = new MysteriousDevice();
mysteriousDevice.on(); // И что же здесь должно произойти? Мы получим тост, или ядерный апокалипсис?
}
}
Давай посмотрим, что у нас получилось. Загадочное устройство происходит одновременно и от Тостера, и от Ядерной Бомбы. У обоих есть метод on()
, и в результате непонятно, какой из методов on()
должен срабатывать у объекта MysteriousDevice
, если мы его вызовем. Объект никак не сможет этого понять.
Ну и в качестве вишенки на торте: у Ядерной Бомбы нет метода off()
, так что если мы не угадали, отключить устройство будет нельзя.
Именно из-за такой «непонятки», когда объекту неясно, какое поведение он должен выбрать, создатели Java отказались от множественного наследования. Впрочем, ты узнаешь, что классы Java реализуют множество интерфейсов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ