Если контракт задаваемого метода совпадает с контрактом прародительского метода, говорят, что метод переопределён. Если у двух методов имена совпадают, но сигнатуры различны – говорят, что производится перегрузка (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.
Наследование и правила видимости. Зарезервированное слово super
В данном параграфе рассматривается ряд нетривиальных ситуаций, связанных с правилами видимости при наследовании.
Поля и методы, помеченные как private (“закрытый, частный”) наследуются, но в классах-наследниках недоступны. Это сделано в целях обеспечения безопасности. Пусть, например, некий класс Password1 обеспечивает проверку правильности пароля, и у него имеется строковое поле password (“пароль”), в котором держится пароль и с которым сравнивается введённый пользователем пароль. Если оно имеет тип public, такое поле общедоступно, и сохранить его в тайне мы не сможем. При отсутствии модификатора видимости или модификаторе protected на первый взгляд имеется необходимое ограничение доступа. Но если мы напишем класс Password2, являющийся наследником от Password1, в нём легко написать метод, “вскрывающий” пароль:
public String getPass(){
return password;
};
Если же поставить модификатор private, то в потомке до прародительского поля password не добраться!
То, что private-поля наследуются, проверить достаточно просто: зададим класс
public class TestPrivate1 {
private String s=”Значение поля private”;
public String get_s(){
return s;
}
}
и его потомок, который просто имеет другое имя, но больше ничего не делает:
public class TestPrivate2 extends TestPrivate1 {
}
Если из объекта, являющегося экземпляром TestPrivate2, вызвать метод get_s(), мы получим строку =”Значение поля private”:
TestPrivate2 tst=new TestPrivate2();
System.out.println(tst.get_s());
Таким образом, поле s наследуется. Но если в классе, где оно задано, не предусмотрен доступ к нему с помощью каких-либо методов, доступных в наследнике, извлечь информацию из этого поля оказывается невозможным.
Модификатор protected предназначен для использования соответствующих полей и методов разработчиками классов-наследников. Он даёт несколько большую открытость, чем пакетный вид доступа (по умолчанию, без модификатора), поскольку в дополнении к видимости из текущего пакета позволяет обеспечить доступ к таким членам в классах-наследниках, находящихся в других пакетах. Модификатором protected полезно помечать различного рода служебные методы, ненужные пользователям класса, но необходимые для функциональности этого класса.
Существует “правило хорошего тона”: поля данных принято помечать модификатором private, а доступ к этим полям обеспечивать с помощью методов с тем же именем, но префиксом get (“получить” - доступ по чтению) и set (“установить” - доступ по записи). Эти методы называют “геттерами” и “сеттерами”. Такие правила основаны на том, что прямой доступ по записи к полям данных может разрушить целостность объекта.
Рассмотрим следующий пример: пусть у нас имеется фигура, отрисовываемая на экране. Изменение её координат должно сопровождаться отрисовкой на новом месте. Но если мы напрямую изменили поле x или y, фигура останется на прежнем месте, хотя поля имеют новые значения! Если же доступ к полю осуществляется через методы setX и setY, кроме изменения значений полей будут вызваны необходимые методы, обеспечивающие перерисовку фигуры в новом месте. Также можно обеспечить проверку вводимых значений на допустимость.
Возможен и гораздо худший случай доступа к полям напрямую: пусть у нас имеется объект-прямоугольник, у которого заданы поля x1,y1- координаты левого верхнего угла,x2,y2- координаты правого нижнего угла,w - ширина, h – высота, s- площадь прямоугольника. Они не являются независимыми: w=x2-x1, h=y2-y1, s=w*h. Поэтому изменение какого-либо из этих полей должно приводить к изменению других. Если же, скажем, изменить только x2, без изменения w и s, части объекта станут несогласованными. Предсказать, как поведёт себя в таких случаях программа, окажется невозможно!
Ещё хуже обстоит дело при наличии наследования в тех случаях, когда в потомке задано поле с тем же именем, что и в прародителе, имеющее совместимый с прародительским полем тип. Так как для полей данных полиморфизм не работает, возможны очень неприятные ошибки.
Указанные выше правила хорошего тона программирования нашли выражение в среде NetBeans при установленном пакете NetBeans Enterprise Pack. В ней при разработке UML-диаграмм добавление в класс поля автоматически приводит к установке ему модификатора private и созданию двух public-методов с тем же именем, но префиксами get и set. Эти типы видимости в дальнейшем, конечно, можно менять, как и удалять ненужные методы.
Иногда возникает необходимость вызвать поле или метод из прародительского класса. Обычно это бывает в случаях, когда в классе-потомке задано поле с таким же именем (но, обычно, другим типом) или переопределён метод. В результате видимость прародительского поля данных или метода в классе-потомке утеряна. Иногда говорят, что поле или метод затеняются в потомке. В этих случаях используют вызов super.имяПоля или super.имяМетода(список параметров). Слово super в этих случаях означает сокращение от superclass. Если метод или поле заданы не в непосредственном прародителе, а унаследованы от более далёкого прародителя, соответствующие вызовы всё равно будут работать. Но комбинации вида super.super.имя не разрешены.
Использовать вызовы с помощью слова super разрешается только для методов и полей данных объектов. Для методов и переменных класса (то есть объявленных с модификатором static) вызовы с помощью ссылки super запрещены.