Композиция и наследование. ЛЕКЦИЯ 9

advertisement
Композиция и наследование.
ЛЕКЦИЯ 9
Одной из наиболее притягательных возможностей языка Java является возможность повторного
использования кода. Но что действительно "революционно", так это наличие возможности выполнять не
только простое копирование и изменение этого кода.
Как и все в Java, решение с повторным использованием кода вертится вокруг классов. Вы повторно
используете код, создавая новый класс, но вместо того, что бы создавать его с нуля Вы используете уже
существующие классы, которые кто-то уже создал и отладил. Т.е. всегда хочется использовать классы без
копания в их исходном коде. Имеются два способа достижения этого.
Первый - почти прямой: Вы просто создаете объекты уже существующих классов внутри нового класса,
который Вы создаете. Это называется "композиция" , потому, что новый класс создается из объектов уже
существующих классов. Вы просто повторно используете функциональность кода, но не его самого.
Второй подход более искусный. Суть его в том, что создается новый класс с типом существующего класса.
Вы буквально берете оболочку (интерфейс) существующего класса и добавляете свой код к нему без
модификации существующего класса. Этот магический акт называется "наследование", и компилятор
языка при этом выполняет большую часть работы.
Наследование является одним из краеугольных камней объектно-ориентированного
программирования и имеет более широкий смысл, который будет раскрыт далее, когда мы будем
рассматривать понятие полиморфизма.
Синтаксис и поведение для обоих способов: для композиции и наследования, идентичны. Это
обусловлено тем, что оба пути создают новые типы из существующих типов.
Синтаксис композиции
Простой иллюстрацией композиции является следующий пример. Пусть в разрабатываемом классе (файле
Target.java) находится следующий код
class Source {
Source() {
System.out.println(“Конструктор Source”);
}
}
class Target {
Source p;
Target() {
System.out.println(“Конструктор Target”);
p = new Source();
// System.out.println("Значение ссылки на объект Source в конструкторе Target = " + p);
}
public static void main(String args[]){
Target x = new Target();
// System.out.println("Значение ссылки на объект Target = " + x);
}
}
Это, по сути, и является композицией, т.е. происходит прямое использование кода класса Source в классе
Target. Более того, если код для класса Source находится вне файла для класса Target (в другом файле
библиотеки и пакета, присоединенных к классу Target с помощью операторов import и/или package), то
также можно говорить о композиции, происходящей опосредованным использованием кода класса Source.
Вывод данной программы будет
Конструктор Target
Конструктор Source
ссылки на объекты инициализируются в null и если Вы попытаетесь использовать их без инициализации, то
Вы получите исключение (JRE сообщит, что получена ошибка времени выполнения). Единственное, что
можно сделать с не инициализированными ссылками на объекты – это их распечатать, например если
добавить в исходный код печать значений, занесенных в ссылки p и x, то получим просто адрес регистра
памяти, в которой данные ссылки размещены.
Конструктор Target
Значение ссылки на объект Source в конструкторе Target = Source@1ddebcd3
Конструктор Source
Значение ссылки на объект Target = Target@a18aa2
Если же Вы желаете инициализировать полностью эти ссылки, Вы можете сделать это такими способами:
1.
В месте, где объект был определен. Это означает, что они будут всегда проинициализированы до
того, как будет вызван конструктор.
2.
В конструкторе класса.
3.
Прямо перед тем моментом, как Вам действительно понадобится использовать этот объект. Этот
способ часто называют "ленивой инициализацией".
При этом может быть уменьшена перегрузка системы в ситуациях, когда объектам нет необходимости быть
созданными все время работы программы.
Если вы тем не менее не проинициализируете объект в точке определения, то при посылке сообщения ему
неизбежно произойдет исключение.
Синтаксис наследования
Наследование является неотъемлемой частью Java, впрочем, как и других ОО языков программирования.
Это очевидно - Вы всегда осуществляете операцию наследования, когда создаете класс, даже если ваш класс
не является наследником какого либо другого, потому, что Вы неявно наследуете стандартный корневой
класс Java Object.
Синтаксис наследования похож на композицию, но процедура выполнения заметно отличается. Когда Вы
наследуете, Вы "говорите": "Этот класс такой же, как тот старый класс!" Вы излагаете эту фразу в коде
давая классу имя, как обычно, но до того, как начнете работать с телом класса, добавляете ключевое слово
extends следующее до имени базового класса. Когда вы сделаете это, вы автоматически получите все поля
данных и методы базового класса.
Классы-потомки имеют возможность не только создавать свои собственные переменные и методы, но и
наследовать переменные и методы классов-предков. Классы-потомки принято называть подклассами.
Непосредственного предка данного класса называют его суперклассом. Если базовый класс Point был
определен следующим образом
class Point { int х, у;
Point(int х, int у) {
this.x = х;
this.у = у;
}}
то мы можем его расширить, чтобы включить в него третью координату z.
class Point3D extends Point { int z;
Point3D(int x, int y, int z) {
this.x = x;
this.у = у;
this.z = z; }
}}
В этом примере ключевое слово extends используется для того, чтобы сообщить транслятору о намерении
создать подкласс класса Point. Как видите, в этом классе не понадобилось объявлять переменные х и у,
поскольку Point3D унаследовал их от своего суперкласса Point.
super
В примере с классом Point3D частично повторялся код, уже имевшийся в суперклассе. Ключевое слово
super позволяет обратиться непосредственно к конструктору суперкласса и тем самым сократить код
программы.
class Point3D extends Point { int z;
Point3D(int x, int у, int z) {
super(x, y); // Здесь мы вызываем конструктор суперкласса
this.z=z;
public static void main(String
args[]) {
Point3D p = new Point3D(10, 20, 30);
System.out.println( " x = " +
p.x + " y = " + p.y +
" z = " + p.z);
}}
Вот результат работы этой программы:
С:\> java Point3D
x = 10 у = 20 z = 30
Разберем более развернутый пример:
//: c06:Detergent.java
// Свойства и синтаксис наследования.
class Cleanser {
private String s = new String("Супер-класс Cleanser");
public void append(String a) { s += a; }
public void apply() { append(" \nМетод apply()"); }
public void scrub() { append(" \nМетод scrub()"); }
public void print() { System.out.println(s); }
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.apply();
x.scrub();
x.print();
}
}
public class Detergent extends Cleanser {
// Изменяем метод:
public void scrub() {
append(" \nПерегруженный в дочернем классе ");
super.scrub(); // Вызываем метод базового класса
}
// Все методы наследования:
public void foam() { append(" \nМетод дочернего класса foam()"); }
// Проверяем новый класс:
public static void main(String[] args) {
Detergent x = new Detergent();
System.out.println("Тестируем дочерний класс:");
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Тестируем базовый класс:");
Cleanser.main(args);
}
}
Этот пример показывает несколько возможностей. Сперва в методе Cleanser append( ) , String-и
конкатенируются с s при помощи оператора "+=", это один из операторов (с плюсом впереди), который
перегружается Java для работы с типом String.
Во-вторых, оба Cleanser и Detergent содержат метод main( ). Вы можете создать main( ) для каждого из
ваших классов и часто рекомендуется писать такой код для тестирования каждого из классов. Если же у Вас
имеется множество классов в программе, то выполнится только метод main( ) того класса, который был
вызван из командной стоки. Так что в этом случае, когда вы вызовите java Detergent, будет вызван метод
Detergent.main( ) . Но так же вы можете вызвать java Cleanser для выполнения Cleanser.main( ), несмотря
даже на то, что класс Cleanser не public . Эта техника помещения метода main( ) в каждый класс позволяет
легко проверять каждый из классов программы по отдельности. И Вам нет необходимости удалять main( )
когда вы закончили проверки, Вы можете оставить его для будущих проверок.
Здесь Вы можете видеть, что Detergent.main( ) явно вызывает Cleanser.main( ) , передавая ему те же самые
аргументы из командной строки(тем не менее, Вы могли были передать ему любой , массив элементов типа
String).
Важно то, что все методы в Cleanser - public. Помните, если Вы оставите любой из спецификаторов доступа
в состоянии по умолчанию, т.е. он будет friendly, то доступ к нему могут получить только члены этого же
пакета. Поэтому в этом пакете все могут использовать эти методы, если у них нет спецификатора доступа.
Detergent эти проблемы не имеет, к примеру. Но в любом случае, если класс из другого пакета попытается
наследовать Cleanser он получит доступ только к членам со спецификатором public. Так что если Вы
планируете использовать наследование, то в качестве главного правила делайте все поля private и все
методы public. (protected так же могут получить доступ к наследуемым классам, но Вы узнаете об этом
позже.)
Заметьте, что Cleanser имеет набор методов из родительского интерфейса: append( ), apply( ), scrub( ), и
print( ). Из-за того, что Detergent произошел от Cleanser (при помощи ключевого слова extends ) он
автоматически получил все те методы, что есть в его интерфейсе, даже не смотря на то, что вы не видите их
среди определенных в Detergent. Вы можете подумать о наследовании, а уже только затем о повторном
использовании интерфейса.
Как видно, в коде класса Detergent (метод scrub( )) , возможно взять метод, который определяется в
базовом классе, а затем уже его модифицировать (оставив неизменным имя). Т.е сделать замещение
(перегрузку) метода базового класса. Однако внутри дочернего scrub( ) вы не можете просто вызвать
базовый scrub( ), поскольку эта операция вызовет рекурсивный вызов (одно и то же имя). Для разрешения
этой проблемы в Java используется ключевое слово super , которое ссылается на метод суперкласса.
Поэтому выражение super.scrub( ) вызывает метод базового класса scrub( ).
При наследовании вы не ограничены в использовании методов только базового класса. Вы можете так же
добавлять новые методы в новый дочерний класс. Это сделать очень просто, нужно просто определить их.
Метод foam( ) тому демонстрация.
В Detergent.main( ) вы можете увидеть, что у объекта Detergent Вы можете вызвать все методы, которые
доступны в Cleanser так же, как и в Detergent (в том числе и foam( )).
Выводом программы, после запуска java Detergent будет
Тестируем дочерний класс:
Супер-класс Cleanser
Метод apply()
Перегруженный в дочернем классе
Метод scrub()
Метод дочернего класса foam()
Тестируем базовый класс:
Супер-класс Cleanser
Метод apply()
Метод scrub()
Инициализация базового класса
До этого у нас было запутано два класса - базовый и произошедший от него вместо просто одного, это
может привести в небольшое замешательство при попытке представить результирующий объект
произведенный произошедшим (дочерним) классом. С наружи он выглядит, как новый класс имеющий тот
же интерфейс, что и базовый класс и может иметь те же самые методы и поля. Но наследование не просто
копирует интерфейс базового класса. Когда Вы создаете объект произошедшего (дочернего) класса он
содержит еще и подобъект базового класса. Этот подобъект точно такой же, как если бы вы создали объект
базового класса самостоятельно.
Естественно, что необходимо правильно и корректно проинициализировать этот подобъект и здесь есть
только один гарантированный путь: осуществить инициализацию в конструкторе, путем вызова
конструктора базового класса, который имеет все необходимые сведения и привилегии для осуществления
инициализации самого базового класса. Java автоматически вставляет вызов базового класса в конструктор
произошедшего (наследуемого) от этого класса. Следующий пример демонстрирует эту работу с трети
уровнем наследования:
//: c06:Cartoon.java
// Конструктор вызывается на стадии инициализации.
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
} ///:~
Вывод этой программы показывает автоматические вызовы:
Art constructor
Drawing constructor
Cartoon constructor
Как Вы можете видеть конструктор базового класса проинициализировал его до того, как к нему смог
получить доступ произошедший от него класс.
Даже, если Вы не создаете конструктор для Cartoon( ), компилятор синтезирует конструктор по умолчанию
для вызова конструктора базового класса.
Конструктор с аргументами
Предыдущий пример имеет конструктор по умолчанию; и при этом он не имеет каких либо аргументов. Для
компилятора такой вызов прост, нет ненужных вопросов по поводу аргументов, которые нужно передать.
Если Ваш класс не имеет аргументов по умолчанию или если Вы хотите вызвать конструктор базового
класса, который имеет аргументы, Вы должны просто использовать ключевое слово super и передать ему
список аргументов:
//: c06:Chess.java
// Наследование, конструкторы и аргументы.
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
} ///:~
Если же Вы не вызовите конструктор базового класса в BoardGame( ), тогда компилятор выдаст сообщение,
что он не может найти конструктор для Game( ). В дополнение к вышесказанному - вызов конструктора
базового класса должен быть осуществлен в первую очередь в конструкторе класса наследника.
(Компилятор сообщит Вам об этом, если Вы сделали что-то не так.)
Обработка исключений конструктора
Как только что было замечено, компилятор предлагает Вам поместить конструктор базового класса в
конструктор класса наследника. Это означает, что ничего другого не может произойти до его вызова.
Download