39

39


print("Chess move " + moves); return ++moves != MOVES;



}



public static GameFactory factory = new GameFactoryO { public Game getGameO { return new ChessO; }



}:



}



public class Games {



public static void piayGame(GameFactory factory) { Game s = factory.getGameO; while(s. moveO)



}



public static void main(String[] args) { pi ayGame(Checkers.factory); piayGame(Chess.factory);



}



} /* Output: Checkers move 0 Checkers move 1 Checkers move 2 Chess move 0 Chess move 1 Chess move 2 Chess move 3 *///•-



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



public boolean moveО {



print("Checkers move " + moves); return ++moves != MOVES;



}



public static GameFactory factory = new GameFactoryO { public Game getGameO { return new Checkers О; }






Вложенные классы






Если связь между объектом внутреннего класса и объектом внешнего класса не нужна, можно сделать внутренний класс статическим (объявить его как static). Часто такой класс называютвложенным[2]
(nested). Чтобы понять смысл ключевого



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



•       для создания объекта статического внутреннего класса не нужен объект внешнего класса;



•       из объекта вложенного класса нельзя обращаться к не-статическим чле­нам внешнего класса.



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



//• i nnerclasses/Parcel 11.java



// Вложенные (статические внутренние) классы



public class Parcel 11 {



private static class PContents implements Contents { private int i = 11; public int valueO { return i; }



}



protected static class ParcelDestination implements Destination { private String label;



private Parcel Destination(String whereTo) { label = whereTo,



}



public String readLabelO { 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 contO {



return new Parcel Contents О,



}



public static void main(String[] args) { Contents с = contentsO, Destination d = destinationC'TacMaHHfl"),



}



} ///;-



В методе main() не требуется объекта класса Parcelll; вместо этого для вызова методов, возвращающих ссылки на Contents и Destination, используется обыч­ный синтаксис обращения к статическим членам класса.



Как было сказано ранее, в обычном (не-статическом) внутреннем классе для обращения к объекту внешнего класса используется специальная ссылка this. Во вложенном классе такая ссылка недействительна (по аналогии со статиче­скими методами).



Классы внутри интерфейсов



Обычно интерфейс не может содержать программный код, но вложенный классможетстать его частью. Любой класс, размещенный внутри интерфейса, авто­матически является public и static. Так как класс является статическим, он не на­рушает правил обращения с интерфейсом — этот вложенный класс просто ис­пользует пространство имен интерфейса. Во внутреннем классе даже можно реализовать окружающий интерфейс:



//• innerclasses/ClassInlnterface.java // {main: ClassInlnterfaceSTest}



public interface Classlnlnterface { void howdyO;



class Test implements Classlnlnterface { public void howdyO {



System. out.printlnCnpHBeT!");.



}



public static void main(String[] args) { new Test О .howdyO.



}



}



} /* Output Привет! *///•-



Вложение классов в интерфейсы может пригодиться для создания обобщен­ного кода, используемого с разными реализациями этого интерфейса.



Ранее в книге я предлагал помещать в каждый класс метод main(), позволяю­щий при необходимости протестировать данный класс. Недостатком такого подхода является дополнительный скомпилированный код, увеличивающий размеры программы. Если для вас это нежелательно, используйте статический внутренний класс для хранения тестового кода:



//• innerclasses/TestBed.java



// Помещение тестового кода во вложенный класс



// {main: TestBedSTester}



public class TestBed {



public void f() { System.out.printlnC'fO"): } public static class Tester {



public static void main(String[] args) { TestBed t = new TestBedO; t.fO:



}



}



} /* Output: f() *///:-



При компиляции этого файла создается отдельный класс с именем TestBed$ Tester (для запуска тестового кода наберите команду java TestBed$Tester). Вы мо­жете использовать этот класс для тестирования, но включать его в окончатель­ную версию программы необязательно; файл TestBed$Tester.class можно просто удалить перед окончательной сборкой программы.



Доступ вовне из многократно вложенных классов



Независимо от глубины вложенности, внутренний класс всегда может нап­рямую обращаться ко всем членам всех классов, в которые он встроен. Следую­щая программа демонстрирует этот факт
1
:



//: innerclasses/MultiNestingAccess.java // Вложенные классы могут обращаться ко всем членам всех // классов, в которых они находятся.



class MNA {



private void f() {} class A {



private void g() {} public class В {



void h() {



g();



f():



}



}



}



}



public class MultiNestingAccess {



public static void main(String[] args) { MNA mna = new MNA(); MNA.A mnaa = mna.new AO; MNA.А.В mnaab = mnaa.new BO; mnaab h();



}



} ///.-



Как видно из примера, в классе MNA.A.B методы f() и д() вызываются без до­полнительных описаний (несмотря на то, что они объявлены как private). Этот пример также демонстрирует синтаксис, который следует использовать при создании объектов внутренних классов произвольного уровня вложенности из другого класса. Синтаксис .new обеспечивает правильную область действия, и вам не приходится уточнять имя класса при вызове конструктора.



Внутренние классы: зачем?



К настоящему моменту мы подробно рассмотрели синтаксис и семантику рабо­ты внутренних классов, но это не дало ответа на вопрос, зачем они вообще нужны.



Что же заставило создателей Java добавить в язык настолько фундаментальное свойство?



Обычно внутренний класс наследует от класса или реализует интерфейс, а код внутреннего класса манипулирует объектом внешнего класса, в котором он был создан. Значит, можно сказать, что внутренний класс — это нечто вроде «окна» во внешний класс.



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



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



Без возможности внутренних классов наследовать реализацию более чем од­ного реального или абстрактного класса некоторые задачи планирования и про­граммирования становятся практически неразрешимыми. Поэтому внутренний класс выступает как «довесок» решения проблемы множественного наследова­ния. Интерфейсы берут на себя часть этой задачи, тогда как внутренние классы фактически обеспечивают «множественное наследование реализации». Други­ми словами, внутренние классы позволяют наследовать от нескольких не-ин- терфейсов.



Чтобы понять все сказанное до конца, рассмотрим ситуацию, где два интер­фейса тем или иным способом должны быть реализованы в классе. Вследствие гибкости интерфейсов возможен один из двух способов решения: отдельный одиночный класс или внутренний класс:



//: innerclasses/MultiInterfaces.java



// Два способа реализации нескольких интерфейсов.



interface А {}



interface В {}



class X implements А, В {}



class Y implements A { В makeBO {



// Безымянный внутренний класс: return new ВО {};



}



}



public class MultiInterfaces { static void takesA(A a) {} static void takesB(B b) {} public static void main(String[] args) { X x = new X(); Y у = new Y(); takesA(x);






takesA(y), takesB(x), takesB(y makeBO).



}



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



Но если вместо интерфейсов имеются реальные или абстрактные классы и новый класс должен как-то реализовать функциональность двух других, при­дется прибегнуть к внутренним классам:



//: innerclasses/MultiImplementation.java // При использовании реальных или абстрактных классов // "множественное наследование реализации" возможно // только с применением внутренних классов package innerclasses;



class D {} abstract class E {}



class Z extends D {



E makeEO { 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.makeEO);



}



} ///:-



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



•       У внутреннего класса может существовать произвольное количество эк­земпляров, каждый из которых обладает собственной информацией со­стояния, не зависящей от состояния объекта внешнего класса.



•       Один внешний класс может содержать несколько внутренних классов, по-разному реализующих один и тот же интерфейс или наследующих от единого базового класса. Вскоре мы рассмотрим пример такой конст­рукции.



•       Место создания объекта внутреннего класса не привязано к месту и вре­мени создания объекта внешнего класса.



• Внутренний класс не использует тип отношений классов «является тем-то», способных вызвать недоразумения; он представляет собой от­дельную сущность.



Например, если бы в программе Sequence.java отсутствовали внутренние классы, пришлось бы заявить, что «класс Sequence есть класс Selector», и при этом ограничиться только одним объектом Selector для конкретного объекта Sequence. А вы можете с легкостью определить второй метод, reverseSelector(), создающий объект Selector для перебора элементов Sequence в обратном порядке. Такую гибкость обеспечивают только внутренние классы.



Замыкания и обратные вызовыЗамыканием(closure) называется вызываемый объект, который сохраняет ин­формацию о контексте, он был создан. Из этого определения видно, что внут­ренний класс является объектно-ориентированным замыканием, поскольку он не только содержит информацию об объекте внешнего класса («место созда­ния»), но к тому же располагает ссылкой на весь объект внешнего класса, с по­мощью которой он может манипулировать всеми членами этого объекта, в том числе и закрытыми (private).



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