Продолжаем тему
наследования и на этом занятии поговорим об абстрактных классах. Абстрактные
классы есть во многих языках программирования, в том числе и в Java. Они позволяют
описывать поля, методы, но не требуют их конкретизации и реализации. Например, мы
описываем класс автомобиль (Car), но пока незнаем как будут реализованы
его методы. В этом случае можно объявить его как абстрактный и просто прописать
переменные и методы без конкретного наполнения:
abstract class Car {
String model;
public abstract void go();
public abstract void stop();
public abstract void draw();
}
Смотрите, мы
здесь определили одно поле model и три абстрактных метода: go, stop и draw. Абстракция в
данном случае означает, что мы знаем что хотим от автомобиля, но пока незнаем
как это будем делать. Своего рода, это некий набросок – абстракция, причем,
абстракция на уровне класса и методов.
В
действительности, если класс содержит хотя бы один абстрактный метод, то он
должен быть объявлен как абстрактный. И, если мы попытаемся убрать ключевое
слово abstract в определении
класса, то возникнет ошибка. С другой стороны, если бы данный класс не содержал
бы ни одного абстрактного метода, например, вот так:
abstract class Car {
String model;
public void go() {}
public void stop() {}
public void draw() {}
}
То ключевое
слово abstract можно как использовать,
так и не использовать. Тогда в чем отличие этого абстрактного класса Car от такого же,
но не абстрактного? В действительности, только одним: для абстрактных классов
нельзя создавать экземпляры, то есть, вот такая строчка приведет к ошибке:
Но, если убрать
слово abstract, то ошибки уже
не будет. Конечно, это скорее искусственный пример, показывающий отличие в
определении абстрактного и не абстрактного классов. В реальности, абстрактные
классы, как правило, содержат абстрактные методы, как мы это сделали вначале. Но
наряду с абстрактными методами можно записывать и обычные, например, так:
abstract class Car {
String model;
public abstract void go();
public abstract void stop();
public abstract void draw();
public void setModel(String model) {
this.model = model;
}
}
Здесь
используется сеттер для задания поля model.
Ну, хорошо, мы
можем объявлять абстрактные классы и методы, но зачем они нужны, если нельзя ни
создавать объекты такого класса, ни вызывать такие методы? Все верно. И такие
классы создаются исключительно для дальнейшего наследования и конкретизации их
работы уже в дочерних классах. А абстракция позволяет сразу описать необходимые
интерфейсы (то есть, методы), через которые в дальнейшем будут вызываться соответствующие
переопределенные методы дочерних классов. Это часто очень удобно при реализации
больших проектов, когда в целом определены информационные потоки через
абстрактные классы и интерфейсы, а далее, создаются производные классы для их
конкретного наполнения, реализации.
Итак, давайте
тоже наполним конкретикой наш абстрактный класс Car. Для этого
определим дочерний класс и назовем его, например, ToyotaCorolla. Если написать
вот такие строчки:
class ToyotaCorolla extends Car {
}
то
интегрированная среда нам сразу укажет на ошибку. Дело в том, что в нашем
абстрактном классе определены три виртуальных метода, поэтому мы обязаны их
здесь переопределить:
class ToyotaCorolla extends Car {
public void go() {
System.out.println("Toyota Corolla едет");
}
public void stop() {
System.out.println("Toyota Corolla остановилась");
}
public void draw() {
System.out.println("Рисование машины Toyota Corolla");
}
}
Теперь никаких
ошибок не будет и мы можем создать экземпляр этого класса:
ToyotaCorolla car1 = new ToyotaCorolla();
Или, используя
обобщенный тип ссылок на абстрактный класс:
Car car1 = new ToyotaCorolla();
И, далее, мы
можем вызывать методы go, stop и draw, определенные в
дочернем классе:
car1.go();
car1.stop();
car1.draw();
Давайте для
примера добавим еще один дочерний класс ToyotaCamry:
class ToyotaCamry extends Car {
public void go() {
System.out.println("Toyota Camry едет");
}
public void stop() {
System.out.println("Toyota Camry остановилась");
}
public void draw() {
System.out.println("Рисование машины Toyota Camry");
}
}
Определим массив
обобщенных ссылок в функции main:
final int N = 4;
Car cars[] = new Car[N];
Присвоим им экземпляры
дочерних классов:
cars[0] = new ToyotaCorolla();
cars[1] = new ToyotaCamry();
cars[2] = new ToyotaCorolla();
cars[3] = new ToyotaCamry();
И вызовем общие
методы базового класса Car, реализованные в дочерних классах:
for(int i = 0;i < N; ++i) {
cars[i].go();
cars[i].stop();
cars[i].draw();
}
Видите,
благодаря тому, что в базовом классе прописаны виртуальные методы go, stop и draw, мы имеем
возможность вызывать их, используя единый интерфейс – ссылки на экземпляры
базового класса Car. Это еще один пример полиморфизма в ООП.
Но здесь у вас
может возникнуть вопрос: зачем городить огород, придумывать абстрактный класс Car, когда все то
же самое можно было сделать и с обычным классом Car. Да, все верно,
например, можно прописать базовый класс в виде:
class Car {
String model;
public void go() {}
public void stop() {}
public void draw() {}
public void setModel(String model) {
this.model = model;
}
}
И все тоже
работало бы. Но в такой реализации есть принципиальные отличия от предыдущей (с
абстрактным классом). Во-первых, здесь нам приходится прописывать пустые
реализации у методов и такие методы можно уже не переопределять в дочерних
классах. Абстрактные же методы обязаны быть определены в производных классах и
это, с одной стороны, гарантирует их требуемое наполнение, а с другой, помогает
избежать ошибок, если программист забудет их определить.
Вторым важным
моментом отличия абстрактных классов от обычных – невозможность создания их
экземпляров. Это позволяет дополнительно защитить программу от ее
нежелательного использования, когда класс должен браться исключительно в
качестве базового и не разрешать создавать свои экземпляры.
Вот эти моменты
обусловили появление абстрактных классов и методов в языке Java.
Путь кодера
Подвиг 1. Объявите
абстрактный класс Geom для представления геометрических фигур с
полями: width, color для определения
толщины и цвета линии, а также с абстрактным методом draw() для рисования
конкретного графического примитива. Затем, запишите дочерние классы Line, Rect, Ellipse для
представления линий, прямоугольников и эллипсов. Определите в них поля для
хранения координат этих фигур и метод draw() для их
рисования. Создайте обобщенные ссылки Geom на объекты
дочерних классов и вызовите у них метод draw().
Подвиг 2. Объявите
абстрактный класс Recipes (рецепты) с полями: название, тип
(вегетарианский/обычный). И абстрактными методами: showIngredients (показать
ингредиенты), showRecipe (показать
рецепт). Описать несколько дочерних классов: Salad (для салатов), Pizza (для пицц), Porridge (для
каш). В каждом дочернем классе определить поле для списка ингредиентов (в виде
строки) и описания самого рецепта (в виде строки). А также реализовать
абстрактные методы базового класса Recipes. Создать
несколько экземпляров дочерних классов и через общий интерфейс (в виде ссылок
типа Recipes) вызвать методы
showRecipe и showIngredients.