Выбрать главу

public class BigEgg extends Egg { public class Yolk {

public YolkO { print("BigEgg YolkO"): }

}

public static void main(String[] args) { new BigEggO;

}

} /* Output New EggO Egg. YolkO *///•-

Конструктор по умолчанию автоматически синтезируется компилятором, а в нем вызывается конструктор по умолчанию из базового класса. Можно по­думать, что при создании объекта BigEgg должен использоваться «переопреде­ленный» класс Yolk, но это отнюдь не так, как видно из результата работы про­граммы.

Этот пример просто показывает, что при наследовании от внешнего класса ничего особенного с внутренними классами не происходит. Два внутренних класса — совершенно отдельные составляющие, с независимыми пространства­ми имен. Впрочем, возможность явного наследования от внутреннего класса со­хранилась:

//: innerclasses/BigEgg2.java

// Правильное наследование внутреннего класса,

i mport stati с net.mi ndvi ew.uti1.Pri nt.*;

class Egg2 {

protected class Yolk {

public YolkO { print("Egg2.YolkO"): } public void f() {

print("Egg2 Yolk.fO"):}

}

private Yolk у = new YolkO,                                                                            продолжение &

public Egg2() { print("New Egg2()"); } public void insertYolk(Yolk yy) { у = yy; } public void g() { y.f(); }

}

public class BigEgg2 extends Egg2 {

public class Yolk extends Egg2 Yolk {

public YolkO { print("BigEgg2.Yolk()"); }

public void f() { System.out.println("BigEgg2.Yolk.f()"); }

}

public BigEgg2() { insertYolk(new YolkO); } public static void main(String[] args) { Egg2 e2 = new BigEgg2(); e2.g();

}

} /* Output: Egg2. YolkO New Egg2() Egg2. YolkO BigEgg2. YolkO BigEgg2.Yolk.f() *///•-

Теперь класс BigEgg2.Yolk явно расширяет класс Egg2.Yolk и переопределяет его методы. Метод insertYolk() позволяет классу BigEgg2 повысить один из своих объектов Yolk до ссылки у в классе Egg2, поэтому при вызове y.f() в методе д() используется переопределенная версия f(). Второй вызов Egg2.Yolk() — это вы­зов конструктора базового класса из конструктора класса BigEgg2.Yolk. Мы так­же видим, что при вызове метода д() используется «обновленная» версия мето- даЮ.

Локальные внутренние классы

Как было замечено ранее, внутренние классы также могут создаваться в блоках кода — чаще всего в теле метода. Локальный внутренний класс не может иметь спецификатора доступа, так как он не является частью внешнего класса, но для него доступны все неизменные (final) переменные текущего блока и все члены внешнего класса. Следующий пример сравнивает процессы создания локально­го внутреннего класса и безымянного внутреннего класса:

//: innerclasses/LocalInnerClass.java // Хранит последовательность объектов, import static net.mindview.util.Print.*;

interface Counter { int nextO;

}

public class LocalInnerClass { private int count = 0; Counter getCounter(final String name) { // Локальный внутренний класс: class Local Counter implements Counter { public Local CounterО {

return new Local CounterО;

}

// To же самое с безымянным внутренним классом: Counter getCounter2(final String name) { return new CounterO {

// У безымянного внутреннего класса не может быть // именованного конструктора, «легальна» только

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

print ("Counter О");

}

public int next О {

printnb(name): // неизменный аргумент return count++:

}

public static void main(String[] args) {

LocalInnerClass lie = new LocalInnerClassO: Counter

cl = lic.getCounter(" локальный"), c2 = lic.getCounter2(" безымянный"): for(int i = 0: i < 5: i++) print(cl.nextO): for(int i = 0: i < 5: i++) print(c2.next());

}

}

} /* Output: Local CounterO CounterO локальный 0 локальный 1 локальный 2 локальный 3 локальный 4 безымянный 5 безымянный 6 безымянный 7 безымянный 8 безымянный 9 *///:-

// У локального внутреннего класса // может быть собственный конструктор: pri nt("Local Counter()");

}

public int next О {

printnb(name): // неизменный аргумент return count++;

Объект Counter возвращает следующее по порядку значение. Он реализован и как локальный класс, и как безымянный внутренний класс, с одинаковым по­ведением и характеристиками. Поскольку имя локального внутреннего класса недоступно за пределами метода, доводом для применения локального класса вместо безымянного внутреннего может быть необходимость в именованном

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

Другая причина для использования локального внутреннего класса вместо безымянного внутреннего — необходимость создания более чем одного объекта такого класса.

Идентификаторы внутренних классов

Так как каждый класс компилируется в файл с расширением .class, содержащий полную информацию о создании его экземпляров (эта информация помещается в «мета-класс», называемый объектом Class), напрашивается предположение, что внутренние классы также создают файлы .class для хранения информации о своих объектах Class. Имена этих файлов-классов строятся по жестко задан­ной схеме: имя объемлющего внешнего класса, затем символ $ и имя внутрен­него класса. Например, для программы LocallnnerClass.java создаются следую­щие файлы с расширением .class:

Counter.class

LocalInnerClass$2.class

LocalInnerClass$lLocalCounter.class

LocalInnerClass.class

Если внутренние классы являются безымянными, компилятор использует в качестве их идентификаторов номера. Если внутренние классы вложены в другие внутренние классы, их имена просто присоединяются после символа $ и идентификаторов всех внешних классов.

Хотя такая схема построения внутренних имен проста и прямолинейна, она вполне надежна и работает практически в любых ситуациях. Так как она явля­ется стандартной для языка Java, все получаемые файлы автоматически стано­вятся платформно-независимыми.

Резюме

Интерфейсы и внутренние классы — весьма нетривиальные концепции, и во мно­гих других объектно-ориентированных языках вы их не найдете. Например, в С++ нет ничего похожего. Вместе они решают те задачи, которые С++ пыта­ется решить с применением множественного наследования. Однако множест­венное наследование С++ создает массу проблем; по сравнению с ним интер­фейсы и внутренние классы Java гораздо более доступны.

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

[1] Эта концепция внутренних классов сильно отличается от концепции вложенных классов С++, кото­рые представляют собой простой механизм для сокрытия имен. Вложенные классы С++ не имеют связи с объектом-оболочкой и прав доступа к его элементам.

[2] Близкий аналог вложенных классов С++, за тем исключением, что в Java вложенные классы спо­собны обращаться к закрытым членам внешнего класса.

[3] Я всегда решал эту задачу с особым удовольствием; она впервые появилась в одной из первых моих книгС++ Inside & Out, но Java-реализация выглядит гораздо элегантнее.