logo
Методичка Java

Наследование. Суперклассы и подклассы. Переопределение методов

В объектном программировании принято использовать имеющиеся классы в качестве “заготовок” для создания новых классов, которые на них похожи, но обладают более сложной структурой и/или отличающимся поведением. Такие “заготовки” называются прародителями (ancestors), а основанные на них новые классы – потомками (descendants) или наследниками. Классы-потомки получают “в пользование” поля и методы, заданные в классах-прародителях, это называется наследованием (inheritance) полей и методов.

В C++ и Java вместо терминов “прародители” и “потомки” чаще используют неудачные названия “суперклассы” (superclasses) и “подклассы” (subclasses). Как уже говорилось, суперклассы должны быть примитивнее подклассов, но приставка “супер” подталкивает программиста к прямо противоположным действиям.

При задании класса-потомка сначала идут модификаторы, затем после ключевого слова class идёт имя декларируемого класса, затем идёт зарезервированное слово extends (“расширяет”), после чего требуется указать имя класса-родителя (непосредственного прародителя). Если не указывается, от какого класса идёт наследование, родителем считается класс Object. Сам класс-потомок называется наследником, или дочерним.

В синтаксисе Java словом extends подчёркивается, что потомок расширяет то, что задано в прародителе – добавляет новые поля, методы, усложняет поведение. (Но всё это делает класс более специализированным, менее общим).

Далее в фигурных скобках идёт реализация класса – описание его полей и методов. При этом поля данных и методы, имеющиеся в прародителе, в потомке описывать не надо – они наследуются. Однако в случае, если реализация прародительского метода нас не устраивает, в классе-потомке его можно реализовать по другому. В этом случае метод необходимо продекларировать и реализовать в классе-потомке. Кроме того, в потомке можно задавать новые поля данных и методы, отсутствующие в прародителях.

Модификаторы, которые можно использовать:

Таким образом, задание класса-наследника имеет следующий формат:

Модификаторы class ИмяКласса extends ИмяРодителя {

Задание полей;

Задание подпрограмм - методов класса, методов объекта, конструкторов

}

Данный формат относится к классам, не реализующим интерфейсы (interfaces). Работе с интерфейсами будет посвящён отдельный раздел.

Рассмотрим в качестве примера наследование для классов описанной ранее иерархии фигур. Для простоты выберем вариант, в котором Figure - это класс-прародитель иерархии, Dot его потомок, а Circle - потомок Dot (то есть является “жирной точкой”). Напомним, что имена классов принято начинать с заглавной буквы.

Класс Figure опишем как абстрактный – объектов такого типа создавать не предполагается, так как фигура без указания конкретного вида – это, действительно, чистая абстракция. По той же причине методы show (“показать”) и hide (“скрыть”) объявлены как абстрактные. Напомним также, что если в классе хоть один метод является абстрактным, это класс обязан быть объявлен как абстрактный.

public abstract class Figure { //это абстрактный класс

int x=0;

int y=0;

java.awt.Color color;

java.awt.Graphics graphics;

java.awt.Color bgColor;

public abstract void show(); //это абстрактный метод

public abstract void hide(); //это абстрактный метод

public void moveTo(int x, int y){

hide();

this.x= x;

this.y= y;

show();

};

}

Поля x и y задают координаты фигуры, а color – её цвет. Соответствующий тип задан в пакете java.awt. Поле graphics задаёт ссылку на графическую поверхность, по которой будет идти отрисовка фигуры. Соответствующий тип также задан в пакете java.awt. В отличии от полей x, y и color для этого поля при написании класса невозможно задать начальное значение, и оно будет присвоено при создании объекта. То же относится к полю bgColor (от “background color”) – в нём мы будем хранить ссылку на цвет фона графической поверхности. Цветом фона мы будем выводить фигуру в методе hide для того, чтобы она перестала показываться на экране. Это не самый лучший, но зато самый простой способ скрыть фигуру. В дальнейшем при желании реализацию метода можно изменить – это никак не коснётся остальных частей программы. В параграфе, посвящённом конструкторам, в классе FilledCircle мы применим более совершенный способ отрисовки и “скрывания” фигур, основанный на использовании режима рисования XOR (“исключающее или”). Установка этого режима производится методом setXORMode. Такой режим можно использовать для всех наших фигур.

Метод moveTo имеет реализацию несмотря на то, что класс абстрактный, и в этой реализации используются имена абстрактных методов show и hide. Этот вопрос будет подробно обсуждаться в следующем параграфе, посвящённом полиморфизму.

Рассмотрим теперь, как задаётся потомок класса Figure – класс Dot (“Точка”). Для Dot классы Object и Figure будут являться прародителями (суперклассами), причёт Figure будет непосредственным прародителем. Соответственно, для них класс Dot будет являться потомком (подклассом), причём для класса Figure – непосредственным потомком. Класс Dot расширяет (extends) функциональность класса Figure: хотя в нём и не появляется новых полей, зато пишется реализация для методов show и hide, которые в прародительском классе были абстрактными. В классе Figure мы использовали классы пакета java.awt без импорта этого пакета. В классе Dot используется импорт – обычно это удобнее, так как не надо много раз писать длинные имена.

package java_gui_example;

import java.awt.*;

/**

* @author В.В.Монахов

*/

public class Dot extends Figure{

/** Создаёт новый экземпляр типа Dot */

public Dot(Graphics graphics,Color bgColor) {

this.graphics=graphics;

this.bgColor=bgColor;

}

public void show(){

Color oldC=graphics.getColor();

graphics.setColor(Color.BLACK);

graphics.drawLine(x,y,x,y);

graphics.setColor(oldC);

}

public void hide(){

Color oldC=graphics.getColor();

graphics.setColor(bgColor);

graphics.drawLine(x,y,x,y);

graphics.setColor(oldC);

;

}

}

Отметим, что в классе Dot не задаются поля x, y, graphics и метод moveTo – они наследуются из класса Figure. А методы show и hide переопределяются (override) – для них пишется реализация, соответствующая тому, каким именно образом точка появляется и скрывается на экране.

Конструктор Dot(int x, int y, Graphics g) занимается созданием объекта типа Dot и инициализацией его полей. В методах show и hide используются методы объекта graphics. В методе show сначала во временной переменной oldC сохраняется информация о текущем цвете рисования. Затем в качестве текущего цвета устанавливается цёрный цвет (константа java.awt. Color.BLACK). Затем вызывается метод, рисующий точку, в качестве него используется рисование линии с совпадающими началом и концом. После чего восстанавливается первоначальный цвет рисования. Это необходимо для того, чтобы не повлиять на поведение других объектов, пользующихся для каких-либо целей текущим цветом. Такого рода действия являются очень характерными при пользовании разделяемыми (shared) ресурсами. Если вам при работе какого-либо метода требуется изменить состояние разделяемых внешних данных, сначала требуется сохранить информацию о текущем состоянии, а в конце вызова восстановить это состояние.

Термин override (“переопределить”) на русский язык часто переводят как “перекрыть”. Это может вводить в заблуждение, так как имеется ещё одно понятие – перекрытие области видимости (hiding). Такое перекрытие возникает в случае, когда в классе-потомке задаётся поле с тем же именем, что и в прародителе (но, возможно, другого типа). Для методов совпадение имён разрешено, в том числе с именами глобальных и локальных переменных.

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

Если контракт задаваемого метода совпадает с контрактом прародительского метода, говорят, что метод переопределён. Если у двух методов имена совпадают, но сигнатуры различны – говорят, что производится перегрузка (overloading) методов. Перегрузке методов далее будет посвящён отдельный параграф. Если же в одном классе два метода имеют одинаковые сигнатуры, то даже если их контракты отличаются, компилятор выдаёт сообщение об ошибке.

В классе нашего приложения создадим на экранной форме панель, и будем вести отрисовку по ней. Зададим с помощью редактора свойств белый (или какой-нибудь другой) цвет панели – свойство background. Затем зададим переменную dot, которой назначим объект в обработчике нажатия на кнопку:

Dot dot=new Dot(jPanel1.getGraphics(),jPanel1.getBackground());

После создания объекта-точки с помощью переменной dot можно вызывать методы show и hide:

dot.show();

dot.hide();

Создадим на форме пункты ввода/редактирования текста jTextField1 и jTextField2. В этом случае становится можно вызывать метод moveTo, следующим образом задавая координаты, куда должна перемещаться точка:

int newX=Integer.parseInt(jTextField1.getText());

int newY=Integer.parseInt(jTextField2.getText());

dot.moveTo(newX,newY);

Наш пример оказывается достаточно функциональным для того, чтобы увидеть работу с простейшим объектом.

Рассмотрим теперь класс ScalableFigure (“Масштабируемая фигура”), расширяющий класс Figure. Он очень прост.

package java_gui_example;

public abstract class ScalableFigure extends Figure{

int size;

public void resize(int size) {

hide();

this.size=size;

show();

}

}

!!!Класс ScalableFigure является абстрактным – объектов такого типа создавать не предполагается, так как масштабируемая фигура без указания конкретного вида – это абстракция. По этой же причине в классе не заданы реализации методов show и hide.

Зато появилось поле size (“размер”), и метод resize (“изменить размер”), расширяющий этот класс по сравнению с прародителем. Для того, чтобы изменить размер фигуры, отрисовываемой на экране, надо не только присвоить полю size новое значение, но и правильно перерисовать фигуру. Сначала надо её скрыть, затем изменить значение size, после чего показать на экране – уже нового размера. Следует обратить внимание, что мы пишем данный код на уровне абстракций, для нас не имеет значения, какого типа будет фигура – главное, чтобы она была масштабируемая, то есть являлась экземпляром класса-потомка ScalableFigure. О механизме, позволяющем такому коду правильно работать, будет рассказано далее в параграфе, посвящённом полиморфизму.

Опишем класс Circle (“Окружность”), расширяющий класс ScalableFigure.

package java_gui_example;

import java.awt.*;

public class Circle extends ScalableFigure {

Circle(Graphics g,Color bgColor, int r){ //это конструктор

graphics=g;

this.bgColor=bgColor;

size=r;

}

public void show(){

Color oldC=graphics.getColor();

graphics.setColor(Color.BLACK);

graphics.drawOval(x,y,size,size);

graphics.setColor(oldC);

}

public void hide(){

Color oldC=graphics.getColor();

graphics.setColor(bgColor);

graphics.drawOval(x,y,size,size);

graphics.setColor(oldC);

}

};

В классе Circle не задаётся новых полей – в качестве радиуса окружности используется поле size, унаследованное от класса ScalableFigure. Зато введён конструктор, позволяющий задавать радиус при создании окружности.

Кроме того, написаны новые реализации для методов show и hide, поскольку окружность показывается, скрывается и движется по экрану не так, как точка.

Таким образом, усложнение структуры Circle по сравнением со ScalableFigure в основном связано с появлением реализации у методов, которые до этого были абстрактными. Очевидно, класс Circle является более специализированным по сравнению со ScalableFigure, не говоря уж о Figure.

Поля x, y, color, bgColor, graphics и метод moveTo наследуется в Circle из класса Figure. А из ScalableFigure наследуются поле size и метод resize.

Следует особо подчеркнуть, что наследование относится к классам, а не к объектам. Можно говорить, что один класс является наследником другого. Но категорически нельзя – что один объект является наследником другого объекта. Иногда говорят фразы вроде “объект circle является наследником Figure ”. Это не страшно, если подразумевается, что “объект circle является экземпляром класса-наследника Figure”. Слишком долго произносить правильную фразу. Но следует чётко понимать, что имеется в виду, и злоупотреблять такими оборотами не следует.

Класс Circle является непосредственным (прямым) потомком ScalableFigure , а ScalableFigure – непосредственным (прямым) прародителем класса Circle . То есть для ScalableFigure класс Circle является подклассом, а для Circle класс ScalableFigure является суперклассом. Аналогично, для Figure подклассами являются и ScalableFigure, и Circle. А для Circle суперклассами являются и ScalableFigure, и Figure.

Поскольку в Java все классы— потомки класса Object, то Object является прародителем и для Figure, и для ScalableFigure, и для Circle. Но непосредственным прародителем он будет только для Figure.