русс | укр

Языки программирования

ПаскальСиАссемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование

Все о программировании


Linux Unix Алгоритмические языки Аналоговые и гибридные вычислительные устройства Архитектура микроконтроллеров Введение в разработку распределенных информационных систем Введение в численные методы Дискретная математика Информационное обслуживание пользователей Информация и моделирование в управлении производством Компьютерная графика Математическое и компьютерное моделирование Моделирование Нейрокомпьютеры Проектирование программ диагностики компьютерных систем и сетей Проектирование системных программ Системы счисления Теория статистики Теория оптимизации Уроки AutoCAD 3D Уроки базы данных Access Уроки Orcad Цифровые автоматы Шпаргалки по компьютеру Шпаргалки по программированию Экспертные системы Элементы теории информации

Глава 7 ПОВТОРНОЕ ИСПОЛЬЗОВАНИЕ КЛАССОВ


Дата добавления: 2015-06-12; просмотров: 772; Нарушение авторских прав


Содержание · 1 ПОВТОРНОЕ ИСПОЛЬЗОВАНИЕ КЛАССОВ o 1.1 Синтаксис композиции o 1.2 Синтаксис наследования o 1.3 Инициализация базового класса o 1.4 Конструкторы с аргументами o 1.5 Делегирование o 1.6 Сочетание композиции и наследования o 1.7 Обеспечение правильного завершения o 1.8 Сокрытие имен o 1.9 Композиция в сравнении с наследованием o 1.10 protected o 1.11 Восходящее преобразование типов § 1.11.1 Почему «восходящее преобразование»? § 1.11.2 Снова о композиции с наследованием o 1.12 Ключевое слово final § 1.12.1 Неизменные данные § 1.12.2 Пустые константы § 1.12.3 Неизменные аргументы § 1.12.4 Неизменные методы § 1.12.5 Cпецификаторы final и private § 1.12.6 Неизменные классы § 1.12.7 Предостережение o 1.13 Инициализация и загрузка классов § 1.13.1 Инициализация с наследованием o 1.14 Резюме

Возможность повторного использования кода принадлежит к числу важнейших преимуществ Java. Впрочем, по-настоящему масштабные изменения отнюдь не сводятся к обычному копированию и правке кода.

Повторное использование на базе копирования кода характерно для процедурных языков, подобных C, но оно работало не очень хорошо. Решение этой проблемы в Java, как и многое другое, строится на концепции класса. Вместо того чтобы создавать новый класс «с чистого листа», вы берете за основу уже существующий класс, который кто-то уже создал и проверил на работоспособность.

Хитрость состоит в том, чтобы использовать классы без ущерба для существующего кода. В этой главе рассматриваются два пути реализации этой идеи. Первый довольно прямолинеен: объекты уже имеющихся классов просто создаются внутри вашего нового класса. Механизм построения нового класса из объектов существующих классов называется композицией (composition). Вы просто используете функциональность готового кода, а не его структуру.



Второй способ гораздо интереснее. Новый класс создается как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса. Этот механизм называется наследованием (inheritance), и большую часть работы в нем совершает компилятор. Наследование является одним из «краеугольных камней» объектно-ориентированного программирования; некоторые из его дополнительных применений описаны в главе 8.

Синтаксис и поведение типов при использовании композиции и наследования нередко совпадают (что вполне логично, так как оба механизма предназначены для построения новых типов на базе уже существующих). В этой главе рассматриваются оба механизма повторного использования кода.

Синтаксис композиции

До этого момента мы уже довольно часто использовали композицию — ссылка на внедряемый объект просто включается в новый класс. Допустим, вам понадобился объект, содержащий несколько объектов String, пару полей примитивного типа и объект еще одного класса. Для не-примитивных объектов в новый класс включаются ссылки, а примитивы определяются сразу:

//: reusing/SprinklerSystem.java

// Композиция для повторного использования кода.

 

class WaterSource {

private String s;

WaterSource() {

System.out.println("WaterSource()");

s = "Constructed";

}

public String toString() { return s; }

}

 

public class SprinklerSystem {

private String valve1, valve2, valve3, valve4;

private WaterSource source = new WaterSource();

private int i;

private float f;

public String toString() {

Return

"valve1 = " + valve1 + " " +

"valve2 = " + valve2 + " " +

"valve3 = " + valve3 + " " +

"valve4 = " + valve4 + "\n" +

"i = " + i + " " + "f = " + f + " " +

"source = " + source;

}

public static void main(String[] args) {

SprinklerSystem sprinklers = new SprinklerSystem();

System.out.println(sprinklers);

}

}

<spoiler text="Output:"> WaterSource()

valve1 = null valve2 = null valve3 = null valve4 = null

i = 0 f = 0.0 source = Constructed

</spoiler> В обоих классах определяется особый метод toString(). Позже вы узнаете, что каждый не-примитивный объект имеет метод toString(), который вызывается в специальных случаях, когда компилятор располагает не объектом, а хочет получить его строковое представление в формате String. Поэтому в выражении из метода SрrinklerSystem.toString ():

"source = " + source;

компилятор видит, что к строке "source = " «прибавляется» объект класса WaterSource. Компилятор не может это сделать, поскольку к строке можно «добавить» только такую же строку, поэтому он преобразует объект source в String, вызывая метод toString(). После этого компилятор уже в состоянии соединить две строки и передать результат в метод System.out.println() (или статическим методам print() и printnb(), используемым в книге). Чтобы подобное поведение поддерживалось вашим классом, достаточно включить в него методtoString().

Примитивные типы, определенные в качестве полей класса, автоматически инициализируются нулевыми значениями, как упоминалось в главе 2. Однако ссылки на объекты заполняются значениями null, и при попытке вызова метода по такой ссылке произойдет исключение. К счастью, ссылку null можно вывести без выдачи исключения.

Компилятор не создает объекты для ссылок «по умолчанию», и это логично, потому что во многих случаях это привело бы к лишним затратам ресурсов. Если вам понадобится проинициализировать ссылку, сделайте это самостоятельно:

· в точке определения объекта. Это значит, что объект всегда будет инициализироваться перед вызовом конструктора;

· в конструкторе данного класса;

· непосредственно перед использованием объекта. Этот способ часто называют отложенной инициализацией. Он может сэкономить вам ресурсы в ситуациях, где создавать объект каждый раз необязательно и накладно;

· с использованием инициализации экземпляров.

В следующем примере продемонстрированы все четыре способа:

//: reusing/Bath.java

// Инициализация в конструкторе с композицией.

import static net.mindview.util.Print.*;

 

class Soap {

private String s;

Soap() {

print("Soap()");

s = "Constructed";

}

public String toString() { return s; }

}

 

public class Bath {

private String // Инициализация в точке определения :

s1 = "Happy",

s2 = "Happy",

s3, s4;

private Soap castille;

private int i;

private float toy;

public Bath() {

print("Inside Bath()");

s3 = "Joy";

toy = 3.14f;

castille = new Soap();

}

// Инициализация экземпляра:

{ i = 47; }

public String toString() {

if(s4 == null) // Отложенная инициализация:

s4 = "Joy";

Return

"s1 = " + s1 + "\n" +

"s2 = " + s2 + "\n" +

"s3 = " + s3 + "\n" +

"s4 = " + s4 + "\n" +

"i = " + i + "\n" +

"toy = " + toy + "\n" +

"castille = " + castille;

}

public static void main(String[] args) {

Bath b = new Bath();

print(b);

}

}

<spoiler text="Output:">

Inside Bath()

Soap()

s1 = Happy

s2 = Happy

s3 = Joy

s4 = Joy

i = 47

toy = 3.14

castille = Constructed

</spoiler> Заметьте, что в конструкторе класса Bath команда выполняется до проведения какой-либо инициализации. Если инициализация в точке определения не выполняется, нет никаких гарантий того, что она будет выполнена перед отправкой сообщения по ссылке объекта — кроме неизбежных исключений времени выполнения. При вызове метода toString() в нем присваивается значение ссылке s4, чтобы все поля были должным образом инициализированы к моменту их использования.

Синтаксис наследования

Наследование является неотъемлемой частью Java (и любого другого языка ООП). Фактически оно всегда используется при создании класса, потому что, даже если класс не объявляется производным от другого класса, он автоматически становится производным от корневого класса Java Object.

Синтаксис композиции очевиден, но для наследования существует совершенно другая форма записи. При использовании наследования вы фактически говорите: «Этот новый класс похож на тот старый класс». В программе этот факт выражается перед фигурной скобкой, открывающей тело класса: сначала записывается ключевое слово extends, а затем имя базового (base) класса. Тем самым вы автоматически получаете доступ ко всем полям и методам базового класса. Пример:

//: reusing/Detergent.java

// Синтаксис наследования и его свойства.

import static net.mindview.util.Print.*;

 

class Cleanser {

private String s = "Cleanser";

public void append(String a) { s += a; }

public void dilute() { append(" dilute()"); }

public void apply() { append(" apply()"); }

public void scrub() { append(" scrub()"); }

public String toString() { return s; }

public static void main(String[] args) {

Cleanser x = new Cleanser();

x.dilute(); x.apply(); x.scrub();

print(x);

}

}

 

public class Detergent extends Cleanser {

// Изменяем метод:

public void scrub() {

append(" Detergent.scrub()");

super.scrub(); // Вызываем метод базового класса

}

// Добавляем новые методы к интерфейсу :

public void foam() { append(" foam()"); }

// Проверяем новый класс:

public static void main(String[] args) {

Detergent x = new Detergent();

x.dilute();

x.apply();

x.scrub();

x.foam();

print(x);

print("Testing base class:");

Cleanser.main(args);

}

}

<spoiler text="Output:">

Cleanser dilute() apply() Detergent.scrub() scrub() foam()

Testing base class:

Cleanser dilute() apply() scrub()

</spoiler> Пример демонстрирует сразу несколько особенностей наследования. Во-первых, в методе класса Cleanser.append() новые строки присоединяются к строке s оператором +=, одним из операторов, специально «перегруженных» создателями Java для строк (String).

Во-вторых, как Cleanser, так и Detergent содержат метод main(). Вы можете определить метод main() в каждом из своих классов; это позволяет встраивать тестовый код прямо в класс. Метод main() даже не обязательно удалять после завершения тестирования, его вполне можно оставить на будущее.

Даже если у вас в программе имеется множество классов, из командной строки исполняется только один (так как метод main() всегда объявляется как public, то неважно, объявлен ли класс, в котором он описан, как public). В нашем примере команда java Detergentвызывает метод Detergent.main(). Однако вы также можете использовать команду java Cleanser для вызова метода Cleanser.main(), хотя класс Cleanser не объявлен открытым. Даже если класс обладает доступом в пределах класса, открытый метод main() остается доступным.

Здесь метод Detergent.main() вызывает Cleanser.main() явно, передавая ему собственный массив аргументов командной строки (впрочем, для этого годится любой массив строк).

Важно, что все методы класса Cleanser объявлены открытыми. Помните, что при отсутствии спецификатора доступа, член класса автоматически получает доступ «в пределах пакета», что позволяет обращаться к нему только из текущего пакета. Таким образом, в пределах данного пакета при отсутствии спецификатора доступа вызов этих методов разрешен кому угодно — например, это легко может сделать класс Detergent.

Но если бы какой-то класс из другого пакета был объявлен производным от класса Cleanser, то он получил бы доступ только к его public-членам. С учетом возможности наследования все поля обычно помечаются как private, а все методы — как public. (Производный класс также получает доступ к защищенным (protected) членам базового класса, но об этом позже.) Конечно, иногда вы будете отступать от этих правил, но в любом случае полезно их запомнить.

Класс Cleanser содержит ряд методов: append(), dilute(), apply(), scrub() и toString(). Так как класс Detergent произведен от класса Cleanser (с помощью ключевого слова extends), он автоматически получает все эти методы в своем интерфейсе, хотя они и не определяются явно в классе Detergent. Таким образом, наследование обеспечивает повторное использование класса.

Как показано на примере метода scrub(), разработчик может взять уже существующий метод базового класса и изменить его. Возможно, в этом случае потребуется вызвать метод базового класса из новой версии этого метода. Однако в методе scrub() вы не можете просто вызвать scrub() — это приведет к рекурсии, а нам нужно не это. Для решения проблемы в Java существует ключевое слово super, которое обозначает «суперкласс», то есть класс, производным от которого является текущий класс. Таким образом, выражение super.scrub()обращается к методу scrub() из базового класса.

При наследовании вы не ограничены использованием методов базового класса. В производный класс можно добавлять новые методы тем же способом, что и раньше, то есть просто определяя их. Метод foam() — наглядный пример такого подхода.

В методе Detergent.main() для объекта класса Detergent вызываются все методы, доступные как из класса Cleanser, так и из класса Detergent (имеется в виду метод foam()).

 

Инициализация базового класса

Так как в наследовании участвуют два класса, базовый и производный, не сразу понятно, какой же объект получится в результате. Внешне все выглядит так, словно новый класс имеет тот же интерфейс, что и базовый класс, плюс еще несколько дополнительных методов и полей. Однако наследование не просто копирует интерфейс базового класса. Когда вы создаете объект производного класса, внутри него содержится подобъект базового класса. Этот подобъект выглядит точно так же, как выглядел бы созданный обычным порядком объект базового класса. Поэтому извне представляется, будто бы в объекте производного класса «упакован» объект базового класса.

Конечно, очень важно, чтобы подобъект базового класса был правильно инициализирован, и гарантировать это можно только одним способом: выполнить инициализацию в конструкторе, вызывая при этом конструктор базового класса, у которого есть необходимые знания и привилегии для проведения инициализации базового класса. Java автоматически вставляет вызовы конструктора базового класса в конструктор производного класса. В следующем примере задействовано три уровня наследования:

//: reusing/Cartoon.java

// Вызовы конструкторов при проведении наследования

import static net.mindview.util.Print.*;

 

class Art {

Art() { print("Art constructor"); }

}

 

class Drawing extends Art {

Drawing() { print("Drawing constructor"); }

}

 

public class Cartoon extends Drawing {

public Cartoon() { print("Cartoon constructor"); }

public static void main(String[] args) {

Cartoon x = new Cartoon();

}

}

<spoiler text="Output:">

Art constructor

Drawing constructor

Cartoon constructor

</spoiler> Как видите, конструирование начинается с «самого внутреннего» базового класса, поэтому базовый класс инициализируется еще до того, как он станет доступным для конструктора производного класса. Даже если конструктор класса Cartoon не определен, компилятор сгенерирует конструктор по умолчанию, в котором также вызывается конструктор базового класса.

 

Конструкторы с аргументами

В предыдущем примере использовались конструкторы по умолчанию, то есть конструкторы без аргументов. У компилятора не возникает проблем с вызовом таких конструкторов, так как вопросов о передаче аргументов не возникает. Если класс не имеет конструктора по умолчанию или вам понадобится вызвать конструктор базового класса с аргументами, этот вызов придется оформить явно, с указанием ключевого слова super и передачей аргументов:

//: reusing/Chess.java

// Наследование, конструкторы и аргументы.

import static net.mindview.util.Print.*;

 

class Game {

Game(int i) {

print("Game constructor");

}

}

 

class BoardGame extends Game {

BoardGame(int i) {

super(i);

print("BoardGame constructor");

}

}

 

public class Chess extends BoardGame {

Chess() {

super(11);

print("Chess constructor");

}

public static void main(String[] args) {

Chess x = new Chess();

}

}

<spoiler text="Output:">

Game constructor

BoardGame constructor

Chess constructor

</spoiler> Если не вызвать конструктор базового класса в BoardGame(), то компилятор «пожалуется» на то, что не может обнаружить конструктор в форме Game(). Вдобавок вызов конструктора базового класса должен быть первой командой в конструкторе производного класса. (Если вы вдруг забудете об этом, компилятор вам тут же напомнит.)

 

Делегирование

Третий вид отношений, не поддерживаемый в Java напрямую, называется делегированием. Он занимает промежуточное положение между наследованием и композицией: экземпляр существующего класса включается в создаваемый класс (как при композиции), но в то же время все методы встроенного объекта становятся доступными в новом классе (как при наследовании). Например, класс SpaceShipControls имитирует модуль управления космическим кораблем:

//: reusing/SpaceShipControls.java

 

public class SpaceShipControls {

void up(int velocity) {}

void down(int velocity) {}

void left(int velocity) {}

void right(int velocity) {}

void forward(int velocity) {}

void back(int velocity) {}

void turboBoost() {}

}

Для построения космического корабля можно воспользоваться наследованием:

//: reusing/SpaceShip.java

 

public class SpaceShip extends SpaceShipControls {

private String name;

public SpaceShip(String name) { this.name = name; }

public String toString() { return name; }

public static void main(String[] args) {

SpaceShip protector = new SpaceShip("NSEA Protector");

protector.forward(100);

}

}

Однако космический корабль не может рассматриваться как частный случай своего управляющего модуля — несмотря на то, что ему, к примеру, можно приказать двигаться вперед (forward()). Точнее сказать, что SpaceShip содержит SpaceShipControls, и в то же время все методы последнего предоставляются классом SpaceShip. Проблема решается при помощи делегирования:

//: reusing/SpaceShipDelegation.java

 

public class SpaceShipDelegation {

private String name;

private SpaceShipControls controls =

new SpaceShipControls();

public SpaceShipDelegation(String name) {

this.name = name;

}

// Делегированные методы:

public void back(int velocity) {

controls.back(velocity);

}

public void down(int velocity) {

controls.down(velocity);

}

public void forward(int velocity) {

controls.forward(velocity);

}

public void left(int velocity) {

controls.left(velocity);

}

public void right(int velocity) {

controls.right(velocity);

}

public void turboBoost() {

controls.turboBoost();

}

public void up(int velocity) {

controls.up(velocity);

}

public static void main(String[] args) {

SpaceShipDelegation protector =

new SpaceShipDelegation("NSEA Protector");

protector.forward(100);

}

}

Как видите, вызовы методов переадресуются встроенному объекту controls, а интерфейс остается таким же, как и при наследовании. С другой стороны, делегирование позволяет лучше управлять происходящим, потому что вы можете ограничиться небольшим подмножеством методов встроенного объекта. Хотя делегирование не поддерживается языком Java, его поддержка присутствует во многих средах разработки. Например, приведенный пример был автоматически сгенерирован в JetBrains Idea IDE.

Сочетание композиции и наследования

Композиция очень часто используется вместе с наследованием. Следующий пример демонстрирует процесс создания более сложного класса с объединением композиции и наследования, с выполнением необходимой инициализации в конструкторе:

//: reusing/PlaceSetting.java

// Совмещение композиции и наследования.

import static net.mindview.util.Print.*;

 

class Plate {

Plate(int i) {

print("Plate constructor");

}

}

 

class DinnerPlate extends Plate {

DinnerPlate(int i) {

super(i);

print("DinnerPlate constructor");

}

}

 

class Utensil {

Utensil(int i) {

print("Utensil constructor");

}

}

 

class Spoon extends Utensil {

Spoon(int i) {

super(i);

print("Spoon constructor");

}

}

 

class Fork extends Utensil {

Fork(int i) {

super(i);

print("Fork constructor");

}

}

 

class Knife extends Utensil {

Knife(int i) {

super(i);

print("Knife constructor");

}

}

 

// A cultural way of doing something:

class Custom {

Custom(int i) {

print("Custom constructor");

}

}

 

public class PlaceSetting extends Custom {

private Spoon sp;

private Fork frk;

private Knife kn;

private DinnerPlate pl;

public PlaceSetting(int i) {

super(i + 1);

sp = new Spoon(i + 2);

frk = new Fork(i + 3);

kn = new Knife(i + 4);

pl = new DinnerPlate(i + 5);

print("PlaceSetting constructor");

}

public static void main(String[] args) {

PlaceSetting x = new PlaceSetting(9);

}

}

<spoiler text="Output:">

Custom constructor

Utensil constructor

Spoon constructor

Utensil constructor

Fork constructor

Utensil constructor

Knife constructor

Plate constructor

DinnerPlate constructor

PlaceSetting constructor

</spoiler> Несмотря на то, что компилятор заставляет вас инициализировать базовые классы и требует, чтобы вы делали это прямо в начале конструктора, он не следит за инициализацией встроенный объектов, поэтому вы должны сами помнить об этом.

 

Обеспечение правильного завершения

В Java отсутствует понятие деструктора из C++ — метода, автоматически вызываемого при уничтожении объекта. В Java программисты просто «забывают» об объектах, не уничтожая их самостоятельно, так как функции очистки памяти возложены на сборщика мусора.

Во многих случаях эта модель работает, но иногда класс выполняет некоторые операции, требующие завершающих действий. Как упоминалось в главе 5, вы не знаете, когда будет вызван сборщик мусора и произойдет ли это вообще. Поэтому, если в классе должны выполняться действия по очистке, вам придется написать для этого особый метод и сделать так, чтобы программисты-клиенты знали о необходимости вызова этого метода. Более того, как описано в главе 10, вам придется предусмотреть возможные исключения и выполнить завершающие действия в секции finally. Представим пример системы автоматизированного проектирования, которая рисует на экране изображения:

//: reusing/CADSystem.java

// Обеспечение необходимого завершения.

package reusing;

import static net.mindview.util.Print.*;

 

class Shape {

Shape(int i) { print("Shape constructor"); }

void dispose() { print("Shape dispose"); }

}

 

class Circle extends Shape {

Circle(int i) {

super(i);

print("Drawing Circle");

}

void dispose() {

print("Erasing Circle");

super.dispose();

}

}

 

class Triangle extends Shape {

Triangle(int i) {

super(i);

print("Drawing Triangle");

}

void dispose() {

print("Erasing Triangle");

super.dispose();

}

}

 

class Line extends Shape {

private int start, end;

Line(int start, int end) {

super(start);

this.start = start;

this.end = end;

print("Drawing Line: " + start + ", " + end);

}

void dispose() {

print("Erasing Line: " + start + ", " + end);

super.dispose();

}

}

 

public class CADSystem extends Shape {

private Circle c;

private Triangle t;

private Line[] lines = new Line[3];

public CADSystem(int i) {

super(i + 1);

for(int j = 0; j < lines.length; j++)

lines[j] = new Line(j, j*j);

c = new Circle(1);

t = new Triangle(1);

print("Combined constructor");

}

public void dispose() {

print("CADSystem.dispose()");

// Завершение осуществляется в порядке,

// обратном порядку инициализации

t.dispose();

c.dispose();

for(int i = lines.length - 1; i >= 0; i--)

lines[i].dispose();

super.dispose();

}

public static void main(String[] args) {

CADSystem x = new CADSystem(47);

try {

// Код и обработка исключений...

} finally {

x.dispose();

}

}

}

<spoiler text="Output:">

Shape constructor

Shape constructor

Drawing Line: 0, 0

Shape constructor

Drawing Line: 1, 1

Shape constructor

Drawing Line: 2, 4

Shape constructor

Drawing Circle

Shape constructor

Drawing Triangle

Combined constructor

CADSystem.dispose()

Erasing Triangle

Shape dispose

Erasing Circle

Shape dispose

Erasing Line: 2, 4

Shape dispose

Erasing Line: 1, 1

Shape dispose

Erasing Line: 0, 0

Shape dispose

Shape dispose

</spoiler> Все в этой системе является некоторой разновидностью класса Shape (который, в свою очередь, неявно наследует от корневого класса Object). Каждый класс переопределяет метод dispose() класса Shape, вызывая при этом версию метода из базового класса с помощью ключевого слова super.

Все конкретные классы, унаследованные от ShapeCircle, Triangle и Line, имеют конструкторы, которые просто выводят сообщение, хотя во время жизни объекта любой метод может сделать что-то, требующее очистки. В каждом классе есть свой собственный методdispose(), который восстанавливает ресурсы, не связанные с памятью, к исходному состоянию до создания объекта.

В методе main() вы можете заметить два новых ключевых слова, которые будут подробно рассмотрены в главе 10: try и finally. Ключевое слово try показывает, что следующий за ним блок (ограниченный фигурными скобками) является защищенной секцией. Код в секцииfinally выполняется всегда, независимо от того, как прошло выполнение блока try. (При обработке исключений можно выйти из блока try некоторыми необычными способами.) В данном примере секция finally означает: «Что бы ни произошло, в конце всегда вызывать методx.dispose()».

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

Во многих случаях завершающие действия не являются проблемой; достаточно дать сборщику мусора выполнить свою работу. Но уж если понадобилось провести их явно, сделайте это со всей возможной тщательностью и вниманием, так как в процессе сборки мусора трудно в чем-либо быть уверенным. Сборщик мусора вообще может не вызываться, а если он начнет работать, то объекты будут уничтожаться в произвольном порядке. Лучше не полагаться на сборщик мусора в ситуациях, где дело не касается освобождения памяти. Если вы хотите провести завершающие действия, создайте для этой цели свой собственный метод и не полагайтесь на метод finalize().

 

Сокрытие имен

Если какой-либо из методов базового класса Java был перегружен несколько раз, переопределение имени этого метода в производном классе не скроет ни одну из базовых версий (в отличие от C++). Поэтому перегрузка работает вне зависимости от того, где был определен метод — на текущем уровне или в базовом классе:

//: reusing/Hide.java

// Перегрузка имени метода из базового класса

// в производном классе не скроет базовую версию метода.

import static net.mindview.util.Print.*;

 

class Homer {

char doh(char c) {

print("doh(char)");

return 'd';

}

float doh(float f) {

print("doh(float)");

return 1.0f;

}

}

 

class Milhouse {}

 

class Bart extends Homer {

void doh(Milhouse m) {

print("doh(Milhouse)");

}

}

 

public class Hide {

public static void main(String[] args) {

Bart b = new Bart();

b.doh(1);

b.doh('x');

b.doh(1.0f);

b.doh(new Milhouse());

}

}

<spoiler text="Output:">

doh(float)

doh(char)

doh(float)

doh(Milhouse)

</spoiler> Мы видим, что все перегруженные методы класса Homer доступны классу Bart, хотя класс Bart и добавляет новый перегруженный метод (в C++ такое действие спрятало бы все методы базового класса). Как вы увидите в следующей главе, на практике при переопределении методов гораздо чаще используется точно такое же описание и список аргументов, как и в базовом классе. Иначе легко можно запутаться (и поэтому C++ запрещает это, чтобы предотвратить совершение возможной ошибки).

В Java SE5 появилась запись @Override; она не является ключевым словом, но может использоваться так, как если бы была им. Если вы собираетесь переопределить метод, используйте @Override, и компилятор выдаст сообщение об ошибке, если вместо переопределения будет случайно выполнена перегрузка:

//: reusing/Lisa.java

// {CompileTimeError} (Won't compile)

 

class Lisa extends Homer {

@Override

void doh(Milhouse m) {

System.out.println("doh(Milhouse)");

}

}

 

Композиция в сравнении с наследованием

И композиция, и наследование позволяют вам помещать подобъекты внутрь вашего нового класса (при композиции это происходит явно, а в наследовании — опосредованно). Вы можете поинтересоваться, в чем между ними разница и когда следует выбирать одно, а когда — другое.

Композиция в основном применяется, когда в новом классе необходимо использовать функциональность уже существующего класса, но не его интерфейс. То есть вы встраиваете объект, чтобы использовать его возможности в новом классе, а пользователь класса видит определенный вами интерфейс, но не замечает встроенных объектов. Для этого внедряемые объекты объявляются со спецификатором private.

Иногда требуется предоставить пользователю прямой доступ к композиции вашего класса, то есть сделать встроенный объект открытым (public). Встроенные объекты и сами используют сокрытие реализации, поэтому открытый доступ безопасен. Когда пользователь знает, что класс собирается из составных частей, ему значительно легче понять его интерфейс. Хорошим примером служит объект Саr (машина):

//: reusing/Car.java

// Композиция с использованием открытых объектов

 

class Engine {

public void start() {}

public void rev() {}

public void stop() {}

}

 

class Wheel {

public void inflate(int psi) {}

}

 

class Window {

public void rollup() {}

public void rolldown() {}

}

 

class Door {

public Window window = new Window();

public void open() {}

public void close() {}

}

 

public class Car {

public Engine engine = new Engine();

public Wheel[] wheel = new Wheel[4];

public Door

left = new Door(),

right = new Door(); // 2-door

public Car() {

for(int i = 0; i < 4; i++)

wheel[i] = new Wheel();

}

public static void main(String[] args) {

Car car = new Car();

car.left.window.rollup();

car.wheel[0].inflate(72);

}

}

Так как композиция объекта является частью проведенного анализа задачи (а не просто частью реализации класса), объявление членов класса открытыми (public) помогает программисту-клиенту понять, как использовать класс, и облегчает создателю класса написание кода. Однако нужно все-таки помнить, что описанный случай является специфическим и в основном поля класса следует объявлять как private.

При использовании наследования вы берете уже существующий класс и создаете его специализированную версию. В основном это значит, что класс общего назначения адаптируется для конкретной задачи. Если чуть-чуть подумать, то вы поймете, что не имело бы смысла использовать композицию машины и средства передвижения — машина не содержит средства передвижения, она сама есть это средство. Взаимосвязь «является» выражается наследованием, а взаимосвязь «имеет» описывается композицией.

 

protected

После знакомства с наследованием ключевое слово protected наконец-то обрело смысл. В идеале закрытых членов private должно было быть достаточно. В реальности существуют ситуации, когда вам необходимо спрятать что-либо от ок­ружающего мира, тем не менее оставив доступ для производных классов.

Ключевое слово protected — дань прагматизму. Оно означает: «Член класса является закрытым (private) для пользователя класса, но для всех, кто наследует от класса, и для соседей по пакету он доступен». (В Java protected автоматически предоставляет доступ в пределах пакета.)

Лучше всего, конечно, объявлять поля класса как private — всегда стоит оставить за собою право изменять лежащую в основе реализацию. Управляемый доступ наследникам класса предоставляется через методы protected:

//: reusing/Orc.java

// Ключевое слово protected

import static net.mindview.util.Print.*;

 

class Villain {

private String name;

protected void set(String nm) { name = nm; }

public Villain(String name) { this.name = name; }

public String toString() {

return "I'm a Villain and my name is " + name;

}

}

 

public class Orc extends Villain {

private int orcNumber;

public Orc(String name, int orcNumber) {

super(name);

this.orcNumber = orcNumber;

}

public void change(String name, int orcNumber) {

set(name); // Доступно, так как объявлено protected

this.orcNumber = orcNumber;

}

public String toString() {

return "Orc " + orcNumber + ": " + super.toString();

}

public static void main(String[] args) {

Orc orc = new Orc("Limburger", 12);

print(orc);

orc.change("Bob", 19);

print(orc);

}

}

<spoiler text="Output:">

Orc 12: I'm a Villain and my name is Limburger

Orc 19: I'm a Villain and my name is Bob

</spoiler> Как видите, метод change() имеет доступ к методу set(), поскольку тот объявлен как protected. Также обратите внимание, что метод toString() класса Orс определяется с использованием версии этого метода из базового класса.

 

Восходящее преобразование типов

Самая важная особенность наследования заключается вовсе не в том, что оно предоставляет методы для нового класса, — наследование выражает отношения между новым и базовым классом. Ее можно выразить .следующим образом: «Новый класс имеет тип существующего класса».

Данная формулировка — не просто причудливый способ описания наследования, она напрямую поддерживается языком. В качестве примера рассмотрим базовый класс с именем Instrument для представления музыкальных инструментов и его производный класс Wind. Так как наследование означает, что все методы базового класса также доступны в производном классе, любое сообщение, которое вы в состоянии отправить базовому классу, можно отправить и производному классу.

Если в классе Instrument имеется метод play(), то он будет присутствовать и в классе Wind. Таким образом, мы можем со всей определенностью утверждать, что объекты Wind также имеют тип Instrument. Следующий пример показывает, как компилятор поддерживает такое понятие:

//: reusing/Wind.java

// Наследование и восходящее преобразование.

 

class Instrument {

public void play() {}

static void tune(Instrument i) {

// ...

i.play();

}

}

 

// Объекты Wind также являются объектами Instrument,

// поскольку они имеют тот же интерфейс:

public class Wind extends Instrument {

public static void main(String[] args) {

Wind flute = new Wind();

Instrument.tune(flute); // Восходящее преобразование

}

}

Наибольший интерес в этом примере представляет метод tune(), получающий ссылку на объект Instrument. Однако в методе Wind.main() методу tune() передается ссылка на объект Wind. С учетом всего, что говорилось о строгой проверке типов в Java, кажется странным, что метод с готовностью берет один тип вместо другого. Но стоит вспомнить, что объект Wind также является объектом Instrument, и не существует метода, который можно вызвать в методе tune() для объектов Instrument, но нельзя для объектов Wind. В методе tune() код работает для Instrument и любых объектов, производных от Instrument, а преобразование ссылки на объект Wind в ссылку на объект Instrument называется восходящим преобразованием типов (upcasting).

 



<== предыдущая лекция | следующая лекция ==>
Protected | Почему «восходящее преобразование»?


Карта сайта Карта сайта укр


Уроки php mysql Программирование

Онлайн система счисления Калькулятор онлайн обычный Инженерный калькулятор онлайн Замена русских букв на английские для вебмастеров Замена русских букв на английские

Аппаратное и программное обеспечение Графика и компьютерная сфера Интегрированная геоинформационная система Интернет Компьютер Комплектующие компьютера Лекции Методы и средства измерений неэлектрических величин Обслуживание компьютерных и периферийных устройств Операционные системы Параллельное программирование Проектирование электронных средств Периферийные устройства Полезные ресурсы для программистов Программы для программистов Статьи для программистов Cтруктура и организация данных


 


Не нашли то, что искали? Google вам в помощь!

 
 

© life-prog.ru При использовании материалов прямая ссылка на сайт обязательна.

Генерация страницы за: 0.309 сек.