Привет! Поговорим об абстрактных классах в Java. Абстрактные классы в Java на конкретных примерах - 1

Почему классы называют «абстрактными»

Ты наверняка помнишь, что такое «абстракция» — мы это уже проходили :) Если вдруг подзабыл — не страшно, вспомним: это принцип ООП, согласно которому при проектировании классов и создании объектов необходимо выделять только главные свойства сущности, и отбрасывать второстепенные. Например, если будем проектировать класс 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 на конкретных примерах - 2 Именно из-за такой «непонятки», когда объекту неясно, какое поведение он должен выбрать, создатели Java отказались от множественного наследования. Впрочем, ты узнаешь, что классы Java реализуют множество интерфейсов.