Глава 10 Thinking in Java 4th edition |
ВНУТРЕННИЕ КЛАССЫ
Определение класса может размещаться внутри определения другого класса. Такие классы называются внутренними (inner class). Внутренние классы весьма полезны, так как они позволяют группировать классы, логически принадлежащие друг другу, и управлять доступом к ним.ВНУТРЕННИЕ КЛАССЫ
Определение класса может размещаться внутри определения другого класса. Такие классы называются внутренними (inner class).
Внутренние классы весьма полезны, так как они позволяют группировать классы, логически принадлежащие друг другу, и управлять доступом к ним. Однако следует понимать, что внутренние классы заметно отличаются от композиции.
На первый взгляд создается впечатление, что внутренние классы представляют собой простой механизм сокрытия кода. Однако вскоре вы узнаете, что возможности внутренних классов гораздо шире (они знают о существовании внешних классов и могут работать с ними), а программный код с внутренними классами часто бывает более элегантным и понятным (хотя конечно, этого никто не гарантирует).
В этой главе подробно исследуется синтаксис внутренних классов. Эти возможности представлены для полноты материала, хотя, скорее всего, на первых порах они вам не понадобятся. Возможно, начальные разделы этой главы содержат все, что вам действительно необходимо знать на этой стадии, а к более подробным объяснениям можно относиться как к справочному, дополнительному материалу.
Создание внутренних классов
Внутренние классы создаются в точности так, как и следовало ожидать, — определение класса размещается внутри окружающего класса:
//: innerclasses/Parcel1.java
// Creating inner classes.
public class Parcel1 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
// Использование внутренних классов имеет много общего
// с использованием любых других классов в пределах Parcel1
public void ship(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
System.out.println(d.readLabel());
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship("Tasmania");
}
}
<spoiler text="Output:">
Tasmania
</spoiler>
The inner classes used inside ship( ) look just like ordinary classes. Here, the only practical
difference is that the names are nested within Parceli. You’ll see in a while that this isn’t the
only difference. More typically, an outer class will have a method that returns a reference to
an inner class, as you can see in the to( ) and contents( ) methods:
//: innerclasses/Parcel2.java
// Returning a reference to an inner class.
public class Parcel2 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) {
label = whereTo;
}
String readLabel() { return label; }
}
public Destination to(String s) {
return new Destination(s);
}
public Contents contents() {
return new Contents();
}
public void ship(String dest) {
Contents c = contents();
Destination d = to(dest);
System.out.println(d.readLabel());
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
p.ship("Tasmania");
Parcel2 q = new Parcel2();
// Defining references to inner classes:
Parcel2.Contents c = q.contents();
Parcel2.Destination d = q.to("Borneo");
}
}
<spoiler text="Output:">
Tasmania
</spoiler>
Если вам понадобится создать объект внутреннего класса где-либо, кроме как в не-статическом методе внешнего класса, тип этого объекта должен задаваться в формате
ИмяВнешнегоКласса.ИмяВнутреннегоКласса
что и делается в методе main().
Связь с внешним классом
Пока что внутренние классы выглядят как некоторая схема для сокрытия имен и организации кода — полезная, но не особенно впечатляющая. Однако есть еще один нюанс. Объект внутреннего класса связан с внешним объектом-создателем и может обращаться к его членам без каких-либо дополнительных описаний. Вдобавок для внутренних классов доступны все без исключения элементы внешнего класса. Следующий пример иллюстрирует сказанное:
//: innerclasses/Sequence.java
// Хранение последовательности объектов
interface Selector {
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size) { items = new Object[size]; }
public void add(Object x) {
if(next < items.length)
items[next++] = x;
}
private class SequenceSelector implements Selector {
private int i = 0;
public boolean end() { return i == items.length; }
public Object current() { return items[i]; }
public void next() { if(i < items.length) i++; }
}
public Selector selector() {
return new SequenceSelector();
}
public static void main(String[] args) {
Sequence sequence = new Sequence(10);
for(int i = 0; i < 10; i++)
sequence.add(Integer.toString(i));
Selector selector = sequence.selector();
while(!selector.end()) {
System.out.print(selector.current() + " ");
selector.next();
}
}
}
<spoiler text="Output:">
0 1 2 3 4 5 6 7 8 9
</spoiler>
Класс Sequence — не более чем «оболочка» для массива с элементами Object, имеющего фиксированный размер. Для добавления новых объектов в конец последовательности (при наличии свободного места) используется метод add(). Для выборки каждого объекта в последовательности Sequence предусмотрен интерфейс с именем Selector. Он позволяет узнать, достигнут ли конец последовательности (метод end()), обратиться к текущему объекту (метод current()) и перейти к следующему объекту последовательности (метод next()). Так как Selector является интерфейсом, другие классы вправе реализовать его по-своему, а также другие методы могут использовать интерфейс как аргумент - всё это повышает универсальность кода.
Здесь SequenceSelector является закрытым (private) классом, предоставляющим функциональность интерфейса Selector. В методе main() вы можете наблюдать за процессом создания последовательности Sequence с последующим заполнением ее объектами String. Затем вызывается метод selector() для получения интерфейса Selector, который используется для перемещения по последовательности и выбора ее элементов.
На первый взгляд создание SequenceSelector напоминает создание обычного внутреннего класса. Но присмотритесь к нему повнимательнее. Заметьте, что в каждом из методов end(), current() и next() присутствует ссылка на items, а это не одно из полей класса SequenceSelector, а закрытое (private) поле объемлющего класса. Внутренний класс может обращаться ко всем полям и методам внешнего класса-оболочки, как будто они описаны в нем самом. Это весьма удобно, и вы могли в этом убедиться, изучая рассмотренный пример.
Итак, внутренний класс автоматически получает доступ к членам объемлющего класса. Как же это происходит? Внутренний класс содержит скрытую ссылку на определенный объект окружающего класса, ответственный за его создание. При обращении к члену окружающего класса используется эта (скрытая) ссылка.
К счастью, все технические детали обеспечиваются компилятором, но теперь вы знаете, что объект внутреннего класса можно создать только в сочетании с объектом внешнего класса (как будет показано позже, если внутренний класс не является статическим). Конструирование объекта внутреннего класса требует наличия ссылки на объект внешнего класса; если ссылка недоступна, компилятор выдаст сообщение об ошибке. Большую часть времени весь процесс происходит без всякого участия со стороны программиста.
Конструкции .this и .new
Если вам понадобится получить ссылку на объект внешнего класса, запишите имя внешнего класса, за которым следует точка, а затем ключевое слово this. Полученная ссылка автоматически относится к правильному типу, известному и проверяемому на стадии компиляции, поэтому дополнительные издержки на стадии выполнения не требуются. Следующий пример показывает, как использовать конструкцию .this:
//: innerclasses/DotThis.java
// Обращение к объекту внешнего класса.
public class DotThis {
void f() { System.out.println("DotThis.f()"); }
public class Inner {
public DotThis outer() {
return DotThis.this;
// A plain "this" would be Inner's "this"
}
}
public Inner inner() { return new Inner(); }
public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.inner();
dti.outer().f();
}
}
<spoiler text="Output:">
DotThis.f()
</spoiler>
Иногда бывает нужно приказать другому объекту создать объект одного из его внутренних классов. Для этого перед .new указывается ссылка на другой объект внешнего класса:
//: innerclasses/DotNew.java
// Непосредственное создание внутреннего класса в синтаксисе .new
public class DotNew {
public class Inner {}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}
При создании объекта внутреннего класса указывается не имя внешнего класса DotNew, как можно было бы ожидать, а имя объекта внешнего класса. Это также решает проблему видимости имен для внутреннего класса, поэтому мы не используем (а вернее, не можем использовать) запись вида dn.new DotNew.Inner().
Невозможно создать объект внутреннего класса, не имея ссылки на внешний класс. Но если создать вложенный класс (статический внутренний класс), ссылка на объект внешнего класса не нужна.
Рассмотрим пример использования .new в примере Parcel:
//: innerclasses/Parcel3.java
// Использование new для создания экземпляров внутренних классов
public class Parcel3 {
class Contents {
private int i = 11;
public int value() { return i; }
}
class Destination {
private String label;
Destination(String whereTo) { label = whereTo; }
String readLabel() { return label; }
}
public static void main(String[] args) {
Parcel3 p = new Parcel3();
// Для создания экземпляра внутреннего класса
// необходимо использовать экземпляр внешнего класса
Parcel3.Contents c = p.new Contents();
Parcel3.Destination d = p.new Destination("Tasmania");
}
}
Внутренние классы и восходящее преобразование
Мощь внутренних классов по-настоящему проявляется при выполнении восходящего преобразования к базовому классу, и в особенности к интерфейсу. (Получение ссылки на интерфейс по ссылке на реализующий его объект ничем принципиально не отличается от восходящего преобразования к базовому классу.) Причина в том, что внутренний класс — реализация интерфейса — может быть абсолютно невидимым и недоступным окружающему миру, а это очень удобно для сокрытия реализации. Все, что вы при этом получаете, — ссылку на базовый класс или интерфейс.
Для начала определим интерфейсы для предыдущих примеров:
//: innerclasses/Destination.java
public interface Destination {
String readLabel();
}
//: innerclasses/Contents.java
public interface Contents {
int value();
}
Теперь интерфейсы Contents и Destination доступны программисту-клиенту. (Помните, что в объявлении interface все члены класса автоматически являются открытыми (public).)
При получении из метода ссылки на базовый класс или интерфейс возможны ситуации, в которых вам не удастся определить ее точный тип, как здесь:
//: innerclasses/TestParcel.java
class Parcel4 {
private class PContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
public Destination destination(String s) {
return new PDestination(s);
}
public Contents contents() {
return new PContents();
}
}
public class TestParcel {
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
// Запрещено - нет доступа к private-классу:
//! Parcel4.PContents pc = p.new PContents();
}
}
В класс Parcel4 было добавлено кое-что новое: внутренний класс PContents является закрытым (private), поэтому он недоступен для всех, кроме внешнего класса Раrсе14.
Класс PDestination объявлен как protected, следовательно, доступ к нему имеют только класс Parcel4, классы из одного пакета с Раrсеl4 (так как спецификатор protected также дает доступ в пределах пакета) и наследники класса Раrсеl4.
Таким образом, программист-клиент обладает ограниченной информацией и доступом к этим членам класса. Более того, нельзя даже выполнить нисходящее преобразование к закрытому (private) внутреннему классу (или protected, кроме наследников), поскольку его имя недоступно, как показано в классе Test.
Таким образом, закрытый внутренний класс позволяет разработчику класса полностью запретить использование определенных типов и скрыть все детали реализации класса. Вдобавок, расширение интерфейса с точки зрения программиста-клиента не будет иметь смысла, поскольку он не сможет получить доступ к дополнительным методам, не принадлежащим к открытой части класса. Наконец, у компилятора Java появится возможность оптимизировать код.
Внутренние классы в методах и областях действия
Ранее мы рассмотрели ряд типичных применений внутренних классов. В основном ваш код будет содержать «простые» внутренние классы, смысл которых понять нетрудно. Однако синтаксис внутренних классов скрывает множество других, не столь тривиальных способов их использования: внутренние классы можно создавать внутри метода или даже в пределах произвольного блока. На то есть две причины:
- как было показано ранее, вы реализуете некоторый интерфейс, чтобы затем создавать и возвращать ссылку его типа;
- вы создаете вспомогательный класс для решения сложной задачи, но при этом не хотите, чтобы этот класс был открыт для посторонних.
В следующих примерах рассмотренная недавно программа будет изменена, благодаря чему у нас появятся:
- класс, определенный внутри метода;
- класс, определенный внутри области действия (блока), которая находится внутри метода;
- безымянный класс, реализующий интерфейс;
- безымянный класс, расширяющий класс, у которого отсутствует конструктор по умолчанию;
- безымянный класс, выполняющий инициализацию полей;
- безымянный класс, осуществляющий конструирование посредством инициализации экземпляра (безымянные внутренние классы не могут иметь конструкторы).
Первый пример демонстрирует создание целого класса в контексте метода (вместо создания в контексте другого класса). Такие внутренние классы называются локальными:
//: innerclasses/Parcel5.java
// Вложение класса в тело метода.
public class Parcel5 {
public Destination destination(String s) {
class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
}
return new PDestination(s);
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
Destination d = p.destination("Tasmania");
}
}
Теперь класс PDestination является частью метода destination(), а не частью класса Раrсеl5. Поэтому доступ к классу PDestination возможен только из метода destination().
Обратите внимание на восходящее преобразование, производимое в команде return, — из метода возвращается лишь ссылка на базовый класс Destination, и ничего больше. Конечно, тот факт, что имя класса PDestination находится внутри метода destination(), не означает, что объект PDestination после выхода из этого метода станет недоступным.
Идентификатор PDestination может использоваться для внутренних классов каждого отдельного класса в одном подкаталоге, без порождения конфликта имен.
Следующий пример демонстрирует, как можно вложить внутренний класс в произвольную область действия:
//: innerclasses/Parcel6.java
// Вложение класса в область действия
public class Parcel6 {
private void internalTracking(boolean b) {
if(b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() { return id; }
}
TrackingSlip ts = new TrackingSlip("ожидание");
String s = ts.getSlip();
}
// Здесь использовать класс нельзя!
// Вне области видимости
//! TrackingSlip ts = new TrackingSlip("x");
}
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.track();
}
}
Класс TrackingSlip вложен в область действия команды if. Это не значит, что класс создается в зависимости от условия — он компилируется вместе со всем остальным кодом. Однако при этом он недоступен вне контекста, в котором был определен. В остальном он выглядит точно так же, как и обычный класс.
Безымянные внутренние классы
Следующий пример выглядит немного странно:
//: innerclasses/Parcel7.java
// Метод возвращает экземпляр безымянного внутреннего класса
public class Parcel7 {
public Contents contents() {
return new Contents() {// Вставить определение класса
private int i = 11;
public int value() { return i; }
}; // В данной ситуации точка с запятой необходима
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
}
Метод contents() совмещает создание возвращаемого значения с определением класса, который это возвращаемое значение и представляет! Вдобавок, этот класс является безымянным — у него отсутствует имя. Ситуация запутывается еще тем, что поначалу мы будто бы приступаем к созданию объекта Contents, а потом, остановившись перед точкой с запятой, говорим: «Стоп, а сюда я подкину определение класса».
Такая необычная форма записи значит буквально следующее: «Создать объект безымянного класса, который унаследован от Contents». Ссылка, которая возвращается при этом из выражения new, автоматически повышается до базового типа Contents. Синтаксис записи безымянного внутреннего класса является укороченной формой записи такой конструкции:
//: innerclasses/Parcel7b.java
// Расширенная версия Parcel7.java
public class Parcel7b {
class MyContents implements Contents {
private int i = 11;
public int value() { return i; }
}
public Contents contents() { return new MyContents(); }
public static void main(String[] args) {
Parcel7b p = new Parcel7b();
Contents c = p.contents();
}
}
В безымянном внутреннем классе базовый класс Contents создается с использованием конструктора по умолчанию. Следующая программа показывает, как следует поступать, если базовый класс требует вызова конструктора с аргументами:
//: innerclasses/Parcel8.java
// Вызов конструктора базового класса.
public class Parcel8 {
public Wrapping wrapping(int x) {
// Вызов конструктора базового класса:
return new Wrapping(x) { // // аргумент конструктора.
public int value() {
return super.value() * 47;
}
}; // // Требуется точка с запятой
}
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Wrapping w = p.wrapping(10);
}
}
Требуемый аргумент просто передается в конструктор базового класса, как в рассмотренном примере х в выражении new Wrapping(x). Хотя это обычный класс с реализацией, Wrapping также используется в качестве общего «интерфейса» для своих производных классов:
//: innerclasses/Wrapping.java
public class Wrapping {
private int i;
public Wrapping(int x) { i = x; }
public int value() { return i; }
}
Класс Wrapping имеет конструктор с аргументом — просто для того, чтобы ситуация стала чуть более интересной.
Точка с запятой в конце безымянного внутреннего класса поставлена вовсе не для того, чтобы обозначить конец тела класса (как делается в C++). Вместо этого она указывает на конец выражения, в котором содержится внутренний класс. Таким образом, в данном случае ее использование ничем не отличается от обычного.
Инициализацию также можно провести в точке определения полей безымянного класса:
//: innerclasses/Parcel9.java
// Безымянный внутренний класс, выполняющий инициализацию.
// Более короткая версия программы Parcel5.java
public class Parcel9 {
// Для использования в безымянном внутреннем классе
// аргументы должны быть неизменны (final)
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.destination("Tasmania"#41;;
}
}
Если вы определяете безымянный внутренний класс и хотите при этом использовать объекты, определенные вне этого внутреннего класса, компилятор требует, чтобы переданные на них ссылки объявлялись неизменными (final), как это сделано аргументе destination(). Без такого объявления вы получите сообщение об ошибке при компиляции программы.
Пока мы ограничиваемся простым присваиванием значений полям, указанный подход работает. А если понадобится выполнить некоторые действия, свойственные конструкторам? В безымянном классе именованный конструктор определить нельзя (раз у самого класса нет имени!), но инициализация экземпляра (instance initialization) фактически позволяет добиться желаемого эффекта:
//: innerclasses/AnonymousConstructor.java
// Создание конструктора для безымянного внутреннего класса.
import static net.mindview.util.Print.*;
abstract class Base {
public Base(int i) {
print("Base constructor, i = " + i);
}
public abstract void f();
}
public class AnonymousConstructor {
public static Base getBase(int i) {
return new Base(i) {
{ print("Inside instance initializer"); }
public void f() {
print("In anonymous f()");
}
};
}
public static void main(String[] args) {
Base base = getBase(47);
base.f();
}
}
<spoiler text="Output:">
Base constructor, i = 47
Inside instance initializer
In anonymous f()
</spoiler>
В таком случае переменная і не обязана быть неизменной (final). И хотя і передается базовому конструктору безымянного класса, она никогда не используется напрямую внутри безымянного класса.
Вернемся к нашим объектам Parcel, на этот раз выполнив для них инициализацию экземпляра. Отметьте, что параметры метода destination() должны быть объявлены неизменными, так как они используются внутри безымянного класса:
//: innerclasses/Parcel10.java
// Демонстрация "инициализации экземпляра" для
// конструирования безымянного внутреннего класса
public class Parcel10 {
public Destination
destination(final String dest, final float price) {
return new Destination() {
private int cost;
// Инициализация экземпляра для каждого объекта:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel10 p = new Parcel10();
Destination d = p.destination("Tasmania", 101.395F);
}
}
<spoiler text="Output:">
Over budget!
</spoiler>
Внутри инициализатора экземпляра виден код, недоступный при инициализации полей (то есть команда if). Поэтому инициализатор экземпляра фактически является конструктором безымянного внутреннего класса. Конечно, возможности его ограничены; перегружать такой инициализатор нельзя, и поэтому он будет присутствовать в классе только в единственном числе.
Возможности безымянных внутренних классов несколько ограничены по сравнению с обычным наследованием — они могут либо расширять класс, либо реализовывать интерфейс, но не то и другое одновременно. А если вы выберете второй вариант, реализовать можно только один интерфейс.
Снова о методе-фабрике
Посмотрите, насколько приятнее выглядит пример interfaces/Factories.java при использовании безымянных внутренних классов:
//: innerclasses/Factories.java
import static net.mindview.util.Print.*;
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
private Implementation1() {}
public void method1() {print("Implementation1 method1");}
public void method2() {print("Implementation1 method2");}
public static ServiceFactory factory =
new ServiceFactory() {
public Service getService() {
return new Implementation1();
}
};
}
class Implementation2 implements Service {
private Implementation2() {}
public void method1() {print("Implementation2 method1");}
public void method2() {print("Implementation2 method2");}
public static ServiceFactory factory =
new ServiceFactory() {
public Service getService() {
return new Implementation2();
}
};
}
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(Implementation1.factory);
// Реализации полностью взаимозаменяемы;
serviceConsumer(Implementation2.factory);
}
}
<spoiler text="Output:">
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2
</spoiler>
Теперь конструкторы Implementation1 и Implementation2 могут быть закрытыми, и фабрику необязательно оформлять в виде именованного класса. Кроме того, часто бывает достаточно одного фабричного объекта, поэтому в данном случае он создается как статическое поле в реализации Service. Наконец, итоговый синтаксис выглядит более осмысленно.
Пример interfaces/Games.java тоже можно усовершенствовать с помощью безымянных внутренних классов:
//: innerclasses/Games.java
// Использование анонимных внутренних классов в библиотеке Game
import static net.mindview.util.Print.*;
interface Game { boolean move(); }
interface GameFactory { Game getGame(); }
class Checkers implements Game {
private Checkers() {}
private int moves = 0;
private static final int MOVES = 3;
public boolean move() {
print("Checkers move " + moves);
return ++moves != MOVES;
}
public static GameFactory factory = new GameFactory() {
public Game getGame() { return new Checkers(); }
};
}
class Chess implements Game {
private Chess() {}
private int moves = 0;
private static final int MOVES = 4;
public boolean move() {
print("Chess move " + moves);
return ++moves != MOVES;
}
public static GameFactory factory = new GameFactory() {
public Game getGame() { return new Chess(); }
};
}
public class Games {
public static void playGame(GameFactory factory) {
Game s = factory.getGame();
while(s.move())
;
}
public static void main(String[] args) {
playGame(Checkers.factory);
playGame(Chess.factory);
}
}
<spoiler text="Output:">
Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2
</spoiler>
Вспомните совет, данный в конце предыдущей главы: отдавать предпочтение классам перед интерфейсами. Если архитектура системы требует применения интерфейса, вы это поймете. В остальных случаях не применяйте интерфейсы без крайней необходимости.
Вложенные классы
Если связь между объектом внутреннего класса и объектом внешнего класса не нужна, можно сделать внутренний класс статическим (объявить его как static). Часто такой класс называют вложенным (nested).
Чтобы понять смысл ключевого слова static в отношении внутренних классов, следует вспомнить, что в объекте обычного внутреннего класса тайно хранится ссылка на объект создавшего его объемлющего внешнего класса. При использовании статического внутреннего класса такой ссылки не существует. Применение статического внутреннего класса означает следующее:
- для создания объекта статического внутреннего класса не нужен объект внешнего класса;
- из объекта вложенного класса нельзя обращаться к нестатическим членам внешнего класса.
Есть и еще одно различие между вложенными и обычными внутренними классами. Поля и методы обычного внутреннего класса определяются только на уровне внешнего класса, поэтому обычные внутренние классы не могут содержать статические данные, поля и классы. Но вложенные классы не имеют таких ограничений:
//: innerclasses/Parcel11.java
// Вложенные (статические внутренние) классы
public class Parcel11 {
private static class ParcelContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected static class ParcelDestination
implements Destination {
private String label;
private ParcelDestination(String whereTo) {
label = whereTo;
}
public String readLabel() { return label; }
// Вложенные классы могут содержать другие статические элементы;
public static void f() {}
static int x = 10;
static class AnotherLevel {
public static void f() {}
static int x = 10;
}
}
public static Destination destination(String s) {
return new ParcelDestination(s);
}
public static Contents contents() {
return new ParcelContents();
}
public static void main(String[] args) {
Contents c = contents();
Destination d = destination("Tasmania");
}
}
В методе main() не требуется объекта класса Parcel11; вместо этого для вызова методов, возвращающих ссылки на Contents и Destination, используется обычный синтаксис обращения к статическим членам класса.
Как было сказано ранее, в обычном (не-статическом) внутреннем классе для обращения к объекту внешнего класса используется специальная ссылка this. Во вложенном классе такая ссылка недействительна (по аналогии со статическими методами).
Классы внутри интерфейсов
Обычно интерфейс не может содержать программный код, но вложенный класс может стать его частью. Любой класс, размещенный внутри интерфейса, автоматически является public и static. Так как класс является статическим, он не нарушает правил обращения с интерфейсом — этот вложенный класс просто использует пространство имен интерфейса. Во внутреннем классе даже можно реализовать окружающий интерфейс:
//: innerclasses/ClassInInterface.java
// {main: ClassInInterface$Test}
public interface ClassInInterface {
void howdy();
class Test implements ClassInInterface {
public void howdy() {
System.out.println("Howdy!");
}
public static void main(String[] args) {
new Test().howdy();
}
}
}
<spoiler text="Output:">
Howdy!
</spoiler>
Вложение классов в интерфейсы может пригодиться для создания обобщенного кода, используемого с разными реализациями этого интерфейса.
Ранее в книге я предлагал помещать в каждый класс метод main(), позволяющий при необходимости протестировать данный класс. Недостатком такого подхода является дополнительный скомпилированный код, увеличивающий размеры программы. Если для вас это нежелательно, используйте статический внутренний класс для хранения тестового кода:
//: innerclasses/TestBed.java
// Помещение тестового кода во вложенный класс.
// {main: TestBed$Tester}
public class TestBed {
public void f() { System.out.println("f()"); }
public static class Tester {
public static void main(String[] args) {
TestBed t = new TestBed();
t.f();
}
}
}
<spoiler text="Output:">
f()
</spoiler>
При компиляции этого файла создается отдельный класс с именем TestBed$Tester (для запуска тестового кода наберите команду java TestBed$Tester). Вы можете использовать этот класс для тестирования, но включать его в окончательную версию программы необязательно; файл TestBed$Tester.class можно просто удалить перед окончательной сборкой программы.
Доступ вовне из многократно вложенных классов
Независимо от глубины вложенности, внутренний класс всегда может напрямую обращаться ко всем членам всех классов, в которые он встроен. Следующая программа демонстрирует этот факт:
//: innerclasses/MultiNestingAccess.java
// Вложенные классы могут обращаться ко всем членам всех
// классов, в которых они находятся.
class MNA {
private void f() {}
class A {
private void g() {}
public class B {
void h() {
g();
f();
}
}
}
}
public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
Как видно из примера, в классе MNA.A.B методы f() и g() вызываются без дополнительных описаний (несмотря на то, что они объявлены как private). Этот пример также демонстрирует синтаксис, который следует использовать при создании объектов внутренних классов произвольного уровня вложенности из другого класса. Синтаксис .new обеспечивает правильную область действия, и вам не приходится уточнять имя класса при вызове конструктора.
Внутренние классы: зачем?
К настоящему моменту мы подробно рассмотрели синтаксис и семантику работы внутренних классов, но это не дало ответа на вопрос, зачем они вообще нужны.
Что же заставило создателей Java добавить в язык настолько фундаментальное свойство?
Обычно внутренний класс наследует от класса или реализует интерфейс, а код внутреннего класса манипулирует объектом внешнего класса, в котором он был создан. Значит, можно сказать, что внутренний класс — это нечто вроде «окна» во внешний класс.
Возникает резонный вопрос: «Если мне понадобится ссылка на интерфейс, почему бы внешнему классу не реализовать этот интерфейс?» Ответ: «Если это все, что вам нужно, — значит, так и следует поступить». Но что же отличает внутренний класс, реализующий интерфейс, от внешнего класса, реализующего тот же интерфейс? Далеко не всегда удается использовать удобство интерфейсов — иногда приходится работать и с реализацией. Поэтому наиболее веская причина для использования внутренних классов такова:
Каждый внутренний класс способен независимо наследовать определенную реализацию. Таким образом, внутренний класс не ограничен при наследовании в ситуациях, где внешний класс уже наследует реализацию.
Без возможности внутренних классов наследовать реализацию более чем одного реального или абстрактного класса некоторые задачи планирования и программирования становятся практически неразрешимыми. Поэтому внутренний класс выступает как «довесок» решения проблемы множественного наследования. Интерфейсы берут на себя часть этой задачи, тогда как внутренние классы фактически обеспечивают «множественное наследование реализации». Другими словами, внутренние классы позволяют наследовать от нескольких не-интерфейсов.
Чтобы понять все сказанное до конца, рассмотрим ситуацию, где два интерфейса тем или иным способом должны быть реализованы в классе. Вследствие гибкости интерфейсов возможен один из двух способов решения: отдельный одиночный класс или внутренний класс:
//: innerclasses/MultiInterfaces.java
// Два способа реализации нескольких интерфейсов.
package innerclasses;
interface A {}
interface B {}
class X implements A, B {}
class Y implements A {
B makeB() {
// Безымянный внутренний класс:
return new B() {};
}
}
public class MultiInterfaces {
static void takesA(A a) {}
static void takesB(B b) {}
public static void main(String[] args) {
X x = new X();
Y y = new Y();
takesA(x);
takesA(y);
takesB(x);
takesB(y.makeB());
}
}
Конечно, выбор того или иного способа организации кода зависит от конкретной ситуации. Впрочем, сама решаемая вами задача должна подсказать, что для нее предпочтительно: один отдельный класс или внутренний класс.
Но при отсутствии других ограничений оба подхода, использованные в рассмотренном примере, ничем не отличаются с точки зрения реализации. Оба они работают.
Но если вместо интерфейсов имеются реальные или абстрактные классы и новый класс должен как-то реализовать функциональность двух других, придется прибегнуть к внутренним классам:
//: innerclasses/MultiImplementation.java
// При использовании реальных или абстрактных классов
// "множественное наследование реализации" возможно
// только с применением внутренних классов
package innerclasses;
class D {}
abstract class E {}
class Z extends D {
E makeE() { return new E() {}; }
}
public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args) {
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
}
Если нет необходимости решать задачу «множественного наследования реализации», скорее всего, вы без особого труда напишите программу, не прибегая к особенностям внутренних классов. Однако внутренние классы открывают перед вами ряд дополнительных возможностей:
- У внутреннего класса может существовать произвольное количество экземпляров, каждый из которых обладает собственной информацией состояния, не зависящей от состояния объекта внешнего класса.
- Один внешний класс может содержать несколько внутренних классов, по-разному реализующих один и тот же интерфейс или наследующих от единого базового класса. Вскоре мы рассмотрим пример такой конструкции.
- Место создания объекта внутреннего класса не привязано к месту и времени создания объекта внешнего класса.
- Внутренний класс не использует тип отношений классов «является тем-то», способных вызвать недоразумения; он представляет собой отдельную сущность.
Например, если бы в программе Sequence.java отсутствовали внутренние классы, пришлось бы заявить, что «класс Sequence есть класс Selector», и при этом ограничиться только одним объектом Selector для конкретного объекта Sequence. А вы можете с легкостью определить второй метод, reverseSelector(), создающий объект Selector для перебора элементов Sequence в обратном порядке. Такую гибкость обеспечивают только внутренние классы.
Замыкания и обратные вызовы
Замыканием (closure) называется вызываемый объект, который сохраняет информацию о контексте, он был создан. Из этого определения видно, что внутренний класс является объектно-ориентированным замыканием, поскольку он не только содержит информацию об объекте внешнего класса («место создания»), но к тому же располагает ссылкой на весь объект внешнего класса, с помощью которой он может манипулировать всеми членами этого объекта, в том числе и закрытыми (private).
При обсуждении того, стоит ли включать в Java некое подобие указателей, самым веским аргументом «за» была возможность обратных вызовов (callback). В механизме обратного вызова некоторому стороннему объекту передается информация, позволяющая ему затем обратиться с вызовом к объекту, который произвел изначальный вызов.
Это очень мощная концепция программирования, к которой мы еще вернемся. С другой стороны, при реализации обратного вызова на основе указателей вся ответственность за его правильное использование возлагается на программиста. Как было показано ранее, язык Java ориентирован на безопасное программирование, поэтому указатели в него включены не были.
Замыкание, предоставляемое внутренним классом, — хорошее решение, гораздо более гибкое и безопасное, чем указатель. Рассмотрим пример:
//: innerclasses/Callbacks.java
// Использование внутренних классов
// для реализации обратных вызовов
package innerclasses;
import static net.mindview.util.Print.*;
interface Incrementable {
void increment();
}
// Простая реализация интерфейса:
class Callee1 implements Incrementable {
private int i = 0;
public void increment() {
i++;
print(i);
}
}
class MyIncrement {
public void increment() { print("Other operation"); }
static void f(MyIncrement mi) { mi.increment(); }
}
// Если класс должен вызывать метод increment()
// по-другому, необходимо использовать внутренний класс:
class Callee2 extends MyIncrement {
private int i = 0;
public void increment() {
super.increment();
i++;
print(i);
}
private class Closure implements Incrementable {
public void increment() {
// Указывается метод внешнего класса;
// в противном случае возникает бесконечная рекурсия.
Callee2.this.increment();
}
}
Incrementable getCallbackReference() {
return new Closure();
}
}
class Caller {
private Incrementable callbackReference;
Caller(Incrementable cbh) { callbackReference = cbh; }
void go() { callbackReference.increment(); }
}
public class Callbacks {
public static void main(String[] args) {
Callee1 c1 = new Callee1();
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2.getCallbackReference());
caller1.go();
caller1.go();
caller2.go();
caller2.go();
}
}
<spoiler text="Output:">
Other operation
1
1
2
Other operation
2
Other operation
3
</spoiler>
Этот пример также демонстрирует различия между реализацией интерфейса внешним или внутренним классом. Класс Callee1 — наиболее очевидное решение задачи с точки зрения программирования.
Класс Callee2 наследует от класса MyIncrement, в котором уже есть метод increment), выполняющий действие, никак не связанное с тем, что ожидает от него интерфейс Incrementable. Когда класс MyIncrement наследуется в Callee2, метод increment() нельзя переопределить для использования в качестве метода интерфейса Incrementable, поэтому нам приходится предоставлять отдельную реализацию во внутреннем классе. Также отметьте, что создание внутреннего класса не затрагивает и не изменяет существующий интерфейс внешнего класса.
Все элементы, за исключением метода getCallbackReference(), в классе Callee2 являются закрытыми. Для любой связи с окружающим миром необходим интерфейс Incrementable. Здесь мы видим, как интерфейсы позволяют полностью отделить интерфейс от реализации.
Внутренний класс Closure просто реализует интерфейс Incrementable, предоставляя при этом связь с объектом Callee2 — но связь эта безопасна. Кто бы ни получил ссылку на Incrementable, он в состоянии вызвать только метод increment(), и других возможностей у него нет (в отличие от указателя, с которым программист может вытворять все, что угодно).
Класс Caller получает ссылку на Incrementable в своем конструкторе (хотя передача ссылки для обратного вызова может происходить в любое время), а после этого использует ссылку для «обратного вызова» объекта Callee.
Главным достоинством обратного вызова является его гибкость — вы можете динамически выбирать функции, выполняемые во время работы программы.
Внутренние классы и система управления
В качестве более реального пример использования внутренних классов мы рассмотрим то, что я буду называть здесь системой управления (control framework).
Каркас приложения (application framework) — это класс или набор классов, разработанных для решения определенного круга задач. При работе с каркасами приложений обычно используется наследование от одного или нескольких классов, с переопределением некоторых методов. Код переопределенных методов адаптирует типовое решение, предоставляемое каркасом приложения, к вашим конкретным потребностям.
Система управления представляет собой определенный тип каркаса приложения, основным движущим механизмом которого является обработка событий. Такие системы называются системами, управляемыми по событиям (event-driven system). Одной из самых типичных задач в прикладном программировании является создание графического интерфейса пользователя (GUI), всецело и полностью ориентированного на обработку событий.
Чтобы на наглядном примере увидеть, как с применением внутренних классов достигается простота создания и использования библиотек, мы рассмотрим систему, ориентированную на обработку событий по их «готовности». Хотя в практическом смысле под «готовностью» может пониматься все, что угодно, в нашем случае она будет определяться по показаниям счетчика времени. Далее приводится общее описание управляющей системы, никак не зависящей от того, чем именно она управляет. Нужная информация предоставляется посредством наследования, при реализации метода action().
Начнем с определения интерфейса, описывающего любое событие системы. Вместо интерфейса здесь используется абстрактный класс, поскольку по умолчанию управление координируется по времени, а следовательно, присутствует частичная реализация:
//: innerclasses/controller/Event.java
// Общие для всякого управляющего события методы.
package innerclasses.controller;
public abstract class Event {
private long eventTime;
protected final long delayTime;
public Event(long delayTime) {
this.delayTime = delayTime;
start();
}
public void start() { // Позволяет перезапуск
eventTime = System.nanoTime() + delayTime;
}
public boolean ready() {
return System.nanoTime() >= eventTime;
}
public abstract void action();
}
Конструктор просто запоминает время (от момента создания объекта), через которое должно выполняться событие Event, и после этого вызывает метод start(), который прибавляет к текущему времени интервал задержки, чтобы вычислить время возникновения события. Метод start() отделен от конструктора, благодаря чему становится возможным «перезапуск» события после того, как его время уже истекло; таким образом, объект Event можно использовать многократно. Скажем, если вам понадобится повторяющееся событие, достаточно добавить вызов start() в метод action().
Метод ready() сообщает, что пора действовать — вызывать метод action(). Конечно, метод ready() может быть переопределен любым производным классом, если событие Event активизируется не по времени, а по иному условию.
Следующий файл описывает саму систему управления, которая распоряжается событиями и инициирует их. Объекты Event содержатся в контейнере List<Event>. На данный момент достаточно знать, что метод add() присоединяет объект Event к концу контейнера с типом List, метод size() возвращает количество элементов в контейнере, синтаксис foreach() осуществляет последовательную выборку элементов List, а метод remove() удаляет заданный элемент из контейнера:
//: innerclasses/controller/Controller.java
// Обобщенная система управления
package innerclasses.controller;
import java.util.*;
public class Controller {
// Класс из пакета java.util для хранения событий Event::
private List<Event> eventList = new ArrayList<Event>();
public void addEvent(Event c) { eventList.add(c); }
public void run() {
while(eventList.size() > 0)
// Make a copy so you're not modifying the list
// while you're selecting the elements in it:
for(Event e : new ArrayList<Event>(eventList))
if(e.ready()) {
System.out.println(e);
e.action();
eventList.remove(e);
}
}
}
Метод run() в цикле перебирает копию eventList в поисках событий Event, готовых для выполнения. Для каждого найденного элемента он выводит информацию об объекте методом toString(), вызывает метод action(), а после этого удаляет событие из списка.
Заметьте, что в этой архитектуре совершенно неважно, что конкретно выполняет некое событие Event. В этом и состоит «изюминка» разработанной системы; она отделяет постоянную составляющую от изменяющейся. «Вектором изменения» являются различные действия разнообразных событий Event, выражаемые посредством создания разных субклассов Event.
На этом этапе в дело вступают внутренние классы. Они позволяют добиться двух целей:
- Вся реализация системы управления создается в одном классе, с полной инкапсуляцией всей специфики данной реализации. Внутренние классы используются для представления различных разновидностей action(), необходимых для решения задачи.
- Внутренние классы помогают избежать громоздкой, неудобной реализации, так как у них есть доступ к внешнему классу. Без этой возможности программный код очень быстро станет настолько неприятным, что вам захочется поискать другие альтернативы.
Рассмотрим конкретную реализацию системы управления, разработанную для управления функциями оранжереи. Все события — включение света, воды и нагревателей, звонок и перезапуск системы — абсолютно разнородны. Однако система управления разработана так, что различия в коде легко изолируются. Внутренние классы помогают унаследовать несколько производных версий одного базового класса Event в пределах одного класса. Для каждого типа события от Event наследуется новый внутренний класс, и в его реализации action() записывается управляющий код.
Как это обычно бывает при использовании каркасов приложений, класс GreenhouseControls наследует от класса Controller:
//: innerclasses/GreenhouseControls.java
// Пример конкретного приложения на основе системы
// управления, все находится в одном классе. Внутренние
// классы дают возможность инкапсулировать различную
// функциональность для каждого отдельного события
import innerclasses.controller.*;
public class GreenhouseControls extends Controller {
private boolean light = false;
public class LightOn extends Event {
public LightOn(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов
// физическое включение света
light = true;
}
public String toString() { return "Light is on"; }
}
public class LightOff extends Event {
public LightOff(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов
// физическое выключение света
light = false;
}
public String toString() { return "Light is off"; }
}
private boolean water = false;
public class WaterOn extends Event {
public WaterOn(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов.
// выключения системы полива
water = true;
}
public String toString() {
return "Greenhouse water is on";
}
}
public class WaterOff extends Event {
public WaterOff(long delayTime) { super(delayTime); }
public void action() {
// Сюда помещается аппаратный вызов.
// выключения системы полива
water = false;
}
public String toString() {
return "Greenhouse water is off";
}
}
private String thermostat = "Day";
public class ThermostatNight extends Event {
public ThermostatNight(long delayTime) {
super(delayTime);
}
public void action() {
// Сюда помещается аппаратный вызов.
// thermostat = "Ночь";
thermostat = "Night";
}
public String toString() {
return "Thermostat on night setting";
}
}
public class ThermostatDay extends Event {
public ThermostatDay(long delayTime) {
super(delayTime);
}
public void action() {
// Сюда помещается аппаратный вызов.
// thermostat = "День"
thermostat = "Day";
}
public String toString() {
return "Thermostat on day setting";
}
}
// Пример метода action(), вставляющего
// самого себя в список событий.
public class Bell extends Event {
public Bell(long delayTime) { super(delayTime); }
public void action() {
addEvent(new Bell(delayTime));
}
public String toString() { return "Bing!"; }
}
public class Restart extends Event {
private Event[] eventList;
public Restart(long delayTime, Event[] eventList) {
super(delayTime);
this.eventList = eventList;
for(Event e : eventList)
addEvent(e);
}
public void action() {
for(Event e : eventList) {
e.start(); // Перезапуск каждый раз
addEvent(e);
}
start(); // Возвращаем это событие Event
addEvent(this);
}
public String toString() {
return "Restarting system";
}
}
public static class Terminate extends Event {
public Terminate(long delayTime) { super(delayTime); }
public void action() { System.exit(0); }
public String toString() { return "Terminating"; }
}
}
Заметьте, что поля light, thermostat и ring принадлежат внешнему классу GreenhouseControls, и все же внутренние классы имеют возможность обращаться к ним, не используя особой записи и не запрашивая особых разрешений. Большинство методов action() требует управления оборудованием оранжереи, что, скорее всего, привлечет в программу сторонние низкоуровневые вызовы.
В основном классы Event похожи друг на друга, однако классы Bell и Restart представляют собой особые случаи. Bell выдает звуковой сигнал и добавляет себя в список событий, чтобы звонок позднее сработал снова. Заметьте, что внутренние классы действуют почти как множественное наследование: классы Bell и Restart имеют доступ ко всем методам класса Event, а также ко всем методам внешнего класса GreenhouseControls.
Классу Restart передается массив объектов Event, которые он добавляет в контроллер. Так как Restart также является объектом Event, вы можете добавить этот объект в список событий в методе Restart.action(), чтобы система регулярно перезапускалась.
Следующий класс настраивает систему, создавая объект GreenhouseControls и добавляя в него разнообразные типы объектов Event. Это пример шаблона проектирования «команда» — каждый объект в EventList представляет собой запрос, инкапсулированный в объекте:
//: innerclasses/GreenhouseController.java
// Configure and execute the greenhouse system.
// {Args: 5000}
import innerclasses.controller.*;
public class GreenhouseController {
public static void main(String[] args) {
GreenhouseControls gc = new GreenhouseControls();
// Вместо жесткого кодирования фиксированных данных
// можно было бы считать информацию для настройки
// из текстового файла:
gc.addEvent(gc.new Bell(900));
Event[] eventList = {
gc.new ThermostatNight(0),
gc.new LightOn(200),
gc.new LightOff(400),
gc.new WaterOn(600),
gc.new WaterOff(800),
gc.new ThermostatDay(1400)
};
gc.addEvent(gc.new Restart(2000, eventList));
if(args.length == 1)
gc.addEvent(
new GreenhouseControls.Terminate(
new Integer(args[0])));
gc.run();
}
}
<spoiler text="Output:">
Bing!
Thermostat on night setting
Light is on
Light is off
Greenhouse water is on
Greenhouse water is off
Thermostat on day setting
Restarting system
Terminating
</spoiler>
Класс инициализирует систему, включая в нее нужные события. Если передать программе параметр командной строки, она завершается по истечении заданного количества миллисекунд (используется при тестировании). Конечно, чтобы программа стала более гибкой, описания событий следовало бы не включать в программный код, а загружать из файла.
Этот пример поможет понять всю ценность механизма внутренних классов, особенно в случае с системами управления.
Наследование от внутренних классов
Так как конструктор внутреннего класса связывается со ссылкой на окружающий внешний объект, наследование от внутреннего класса получается чуть сложнее, чем обычное. Проблема состоит в том, что «скрытая» ссылка на объект объемлющего внешнего класса должна быть инициализирована, а в производном классе больше не существует объемлющего объекта по умолчанию. Для явного указания объемлющего внешнего объекта применяется специальный синтаксис:
//: innerclasses/InheritInner.java
// Наследование от внутреннего класса.
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // He компилируется
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
Здесь класс InheritInner расширяет только внутренний класс, а не внешний. Но когда дело доходит до создания конструктора, предлагаемый по умолчанию конструктор не подходит, и вы не можете просто передать ссылку на внешний объект. Необходимо включить в тело конструктора выражение
ссылкаНаОбъемлющийКласс.super();
в теле конструктора. Оно обеспечит недостающую ссылку, и программа откомпилируется.
Можно ли переопределить внутренний класс?
Что происходит, если вы создаете внутренний класс, затем наследуете от его внешнего класса, а после этого заново описываете внутренний класс в производном классе? Другими словами, можно ли переопределить внутренний класс? Это было бы довольно интересно, но «переопределение» внутреннего класса, как если бы он был еще одним методом внешнего класса, фактически не имеет никакого эффекта:
//: innerclasses/BigEgg.java
// Внутренний класс нельзя переопределить
// подобно обычному методу,
import static net.mindview.util.Print.*;
class Egg {
private Yolk y;
protected class Yolk {
public Yolk() { print("Egg.Yolk()"); }
}
public Egg() {
print("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg {
public class Yolk {
public Yolk() { print("BigEgg.Yolk()"); }
}
public static void main(String[] args) {
new BigEgg();
}
}
<spoiler text="Output:">
New Egg()
Egg.Yolk()
</spoiler>
Конструктор по умолчанию автоматически синтезируется компилятором, а в нем вызывается конструктор по умолчанию из базового класса. Можно подумать, что при создании объекта BigEgg должен использоваться «переопределенный» класс Yolk, но это отнюдь не так, как видно из результата работы программы.
Этот пример просто показывает, что при наследовании от внешнего класса ничего особенного с внутренними классами не происходит. Два внутренних класса — совершенно отдельные составляющие, с независимыми пространствами имен. Впрочем, возможность явного наследования от внутреннего класса сохранилась:
//: innerclasses/BigEgg2.java
// Правильное наследование внутреннего класса,
import static net.mindview.util.Print.*;
class Egg2 {
protected class Yolk {
public Yolk() { print("Egg2.Yolk()"); }
public void f() { print("Egg2.Yolk.f()");}
}
private Yolk y = new Yolk();
public Egg2() { print("New Egg2()"); }
public void insertYolk(Yolk yy) { y = yy; }
public void g() { y.f(); }
}
public class BigEgg2 extends Egg2 {
public class Yolk extends Egg2.Yolk {
public Yolk() { print("BigEgg2.Yolk()"); }
public void f() { print("BigEgg2.Yolk.f()"); }
}
public BigEgg2() { insertYolk(new Yolk()); }
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
}
<spoiler text="Output:">
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()
</spoiler>
Теперь класс BigEgg2.Yolk явно расширяет класс Egg2.Yolk и переопределяет его методы. Метод insertYolk() позволяет классу BigEgg2 повысить один из своих объектов Yolk до ссылки у в классе Egg2, поэтому при вызове y.f() в методе g() используется переопределенная версия f(). Второй вызов Egg2.Yolk() — это вызов конструктора базового класса из конструктора класса BigEgg2.Yolk. Мы также видим, что при вызове метода g() используется «обновленная» версия метода.
Локальные внутренние классы
Как было замечено ранее, внутренние классы также могут создаваться в блоках кода — чаще всего в теле метода. Локальный внутренний класс не может иметь спецификатора доступа, так как он не является частью внешнего класса, но для него доступны все неизменные (final) переменные текущего блока и все члены внешнего класса. Следующий пример сравнивает процессы создания локального внутреннего класса и безымянного внутреннего класса:
//: innerclasses/LocalInnerClass.java
// Хранит последовательность объектов
import static net.mindview.util.Print.*;
interface Counter {
int next();
}
public class LocalInnerClass {
private int count = 0;
Counter getCounter(final String name) {
// Локальный внутренний класс:
class LocalCounter implements Counter {
public LocalCounter() {
// У локального внутреннего класса
// может быть собственный конструктор:
print("LocalCounter()");
}
public int next() {
printnb(name); // Access local final
return count++;
}
}
return new LocalCounter();
}
// To же самое с безымянным внутренним классом:
Counter getCounter2(final String name) {
return new Counter() {
// У безымянного внутреннего класса не может быть
// именованного конструктора, «легальна» только
// инициализация экземпляром:
{
print("Counter()");
}
public int next() {
printnb(name); // final аргумент
return count++;
}
};
}
public static void main(String[] args) {
LocalInnerClass lic = new LocalInnerClass();
Counter
c1 = lic.getCounter("Local inner "),
c2 = lic.getCounter2("Anonymous inner ");
for(int i = 0; i < 5; i++)
print(c1.next());
for(int i = 0; i < 5; i++)
print(c2.next());
}
}
<spoiler text="Output:">
LocalCounter()
Counter()
Local inner 0
Local inner 1
Local inner 2
Local inner 3
Local inner 4
Anonymous inner 5
Anonymous inner 6
Anonymous inner 7
Anonymous inner 8
Anonymous inner 9
</spoiler>
Объект Counter возвращает следующее по порядку значение. Он реализован и как локальный класс, и как безымянный внутренний класс, с одинаковым поведением и характеристиками. Поскольку имя локального внутреннего класса недоступно за пределами метода, доводом для применения локального класса вместо безымянного внутреннего может быть необходимость в именованном
конструкторе и (или) перегруженных конструкторах; безымянные внутренние классы допускают только инициализацию экземпляром.
Другая причина для использования локального внутреннего класса вместо безымянного внутреннего — необходимость создания более чем одного объекта такого класса.
Идентификаторы внутренних классов
Так как каждый класс компилируется в файл с расширением .class, содержащий полную информацию о создании его экземпляров (эта информация помещается в «мета-класс», называемый объектом Class), напрашивается предположение, что внутренние классы также создают файлы .class для хранения информации о своих объектах Class. Имена этих файлов-классов строятся по жестко заданной схеме: имя объемлющего внешнего класса, затем символ $ и имя внутреннего класса. Например, для программы LocallnnerClass.java создаются следующие файлы с расширением .class:
Counter.class
LocalInnerClass$2.class
LocalInnerClass$lLocalCounter.class
LocalInnerClass.class
Если внутренние классы являются безымянными, компилятор использует в качестве их идентификаторов номера. Если внутренние классы вложены в другие внутренние классы, их имена просто присоединяются после символа $ и идентификаторов всех внешних классов.
Хотя такая схема построения внутренних имен проста и прямолинейна, она вполне надежна и работает практически в любых ситуациях. Так как она является стандартной для языка Java, все получаемые файлы автоматически становятся платформно-независимыми.
Резюме
Интерфейсы и внутренние классы — весьма нетривиальные концепции, и во многих других объектно-ориентированных языках вы их не найдете. Например, в C++ нет ничего похожего. Вместе они решают те задачи, которые C++ пытается решить с применением множественного наследования. Однако множественное наследование C++ создает массу проблем; по сравнению с ним интерфейсы и внутренние классы Java гораздо более доступны.
Хотя сами по себе эти механизмы не так уж сложны, решение об их использовании принимается на уровне проектирования (как и в случае с полиморфизмом). Со временем вы научитесь сразу оценивать, где большую выгоду даст интерфейс, где внутренний класс, а где нужны обе возможности сразу. А пока достаточно хотя бы в общих чертах ознакомиться с их синтаксисом и семантикой.
------------------------
ТРИО теплый пол отзыв
Заработок на сокращении ссылок
Earnings on reducing links
Код PHP на HTML сайты
Категория: Книги по Java
Комментарии |