©2000 by Bruce Eckel

[ Предыдущая глава ] [ Краткое описание ] [ Таблица содержания ] [ Список ] [ Следующая глава ]

Интерфейсы и внутренние классы

Интерфейсы и внутренние классы предоставляют более изощренные пути для организации и контроля над объектами в вашей системе.

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

В Главе 7, Вы узнали о ключевом слове abstract, которое позволяет вам создавать один или несколько методов в классе, которые не имеют определений, Вы предоставляете только интерфейс, а его реализация будет осуществлена уже в наследниках. Ключевое слово interface создает полностью абстрактный класс, который не предоставляет никаких реализаций ни одного своего компонента. Вы далее узнаете, что interface есть нечто большее, чем просто абстрактный класс, доведенный до конца абстракции, поскольку он позволяет вам создавать вариации C++ множественного наследования, посредством создания класса, который может быть приведен к базовому типу больше раз, чем к один.

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

Интерфейсы

Ключевое слово interface осуществляет, на шаг дальше, концепцию, реализованную в abstract. Вы можете думать, что это просто чисто abstract класс. Он позволяет создателю заложить форму (структуру) класса: имена методов, списки аргументов, возвращаемые типы, но только не тела методов. Interface также может содержать поля, но все они будут, хотя и косвенно static и final. Interface предоставляет только форму, образ, но не предоставляет его реализацию.

Interface "говорит": "Все классы, реализующие этот особый интерфейс будут выглядеть одинаково". Поэтому, любой код, использующий interface знает, какой из методов может быть вызван для этого interface, впрочем, это все. Так что interface используется в качестве установления "протокола" между классами. (Некоторые ООЯ имеют даже встроенное ключевое слово protocol, делающее то же самое действие.)

Что бы создать interface, используйте ключевое слово interface вместо ключевого слова class. Как и у класса, Вы можете добавить ключевое слово public до interface (но только если этот интерфейс определен в файле с тем же именем) или оставить его пустым, тогда он станет "friendly" и его можно будет использовать только членам одного с ним пакета.

Для создания класса согласованного с особенным interface (или группой interface-ов) используйте ключевое слово implements. Тем самым Вы объявляете "Interface это на что похож мой класс, а теперь я скажу, как он должен работать." Все остальное, кроме этого, выглядит, как наследование. Диаграмма для примера с инструментами:


Как только Вы примените interface, то этот класс сразу же становится обычным и в последствии он может быть расширен обычным способом.

Вы можете выбрать явно объявления методов в interface как public. Но они таковыми являются, даже если Вы этого и не объявляете. Так что, когда Вы реализуете interface, методы из него должны быть определены как public. В противном случае, они будут по умолчанию friendly и Вы будете ограничены в доступе к ним во время наследования, поскольку доступ будет запрещен компилятором.

Это Вы можете увидеть в измененном примере Instrument. Заметьте, что каждый метод в interface строго определен, только так компилятор и позволяет делать. В дополнение, ни один из методов в Instrument не определен как public, но они автоматически public по любому:

//: c08:music5:Music5.java
// Интерфейсы.
import java.util.*;

interface Instrument {
  // Константа времени компиляции:
  int i = 5; // static & final
  // Не могут быть получены определения методов:
  void play(); // автоматически public
  String what();
  void adjust();
}

class Wind implements Instrument {
  public void play() {
    System.out.println("Wind.play()");
  }
  public String what() { return "Wind"; }
  public void adjust() {}
}

class Percussion implements Instrument {
  public void play() {
    System.out.println("Percussion.play()");
  }
  public String what() { return "Percussion"; }
  public void adjust() {}
}

class Stringed implements Instrument {
  public void play() {
    System.out.println("Stringed.play()");
  }
  public String what() { return "Stringed"; }
  public void adjust() {}
}

class Brass extends Wind {
  public void play() {
    System.out.println("Brass.play()");
  }
  public void adjust() { 
    System.out.println("Brass.adjust()");
  }
}

class Woodwind extends Wind {
  public void play() {
    System.out.println("Woodwind.play()");
  }
  public String what() { return "Woodwind"; }
}

public class Music5 {
  // Не беспокойтесь о типе, добавленные типы
  // продолжают работать правильно:
  static void tune(Instrument i) {
    // ...
    i.play();
  }
  static void tuneAll(Instrument[] e) {
    for(int i = 0; i < e.length; i++)
      tune(e[i]);
  }
  public static void main(String[] args) {
    Instrument[] orchestra = new Instrument[5];
    int i = 0;
    // Приведение к базовому типу во время добавления в массив:
    orchestra[i++] = new Wind();
    orchestra[i++] = new Percussion();
    orchestra[i++] = new Stringed();
    orchestra[i++] = new Brass();
    orchestra[i++] = new Woodwind();
    tuneAll(orchestra);
  }
} ///:~

Этот кусок кода работает точно так же. Не имеет значения, если Вы приводите к базовому типу, к обычному классу Instrument, abstract классу Instrument, или к интерфейсу Instrument. Поведение остается одно и то же. В частности, Вы можете видеть в методе tune( ), что в нем нет никаких доказательств того, что Instrument это обычный класс или abstract класс или же интерфейс. Это и есть цель: каждый подход (принцип) дает программисту различные варианты контроля над путем создания и использования объектов.

Множественное наследование в Java

Interface это не просто более чистая форма абстрактного класса. Он имее более "высокое" применение. Поскольку interface не имеет реализации всего, что есть в нтем, то нет и массива-хранилища связанного с ним, нет ничего мешающего для комбинации нескольких интерфейсов. И это ценно, поскольку иногда вам требуется нечто: "x есть a и b и c." В C++, этот акт множественных интерфейсов называется множественное наследование, и при этом этот тип тянет за собой "прилипший" багаж, поскольку каждый тип имеет свою реализацию. В Java Вы можете осуществить то же самое, но только один из этих классов может иметь реализацию, так что проблемы, возникающие в C++, не возникают в Java, при комбинировании множества интерфейсов:


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

//: c08:Adventure.java
// Множество интерфейсов.
import java.util.*;

interface CanFight {
  void fight();
}

interface CanSwim {
  void swim();
}

interface CanFly {
  void fly();
}

class ActionCharacter {
  public void fight() {}
}

class Hero extends ActionCharacter 
    implements CanFight, CanSwim, CanFly {
  public void swim() {}
  public void fly() {}
}

public class Adventure {
  static void t(CanFight x) { x.fight(); }
  static void u(CanSwim x) { x.swim(); }
  static void v(CanFly x) { x.fly(); }
  static void w(ActionCharacter x) { x.fight(); }
  public static void main(String[] args) {
    Hero h = new Hero();
    t(h); // Treat - CanFight
    u(h); // Treat - CanSwim
    v(h); // Treat  - CanFly
    w(h); // Treat - ActionCharacter
  }
} ///:~

В этом примере, как Вы можете видеть, класс Hero комбинирует конкретный класс ActionCharacter с интерфейсами CanFight, CanSwim и CanFly. Когда Вы комбинируете именованный класс с интерфейсами, как в этом примере, этот класс должен быть указан первым, а только затем интерфейсы, в противном случае компилятор выдаст сообщение об ошибке.

Заметьте, что сигнатура fight( ) та же самая, в интерфейсе CanFight и в классе ActionCharacter, но обратите внимание, что fight( ) не определена в Hero. Правилом для интерфейса является то, что Вы можете наследовать от него, но тогда Вы получите еще один интерфейс. Если же Вы хотите создать объект нового типа, то это должен быть класс со всеми определениями. Даже в силу того, что Hero не предоставляет определения для fight( ), это определение появляется вместе с ActionCharacter. Поскольку оно предоставляется автоматически и есть возможность создать объект от Hero.

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

Запомните пожалуйста причину использования интерфейсов, кратко ее можно изложить так: возможность приведения к более, чем одному базовому типу. В дополнение вторая причина для использования интерфейсов в том же, в чем и причина использования абстрактных базовых классов: предотвращение создания объектов этого класса программистами и понимания, что это всего лишь интерфейс. Отсюда возникает вопрос: Что Вы должны использовать? Interface или abstract класс? Интерфейс дает вам преимущества абстрактного класса и преимущества интерфейса, так что если нужно создать базовый класс без любых определений методов или переменных, то Вы должны предпочесть абстрактному классу интерфейс. В действительности, если Вы знаете, что что-то собирается стать базовым классом, вашим первым решением должно быть - использовать интерфейс и если только Вы решите, что этот класс должен иметь определения методов и переменных, только тогда Вы должны изменить его на абстрактный класс или если это так необходимо на обычный класс.

Конфликты имен при комбинировании интерфейсов

Вы можете столкнуться с небольшой ловушкой при реализации множественных интерфейсов. В предыдущем примере оба CanFight и ActionCharacter имели идентичные методы void fight( ). Но это не вызвало проблемы поскольку эти методы одинаковы в обоих классах, но что было бы если бы они были бы разными? Вот пример:

//: c08:InterfaceCollision.java

interface I1 { void f(); }
interface I2 { int f(int i); }
interface I3 { int f(); }
class C { public int f() { return 1; } }

class C2 implements I1, I2 {
  public void f() {}
  public int f(int i) { return 1; } // перегружен
}

class C3 extends C implements I2 {
  public int f(int i) { return 1; } // перегружен
}

class C4 extends C implements I3 {
  // Одинаковы, нет проблем:
  public int f() { return 1; } 
}

// Методы различаются только возвращаемым типом:
//! class C5 extends C implements I1 {}
//! interface I4 extends I1, I3 {} ///:~

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

InterfaceCollision.java:23: f() in C cannot 
implement f() in I1; attempting to use 
incompatible return type
found   : int
required: void
InterfaceCollision.java:24: interfaces I3 and I1 are incompatible; both define f
(), but with different return type

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

Расширение интерфейса с наследованием

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

//: c08:HorrorShow.java
// Расширение интерфейса с наследованием.

interface Monster {
  void menace();
}

interface DangerousMonster extends Monster {
  void destroy();
}

interface Lethal {
  void kill();
}

class DragonZilla implements DangerousMonster {
  public void menace() {}
  public void destroy() {}
}

interface Vampire 
    extends DangerousMonster, Lethal {
  void drinkBlood();
}

class HorrorShow {
  static void u(Monster b) { b.menace(); }
  static void v(DangerousMonster d) {
    d.menace();
    d.destroy();
  }
  public static void main(String[] args) {
    DragonZilla if2 = new DragonZilla();
    u(if2);
    v(if2);
  }
} ///:~

DangerousMonster - простое расширение до Monster, создающее новый интерфейс. Который, в свою очередь реализуется в DragonZilla.

Синтаксис, использованный в Vampire, работает только с наследованием интерфейсов. Обычно, Вы можете использовать extends только с одиночным классом, но в силу того, что интерфейсы могут быть созданы из множества других интерфейсов, extends может ссылаться на множество базовых интерфейсов при создании нового интерфейса. Как Вы видите, имена интерфейсов отделены просто запятыми.

Группировка констант

Поскольку любое поле помещенное вами в интерфейс автоматически становится static и final, то интерфейс, по сути, удобная штука для создания групп констант, так же, как и enum в C или C++. К примеру:

//: c08:Months.java
// Использование интерфейса для создания групп констант.
package c08;

public interface Months {
  int
    JANUARY = 1, FEBRUARY = 2, MARCH = 3, 
    APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, 
    AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
    NOVEMBER = 11, DECEMBER = 12;
} ///:~

Заметьте, что в стиле Java используются все буквы в верхнем регистре (с подчеркиваниями для разделения слов) для static final "констант".

Теперь Вы можете использовать эти константы снаружи пакета, просто импортируя c08.* или c08.Months, так же, как Вы импортируете другие пакеты и ссылаться на них примерно так Months.JANUARY. Естественно, то, что Вы получите это просто int, поскольку тут нету никакого дополнительного типа безопасности, какой имеет в C++ enum, но эта техника (наиболее часто используемая) может помочь в случае тяжелого программирования в вашей программе.

Если же Вы хотите получить дополнительные безопасные типы, то вам необходимо создать класс на подобии этого[38]:

//: c08:Month2.java
// Более здоровая система перечисления.
package c08;

public final class Month2 {
  private String name;
  private Month2(String nm) { name = nm; }
  public String toString() { return name; }
  public final static Month2
    JAN = new Month2("January"), 
    FEB = new Month2("February"),
    MAR = new Month2("March"),
    APR = new Month2("April"),
    MAY = new Month2("May"),
    JUN = new Month2("June"),
    JUL = new Month2("July"),
    AUG = new Month2("August"),
    SEP = new Month2("September"),
    OCT = new Month2("October"),
    NOV = new Month2("November"),
    DEC = new Month2("December");
  public final static Month2[] month =  {
    JAN, JAN, FEB, MAR, APR, MAY, JUN,
    JUL, AUG, SEP, OCT, NOV, DEC
  };
  public static void main(String[] args) {
    Month2 m = Month2.JAN;
    System.out.println(m);
    m = Month2.month[12];
    System.out.println(m);
    System.out.println(m == Month2.DEC);
    System.out.println(m.equals(Month2.DEC));
  }
} ///:~

Этот класс называется Month2, поскольку класс с именем Month уже есть в стандартной библиотеке Java. Этот класс final с конструктором private, поэтому никто не может от него наследовать или создать его представление. Представления final static создаются только однажды, в самом классе: JAN, FEB, MAR и т.д. Эти объекты так же используются в массиве month, что позволяет вам получать доступ к именам по их номеру. (Заметьте, что дополнительный месяц JAN в этом массиве осуществляет смещение на единицу, поэтому то декабрь и будет 12-м, а не 11-м.) В main( ) Вы можете видеть безопасный тип: m является объектом Month2, так что он может быть доступен только как Month2. Предыдущий пример Months.java обрабатывал только значения int, так что, месяц представлялся значением типа int, что само по себе не очень безопасно.

Приведенная техника позволяет вам так же использовать == или equals( ) взаимозаменяемо, как показано в конце метода main( ).

Инициализирование полей в интерфейсах

Поля определенные в интерфейсах автоматически становятся static и final. Они не могут быть пустыми (чистыми) final переменными, но они могут быть инициализированы не постоянными выражениями. К примеру:

//: c08:RandVals.java
// Инициализирование полей интерфейса  
// не постоянными инициализаторами.
import java.util.*;

public interface RandVals {
  int rint = (int)(Math.random() * 10);
  long rlong = (long)(Math.random() * 10);
  float rfloat = (float)(Math.random() * 10);
  double rdouble = Math.random() * 10;
} ///:~

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

//: c08:TestRandVals.java

public class TestRandVals {
  public static void main(String[] args) {
    System.out.println(RandVals.rint);
    System.out.println(RandVals.rlong);
    System.out.println(RandVals.rfloat);
    System.out.println(RandVals.rdouble);
  }
} ///:~

Эти поля, естественно, не являются частью интерфейса, вместо этого они размещены в static хранилище этого интерфейса.

Вложенные интерфейсы

[39]Интерфейсы могут быть вложены внутрь классов и других интерфейсов. Такая возможность выявляет несколько очень интересных возможностей:

//: c08:NestingInterfaces.java

class A {
  interface B {
    void f();
  }
  public class BImp implements B {
    public void f() {}
  }
  private class BImp2 implements B {
    public void f() {}
  }
  public interface C {
    void f();
  }
  class CImp implements C {
    public void f() {}
  }
  private class CImp2 implements C {
    public void f() {}
  }
  private interface D {
    void f();
  }
  private class DImp implements D {
    public void f() {}
  }
  public class DImp2 implements D {
    public void f() {}
  }
  public D getD() { return new DImp2(); }
  private D dRef;
  public void receiveD(D d) { 
    dRef = d; 
    dRef.f();
  }
}

interface E {
  interface G {
    void f();
  }
  // избыточный "public":
  public interface H {
    void f();
  }
  void g();
  // Не может быть private внутри интерфейса:
  //! private interface I {}
}

public class NestingInterfaces {
  public class BImp implements A.B {
    public void f() {}
  }
  class CImp implements A.C {
    public void f() {}
  }
  // Не может быть реализован private interface без
  // внутреннего определения класса:
  //! class DImp implements A.D {
  //!  public void f() {}
  //! }
  class EImp implements E {
    public void g() {}
  }
  class EGImp implements E.G {
    public void f() {}
  }
  class EImp2 implements E {
    public void g() {}
    class EG implements E.G {
      public void f() {}
    }
  }
  public static void main(String[] args) {
    A a = new A();
    // Нет доступа A.D:
    //! A.D ad = a.getD();
    // Ничего не возвращается, кроме A.D:
    //! A.DImp2 di2 = a.getD();
    // Нельзя получить доступ к участнику интерфейса:
    //! a.getD().f();
    // Только другой A может что-то делать с getD():
    A a2 = new A();
    a2.receiveD(a.getD());
  }
} ///:~

Синтаксис внутреннего интерфейса внутри класса очевиден и похож на не внутренние интерфейсы, которые могут быть public или friendly. Вы так же видите, что оба внутренних интерфейса public и friendly могут быть реализованы как public, friendly и private внутренние классы.

С этой новой особенностью интерфейсы могут так же быть private, как видно из A.D (тот же самый синтаксис используется для внутренний интерфейсов и для внутренних классов). Что же хорошего в private внутреннем интерфейсе? Как Вы можете догадаться, он может быть реализован только как private внутренний класс, как, например в DImp, но в A.DImp2 видно, что он так же может быть реализован и как public класс. Но все равно, A.DImp2 может быть использован только как сам. Однако не следует пропустить упоминание о том, что реализация private интерфейса это всего лишь путь для принудительного определения методов в этом интерфейсе, без добавления любой информации о типе (это так и есть, без возможности любого приведения к базовому типу).

Метод getD( ) вызывает дальнейшие затруднения связанные с private интерфейсом: это public метод, который возвращает ссылку на private интерфейс. И что Вы будете делать с этим возвращенным значением? В main( ), Вы можете видеть несколько провалившихся попыток что-либо поделать с ним. Единственная вещь способная работать с ним - это может быть объект имеющий на него права, в нашем случае другой объект A, посредством метода received( ).

Интерфейс E показывает, что интерфейсы могут быть вложены друг в друга. Но все равно, правила насчет интерфейсов следующие, все элементы интерфейса должны быть public, но в случае внутреннего интерфейса внутри другого интерфейса они все и так становятся public автоматически и не могут быть сделаны private.

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

Внутренние классы

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

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

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

//: c08:Parcel1.java
// Создание внутреннего класса.

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; }
  }
  // Использование внутреннего класса
  // похоже на использование обычного класса:
  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("Tanzania");
  }
} ///:~

Внутренний класс, использованный внутри ship( ), выглядит так же, как и любой другой класс. И только одно различие бросается в глаза, это то, что его имя расположено после Parcel1. Но в дальнейшем Вы увидите, что это далеко не единственное различие.

Более типичный случай - внешний класс имеет метод, который возвращает ссылку на внутренний класс, например, так:

//: c08:Parcel2.java
// Возвращение ссылки на внутренний класс.

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 cont() { 
    return new Contents(); 
  }
  public void ship(String dest) {
    Contents c = cont();
    Destination d = to(dest);
    System.out.println(d.readLabel());
  }  
  public static void main(String[] args) {
    Parcel2 p = new Parcel2();
    p.ship("Tanzania");
    Parcel2 q = new Parcel2();
    // Определение ссылки на внутренний класс:
    Parcel2.Contents c = q.cont();
    Parcel2.Destination d = q.to("Borneo");
  }
} ///:~

Если Вы хотите создать объект внутреннего класса, где либо еще, кроме не статического метода внешнего класса, то Вы должны определить тип этого объекта, как OuterClassName.InnerClassName, как, например, в main( ).

Внутренний класс и приведение к базовому типу

Недавно, Вы узнали о том, что в Java есть достаточно хорошие механизмы для скрытия классов, их достаточно сделать "friendly" и они будут видны только для классов этого же пакета, и не нужно никаких внутренних классов.

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

Сперва общие интерфейсы должны быть определены в их собственных файлах, тогда они могут быть использованы во всех примерах:

//: c08:Destination.java
public interface Destination {
  String readLabel();
} ///:~
//: c08:Contents.java
public interface Contents {
  int value();
} ///:~

Теперь Contents и Destination представляют интерфейсы доступные для программиста - клиента. (Не забудьте, что interface автоматически делает всех членов класса public.)

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

//: c08:Parcel3.java
// Возвращение ссылки на внутренний класс.

public class Parcel3 {
  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 dest(String s) {
    return new PDestination(s);
  }
  public Contents cont() { 
    return new PContents(); 
  }
}

class Test {
  public static void main(String[] args) {
    Parcel3 p = new Parcel3();
    Contents c = p.cont();
    Destination d = p.dest("Tanzania");
    // Незаконно - нельзя получить доступ к  private классу:
    //! Parcel3.PContents pc = p.new PContents();
  }
} ///:~

Заметьте, поскольку main( ) в Test, то когда Вы захотите запустить эту программу, Вы не выполните Parcel3, а вместо этого:

java Test

В примере, main( ) должен быть расположен в отдельном классе, для того, что бы продемонстрировать защищенность внутреннего класса PContents.

В Parcel3 было добавлено что-то новое: внутренний класс PContents - private, так что никто кроме Parcel3 не может получить к нему доступ. PDestination является protected, так что никто кроме Parcel3 и классов из пакета Parcel3 (поскольку protected так же дает доступ к членам пакета, protected так же означает "friendly"), и наследников Parcel3 не смогут получить доступ к PDestination. Это означает, что клиентский программист имеет ограниченные знания об этих объектах и ограниченный доступ к ним. В действительности, Вы никогда не сможете привести к дочернему типу private внутренний класс (или к protected внутреннему классу, даже если Вы являетесь наследующим), и это происходит потому, что Вы не можете получить доступ к имени, как это можно посмотреть в классе Test. Поэтому private внутренний класс предоставляет разработчику возможность полностью исключить изменение его кода и полностью скрыть детали его реализации. В дополнение, расширение интерфейса так же не принесет пользы, поскольку клиент программист не сможет получить доступ ни к одному из методов, поскольку они не являются частью public interface класса. При этом так же имеется возможность для компилятора по созданию более эффективного кода.

Нормальный (не внутренний) класс не может быть сделан private или protected, только как public или friendly.

Внутренние классы в методе и контексте

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

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

В следующих примерах, предыдущий код будет изменен для получения следующих "результатов":

  1. Класс определен в методе
  2. Класс определен в контексте внутри метода
  3. Анонимный класс реализует интерфейс
  4. Анонимный класс расширяет класс, который имеет конструктор не по умолчанию
  5. Анонимный класс, осуществляющий инициализацию полей
  6. Анонимный класс, который осуществляет создание, используя инициализацию экземпляра (анонимный внутренний класс не может быть с конструктором)

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

//: c08:Wrapping.java
public class Wrapping {
  private int i;
  public Wrapping(int x) { i = x; }
  public int value() { return i; }
} ///:~

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

В первом примере показывается создание целого класса внутри контекста метода (вместо контекста другого класса):

//: c08:Parcel4.java
// Вложенность класса внутри метода.

public class Parcel4 {
  public Destination dest(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) {
    Parcel4 p = new Parcel4();
    Destination d = p.dest("Tanzania");
  }
} ///:~

Класс PDestination скорее часть dest( ) чем Parcel4. (Так же заметьте, что Вы можете использовать идентификатор класса PDestination для внутреннего класса внутри каждого класса в одной и той же поддиректории без конфликта имен.) Следовательно, PDestination не может быть доступен снаружи dest( ). Заметьте, что приведение к базовому типу происходит в операторе возврата, ничего не попадает наружу из dest( ), кроме ссылки на Destination, т.е. на базовый класс. Естественно, факт того, что имя класса PDestination помещено внутри dest( ) еще не означает, что PDestination не правильный объект, который возвращает dest( ).

Следующий пример покажет вам, как Вы можете вложить внутренний класс внутри любого случайного контекста:

//: c08:Parcel5.java
// Вложенный класс внутри контекста.

public class Parcel5 {
  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("slip");
      String s = ts.getSlip();
    }
    // Нельзя его здесь использовать! Вне контекста:
    //! TrackingSlip ts = new TrackingSlip("x");
  }
  public void track() { internalTracking(true); }
  public static void main(String[] args) {
    Parcel5 p = new Parcel5();
    p.track();
  }
} ///:~

Класс TrackingSlip помещен внутри контекста, а так же внутри оператора if. Но это не означает, что этот класс условно создается, он будет скомпилирован вместе с остальным кодом. Тем не менее он не будет доступен снаружи контекста, в котором он был объявлен. Кроме этой особенности он выглядит точно так же, как обычный класс.

Анонимный внутренний класс

Следующий пример несколько странен:

//: c08:Parcel6.java
// Метод возвращающий анонимный внутренний класс.

public class Parcel6 {
  public Contents cont() {
    return new Contents() {
      private int i = 11;
      public int value() { return i; }
    }; // В этом случае требуется точка с запятой
  }
  public static void main(String[] args) {
    Parcel6 p = new Parcel6();
    Contents c = p.cont();
  }
} ///:~

Метод cont( ) комбинирует создание возвращаемого значения с описанием класса, который и есть это возвращаемое значение! В дополнение этот класс еще и не имеет своего имени. Делая тему обсуждения немного запутанной, он выглядит как будто Вы начинаете создавать объект Contents:

return new Contents()

Но затем, до того, как Вы поставите точку запятую, Вы заявляете: "Но подождите, я думаю, я описался в определении класса":

return new Contents() {
  private int i = 11;
  public int value() { return i; }
};

А вот, что означает этот синтаксис: "Создание объекта анонимного класса, который наследует от Contents." Ссылка, возвращаемая выражением new, автоматически приводится к базовому типу, к ссылке Contents. Синтаксис анонимного внутреннего класса - короткая запись следующего кода:

class MyContents implements Contents {
  private int i = 11;
  public int value() { return i; }
}
return new MyContents();

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

//: c08:Parcel7.java
// Анонимный внутренний класс вызывающий
// конструткор базового класса.

public class Parcel7 {
  public Wrapping wrap(int x) {
    // Вызов базового конструктора:
    return new Wrapping(x) { 
      public int value() {
        return super.value() * 47;
      }
    }; // Требуется точка с запятой
  }
  public static void main(String[] args) {
    Parcel7 p = new Parcel7();
    Wrapping w = p.wrap(10);
  }
} ///:~

То есть, Вы просто передаете соответствующий аргумент в конструктор базового класса, конкретно здесь x передается в new Wrapping(x). Анонимный класс не может иметь конструктор, в котором Вы могли бы нормально вызвать super( ).

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

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

//: c08:Parcel8.java
// Анонимный внутренний класс осуществляющий
// инициализацию. Краткая версия
//  Parcel5.java.

public class Parcel8 {
  // Аргумент должен быть final для использования внутри 
  // анонимного внутреннего класса:
  public Destination dest(final String dest) {
    return new Destination() {
      private String label = dest;
      public String readLabel() { return label; }
    };
  }
  public static void main(String[] args) {
    Parcel8 p = new Parcel8();
    Destination d = p.dest("Tanzania");
  }
} ///:~

Если Вы определяете анонимный внутренний класс и хотите использовать в нем объект, который определен снаружи этого анонимного внутреннего класса, то тогда компилятор потребует, что бы данный объект был объявлен, как final. Вот поэтому-то аргумент dest( ) - final. Если же Вы забыли, в противном случае появляется ошибка времени выполнения.

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

//: c08:Parcel9.java
// Использование "инициализации экземпляра" для осуществления 
// создания анонимного внутреннего класса.

public class Parcel9 {
  public Destination 
  dest(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) {
    Parcel9 p = new Parcel9();
    Destination d = p.dest("Tanzania", 101.395F);
  }
} ///:~

Внутри инициализатора Вы можете видеть код, который не будет исполнен как часть инициализатора полей (т.е., выражение if). На самом же деле, инициализатор экземпляра по своей сути есть ничто иное, чем конструктор анонимного класса. Разумеется, что он несколько ограничен; Вы не можете перегрузить инциализатор экземпляра, поэтому у вас есть только один такой "конструктор".

Связь с внешним классом

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

//: c08:Sequence.java
// Поддержка последовательности объектов.

interface Selector {
  boolean end();
  Object current();
  void next();
}

public class Sequence {
  private Object[] obs;
  private int next = 0;
  public Sequence(int size) {
    obs = new Object[size];
  }
  public void add(Object x) {
    if(next < obs.length) {
      obs[next] = x;
      next++;
    }
  }
  private class SSelector implements Selector {
    int i = 0;
    public boolean end() {
      return i == obs.length;
    }
    public Object current() {
      return obs[i];
    }
    public void next() {
      if(i < obs.length) i++;
    }
  }
  public Selector getSelector() {
    return new SSelector();
  }
  public static void main(String[] args) {
    Sequence s = new Sequence(10);
    for(int i = 0; i < 10; i++)
      s.add(Integer.toString(i));
    Selector sl = s.getSelector();    
    while(!sl.end()) {
      System.out.println(sl.current());
      sl.next();
    }
  }
} ///:~

Sequence - просто массив с фиксированным размером, с элементами типа Object, с классом обернутым вокруг него. Вы вызываете метод add( ) для добавления нового Object-а в конец последовательности (если там еще есть место). Для выборки каждого из объекта в Sequence, имеется интерфейс Selector, который позволяет так же вам узнать, что вы в конце end( ), для просмотра текущего current( ) Object-а, и для перехода на следующий next( ) Object в последовательности Sequence. Поскольку Selector - interface, многие другие классы могут реализовать его по своему собственному усмотрению, а так же многие методы могут получать этот интерфейс как аргумент, в порядке создания наследуемого кода.

Здесь, SSelector - private класс, который предоставляет функциональность Selector. В main( ), Вы можете видеть создание Sequence, следующее за добавлением объектов String. Затем Selector создается путем вызова getSelector( ) и при его же помощи можно перемещаться по Sequence выбирая каждый из элементов.

На первый взгляд, создание SSelector выглядит, как просто создание другого внутреннего класса. Но просмотрите его более внимательно. Заметьте, что каждый из методов end( ), current( ) и next( ) ссылаются на obs, который является ссылкой и не является частью SSelector, но он заменяет private поле в окружающем классе. Тем не менее, внутренний класс может получить доступ к методам и полям окружающего класса, как если бы они были его собственными полями.

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

static внутренние классы

Если вам не нужно соединение между внутренним классом и объектом внешнего класса, тогда Вы можете сделать этот внутренний класс static. Для того, что бы понять значение static примененного к внутреннему классу, Вы должны вспомнить то, что этот объект обычного внутреннего класса неявным образом ссылается на объект создавшего его окружающего класса. А если Вы объявите его как static, то это уже не будет правдой. Static внутренний класс означает:

  1. Вам не нужен объект внешнего класса для создания объекта static внутреннего класса.
  2. Вы не можете получить доступ к внешнему объекту из static внутреннего класса.

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

//: c08:Parcel10.java
// Static внутренний класс.

public class Parcel10 {
  private static class PContents 
  implements Contents {
    private int i = 11;
    public int value() { return i; }
  }
  protected static class PDestination
      implements Destination {
    private String label;
    private PDestination(String whereTo) {
      label = whereTo;
    }
    public String readLabel() { return label; }
    // Static внутренний класс может содержать 
    // другие static элементы:
    public static void f() {}
    static int x = 10;
    static class AnotherLevel {
      public static void f() {}
      static int x = 10;
    }
  }
  public static Destination dest(String s) {
    return new PDestination(s);
  }
  public static Contents cont() {
    return new PContents();
  }
  public static void main(String[] args) {
    Contents c = cont();
    Destination d = dest("Tanzania");
  }
} ///:~

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

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

Обычно Вы не можете поместить какой либо код внутрь interface, но static внутренний класс может быть частью interface. Поскольку этот класс static, то он не нарушит правила для интерфейсов - static внутренний класс, только помещается он внутри поля имени интерфейса:

//: c08:IInterface.java
// Static внутренний класс внутри интерфейса.

interface IInterface {
  static class Inner {
    int i, j, k;
    public Inner() {}
    void f() {}
  }
} ///:~

Раньше в этой книге, я предлагал помещать main( ) в каждый класс, что бы иметь возможность его тестировать. Препятствием для этого может служить, избыточный компилированный код. Если он сильно досаждает, то Вы можете использовать static внутренний класс для минимизации вашего тестового кода:

//: c08:TestBed.java
// Помещаем тестовый код в  static внутренний класс.

class TestBed {
  TestBed() {}
  void f() { System.out.println("f()"); }
  public static class Tester {
    public static void main(String[] args) {
      TestBed t = new TestBed();
      t.f();
    }
  }
} ///:~

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

Ссылки на объект внешнего класса

Если вам необходимо сделать ссылку на внешний объект, вы просто указываете имя вашего внешнего объекта и после него добавляете точку и this. К примеру, в классе Sequence.SSelector, любые его методы могут производить хранимые ссылки на внешний класс Sequence просто указывая их как Sequence.this. Результирующая ссылка автоматически становится нужного типа. (Это обстоятельство известно и проверяется на стадии компилирования, поэтому на стадии выполнения не будет излишних накладных расходов.)

Иногда, вам нужно сообщить другим объектам, что бы они создали объекты своих внутренних классов. Что бы это провернуть, вам нужно предоставить ссылку другим внешним классам в выражении new, вот как в примере:

//: c08:Parcel11.java
// Создание экземпляров внутреннего  класса.

public class Parcel11 {
  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) {
    Parcel11 p = new Parcel11();
    // Должны использовать экземпляр внешнего класса
    // для создания экземпляра внутреннего класса:
    Parcel11.Contents c = p.new Contents();
    Parcel11.Destination d =
      p.new Destination("Tanzania");
  }
} ///:~

Для создания объекта внутреннего класса напрямую, Вы уже не должны следовать тем же самым методом и сослаться прямо на внешний класс Parcel11, но вместо этого Вы должны использовать объект внешнего класса для создания объекта внутреннего класса:

Parcel11.Contents c = p.new Contents();

Поэтому нельзя создать объект внутреннего класса до того, как у вас будет создан объект внешнего класса. Эта невозможность происходит из-за того, что внутренний класс "подсоединен" к объекту внешнего класса в котором он был создан. Тем не менее , если Вы сделаете static внутренний класс, то ему уже не нужна ссылка на внешний класс.

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

[41]Совершенно не играет роли, как глубоко может быть вложен внутренний класс, он может совершенно прозрачно получить доступ ко всем элементам всех классов, в которых он вложен, ниже этому пример:

//: c08: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, методы g( ) и f( ) востребуются без каких либо ограничений (несмотря даже на тот факт, что они private). Этот пример также демонстрирует синтаксис требуемый для создания объектов многократно вложенных внутренних классов, когда Вы создаете объект в другом классе. New предоставляет правильную область действия, поэтому вам не нужно квалифицировать имя класса в вызове конструктора.

Наследование от внутренних классов

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

//: c08:InheritInner.java
// Наследование внутреннего класса.

class WithInner {
  class Inner {}
}

public class InheritInner 
    extends WithInner.Inner {
  //! InheritInner() {} // Не компилируется
  InheritInner(WithInner wi) {
    wi.super();
  }
  public static void main(String[] args) {
    WithInner wi = new WithInner();
    InheritInner ii = new InheritInner(wi);
  }
} ///:~

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

enclosingClassReference.super();

внутри конструктора. При этом предоставляется ссылка и программа будет компилироваться.

Может ли быть внутренний класс перегружен?

Что произойдет, когда Вы создадите внутренний класс, а затем наследуете окружающий класс и переопределите внутренний класс? Что же это такое, что возможно переопределить внутренний класс? Да, это именно так и выглядит, но переопределение внутреннего класса это то же самое, как если бы во внешнем классе был бы другой метод, который ничего бы не делал:

//: c08:BigEgg.java
// Внутренний класс не может быть переопределен 
// как метод.

class Egg {
  protected class Yolk {
    public Yolk() {
      System.out.println("Egg.Yolk()");
    }
  }
  private Yolk y;
  public Egg() {
    System.out.println("New Egg()");
    y = new Yolk();
  }
}

public class BigEgg extends Egg {
  public class Yolk {
    public Yolk() {
      System.out.println("BigEgg.Yolk()");
    }
  }
  public static void main(String[] args) {
    new BigEgg();
  }
} ///:~

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

New Egg()
Egg.Yolk()

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

//: c08:BigEgg2.java
// Правильное наследование от внутреннего класса.

class Egg2 {
  protected class Yolk {
    public Yolk() {
      System.out.println("Egg2.Yolk()");
    }
    public void f() {
      System.out.println("Egg2.Yolk.f()");
    }
  }
  private Yolk y = new Yolk();
  public Egg2() {
    System.out.println("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() {
      System.out.println("BigEgg2.Yolk()");
    }
    public void f() {
      System.out.println("BigEgg2.Yolk.f()");
    }
  }
  public BigEgg2() { insertYolk(new Yolk()); }
  public static void main(String[] args) {
    Egg2 e2 = new BigEgg2();
    e2.g();
  }
} ///:~

Теперь BigEgg2.Yolk ясно расширяет Egg2.Yolk и переопределяет его методы. Метод insertYolk( ) позволяет BigEgg2 привести к базовому типу один из его объектов Yolk к ссылке y в Egg2, так что, когда g( ) вызывает y.f( ), то используется переопределенная версия f( ). Вывод же будет такой:

Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()

Второй вызов Egg2.Yolk( ) это вызов конструктора базового класса из конструктора BigEgg2.Yolk. Вы так же можете видеть, что переопределенная версия f( ) используется, когда вызывается g( ).

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

Поскольку, каждый класс создает файл .class, который содержит все информацию о том, как создавать объект этого типа, то Вы можете догадаться, что внутренний класс может так же создавать файл .class содержащий информацию для его объекта Class. Имя такого класса выбирается следующим образом: имя окружающего класса, символ "$", имя внутреннего класса. К примеру, файлы .class созданные InheritInner.java:

InheritInner.class
WithInner$Inner.class
WithInner.class

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

Хотя эта схема генерирования внутренних имен проста и непосредственна, она так же устойчива к различным ситуациям[42]. Поскольку такой подход к генерации, является подмножеством стандартной схемы именования в Java, то сгенерированные файлы становятся автоматически платформо-независимомы. (Заметьте, что компилятор изменяет ваши внутренние классы таким образом, что бы они работали.)

Зачем внутренние классы?

До этого момента Вы видели множество примеров синтаксиса и семантического описания работы внутренних классов, но еще не было ответа на вопрос "А зачем они собственно?". Зачем Sun пошел на такие значительные усилия, что бы добавить эту возможность языка?

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

Вопрос, глубоко задевающий внутренние классы: если мне нужна ссылка на интерфейс, почему я должен реализовывать его во внешнем классе? А вот и ответ: "Если это все , что нужно, то как этого добиться?" Так где разница, между реализацией интерфейса внутренним и внешним классами? А ответ такой - Вы не всегда можете удобно работать с интерфейсами, иногда нужно работать и с реализацией. Отсюда самая непреодолимая причина работы с классами:

Каждый внутренний класс может быть независимо наследован от реализации. Поэтому, внутренний класс не ограничен тем, если внешний класс уже наследовал от реализации.

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

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

//: c08:MultiInterfaces.java
// Два способа, как класс может 
// реализовать множественные интерфейсы.

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());
  }
} ///:~

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

Тем не менее, если у вас есть abstract или конкретный класс, вместо интерфейса, то Вы сразу же становитесь ограниченны в использовании только внутреннего класса, естественно, если все еще требуется реализовать их несколько в одном:

//: c08:MultiImplementation.java
// С конкретным или абстарктным классом, внутренние 
// классы - единственный путь для достижения эффекта
//  "множественная реализация интерфейса."

class C {}
abstract class D {}

class Z extends C {
  D makeD() { return new D() {}; }
} 

public class MultiImplementation {
  static void takesC(C c) {}
  static void takesD(D d) {}
  public static void main(String[] args) {
    Z z = new Z();
    takesC(z);
    takesD(z.makeD());
  }
} ///:~

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

  1. Внутренний класс может иметь несколько экземпляров, каждый со своими собственными данными, независимыми от объекта внешнего класса.
  2. В одном внешнем классе может быть несколько внутренних классов, каждый из которых реализует тот же самый интерфейс или наследует от того же самого класса, но по другому. Пример такого применения будет предоставлен дальше.
  3. Место создания объекта внутреннего класса не связано с созданием объекта внешнего класса.
  4. Не возникает потенциального конфликта в связи "это-есть" (is-a) с внутренним классом, поскольку они раздельные единицы.

Пример. Если бы Sequence.java не использовал бы внутренние классы, то Вы бы сказали Sequence это есть Selector, и Вы бы могли иметь только один Selector в Sequence. Так же, у вас не было бы второго метода getRSelector( ), который происходит от Selector, который собственно двигается в обратном направлении по последовательности. Гибкость такого рода доступна только с использованием внутренних классов.

Замыкания & обратные вызовы

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

Наиболее неоспоримым аргументом для включения можно назвать разновидность указательного механизма в Java позволяющего осуществлять обратные вызовы (callbacks). С обратным вызовом, некоторые другие объекты, могут получить кусочек информации, которая позволит им в дальнейшем передать управление в исходящий объект. Это очень мощная концепция, как Вы увидите потом, в Главе 13 и Главе 16. Если обратный вызов реализуется через использование указателя, то Вы должны очень осторожно с ним обращаться. Как Вы наверное уже могли понять, в Java имеется тенденция для более осторожного программирования, поэтому указатели не включены в этот язык.

"Замыкание" предоставляемое внутренними классами - лучшее решение, чем указатели. Оно более гибкое и намного более безопасное. Вот пример:

//: c08:Callbacks.java
// Использование внутренних классов для возврата

interface Incrementable {
  void increment();
}

// Очень просто реализовать интерфейс:
class Callee1 implements Incrementable {
  private int i = 0;
  public void increment() { 
    i++;
    System.out.println(i);
  }
}

class MyIncrement {
  public void increment() {
    System.out.println("Other operation");
  }
  public static void f(MyIncrement mi) {
    mi.increment();
  }
}

// Если ваш класс должен реализовать increment() по другому,
// Вы должны использовать внутренний класс:
class Callee2 extends MyIncrement {
  private int i = 0;
  private void incr() { 
    i++;
    System.out.println(i);
  }
  private class Closure implements Incrementable {
    public void increment() { incr(); }
  }
  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();
  }
} ///:~

Этот пример так же показывает дальнейшие различия между реализацией интерфейса во внешнем классе и того же самого во внутреннем. Callee1 простое решение в терминах кода. Callee2 наследует от MyIncrement, который уже имеет отличный метод increment( ), который в свою очередь что то делает, при этом еу нужен интерфейс Incrementable. Когда MyIncrement наследуется в Callee2, increment( ) уже не может быть переопределен для использования с Incrementable, поэтому принудительно использовано разделение реализаций с использованием внутреннего класса. Так же заметьте, когда Вы создаете внутренний класс вам уже не нужно добавлять или модифицировать интерфейс внешнего класса.

Обратите внимание на то, что все исключая getCallbackReference( ) в Callee2 с модификатором private. Для того, что бы разрешить любые соединения с внешним миром, можно использовать интерфейс Incrementable. Далее Вы увидите, как интерфейсы поддерживают полное разделение интерфейса и реализации.

Внутренний класс Closure просто реализует Incrementable для того, что бы безопасно перехватить возврат Callee2. Единственный способ получить эту ссылку это вызов increment( ).

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

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

Внутренние классы и структуры управления

Более конкретный пример использования внутренних классов может быть получен в нечто называемом здесь как структуры управления.

Структура управления приложением это класс или набор классов, которые спроектированы для решения частных проблем. Для того, что бы применить структуру управления приложения, Вы должны наследовать от одного или нескольких этих классов и переопределить некоторые методы. Код, который Вы напишите в переопределенных методах, подстроит решения проблем под ваши конкретные задачи. Структура управления - частный тип структур управления приложениями с доминирующей функцией ответа на события; система, которая в основном занимается обработкой событий называется системой обработки событий. Одной из наиболее важных проблем в программировании приложений можно назвать графический пользовательский интерфейс (GUI), который почти полностью завязан на обработке событий. Как Вы сможете увидеть в главе 13, библиотека Java Swing - структура управления, которая элегантно решает проблемы GUI и очень много использует внутренние классы.

Для того, что бы увидеть, как внутренние классы позволяют с легкостью создавать и использовать структуры управления, давайте представим себе структуру управления, чьей задачей будет выполнение событий, когда они будут готовы. Хотя "готовы" здесь может означать что угодно, поэтому выберем вариант зависящий от времени. Теперь у нас есть структура управления без какой либо конкретной информации о том, что же ей нужно в действительности контролировать. Сперва, тут есть интерфейс, который описывает управляемые события. В этой роли выступает абстрактный класс, вместо настоящего интерфейса, поскольку у нас используется поведение на основе таймера:

//: c08:controller:Event.java
// Общие методы для любого контроля осбытий.
package c08.controller;

abstract public class Event {
  private long evtTime;
  public Event(long eventTime) {
    evtTime = eventTime;
  }
  public boolean ready() {
    return System.currentTimeMillis() >= evtTime;
  }
  abstract public void action();
  abstract public String description();
} ///:~

Конструктор просто запоминает время, когда Вы хотите запустить Event, затем ready( ) сообщает, когда приходит время для запуска. Естественно ready( ) должен быть переопределен в дочернем классе на нечто более другое, чем время.

Action( ) - метод, который вызывается, когда Event уже (готов) ready( ), а description( ) дает текстовое сопровождение об этом Event.

Следующий файл содержит структуру управления, которая управляет и удаляет события. Первый класс - простой помощник, чья работа заключается только в содержании объектов Event. Вы можете заменить его любым подходящим контейнером, а в главе 9 Вы откроете для себя другие контейнеры, которые делают этот же трюк, но без какого либо дополнительного кода:

//: c08:controller:Controller.java
// Вместе с  Event, изначальная
// система управления для всех систем управления:
package c08.controller;

// Это просто способ содержаня объектов Event.
class EventSet {
  private Event[] events = new Event[100];
  private int index = 0;
  private int next = 0;
  public void add(Event e) {
    if(index >= events.length)
      return; // (В настоящей жизни нужно обрабатывать исключение)
    events[index++] = e;
  }
  public Event getNext() {
    boolean looped = false;
    int start = next;
    do {
      next = (next + 1) % events.length;
      // Смотрим, не зациклился ли он:
      if(start == next) looped = true;
      // Если зациклился, то обнуляем список 
      // 
      if((next == (start + 1) % events.length)
         && looped)
        return null;
    } while(events[next] == null);
    return events[next];
  }
  public void removeCurrent() {
    events[next] = null;
  }
}

public class Controller {
  private EventSet es = new EventSet();
  public void addEvent(Event c) { es.add(c); }
  public void run() {
    Event e;
    while((e = es.getNext()) != null) {
      if(e.ready()) {
        e.action();
        System.out.println(e.description());
        es.removeCurrent();
      }
    }
  }
} ///:~

EventSet поддерживает Event-ов. (Если бы использовался настоящий контейнер из главы 9, то вам не нужно было бы беспокоиться о максимальном размере, поскольку он изменяет размер самостоятельно). Index используется для сохранения пути на следующее свободное место, а next используется, когда Вы ищите следующий Event в списке, что бы понять, не зациклились ли Вы. Информация о зацикливании особенна важна при вызове getNext( ), поскольку объекты Event должны удаляться из списка (используется removeCurrent( )) как только они были выполнены, поэтому getNext( ) должен найти промежутки в списке и осуществлять выборку без них.

Заметьте, что removeCurrent( ) не просто устанавливает некоторый флаг, сигнализирующий, что объект уже не используется. Вместо этого он устанавливает ссылку в null. Это поведение достаточно важно, поскольку сборщик мусора не сможет очистить объект, на который все еще существует ссылка. Если Вы думаете, что ваши ссылки могут таким образом зависнуть, то тогда хорошей идеей устанавливать их в null, дабы дать возможность сборщику мусора удалить их.

Controller - то место, где на самом деле вся работа и происходит. Он использует EventSet для поддержки его объектов Event, а метод addEvent( ) позволяет вам добавлять новые события в список. Но все таки главным методом является run( ). Этот метод циклически просматривает EventSet, выискивая объекты Event которые ready( ) (готовы) для обработки. Для всех объектов, которые готовы, он вызывает метод action( ), печатает описание - description( ), а затем удаляет событие - Event из списка.

Заметьте, что пока Вы ничего не знаете о том, что же на самом делают эти Event. И вот это и есть основная проблема проектировки; как отделить те вещи, которые должны изменяться от тех вещей, которые всегда постоянны? Или, если воспользоваться моими терминами - "вектор изменения", различные поведения различных типов объектов Event. Здесь Вы можете выражать различные действия созданием различных подклассов Event.

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

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

Рассмотрим частную реализацию структуры управления, спроектированную для управления функциями гринхауза (greenhouse functions)[43]. Каждое из действий полностью уникально и отличается от других: включение света и термостатов, выключение их, звон колокольчиков и рестартинг всей системы. Но структура управления просто изолирована в этом отличном (от других) коде. Внутренний класс позволяет вам получить множественно наследуемые классы от одного и того же базового класса (т.е. несколько наследников от одного в одном), Event, в одном единственном классе. Для каждого типа действия Вы наследуете новый внутренний класс от Event и пишите код контроля внутри action( ).

Как и для всех остальных структур управления, класс GreenhouseControls наследуется от Controller:

//: c08:GreenhouseControls.java
import c08.controller.*;

public class GreenhouseControls 
    extends Controller {
  private boolean light = false;
  private boolean water = false;
  private String thermostat = "Day";
  private class LightOn extends Event {
    public LightOn(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Сюда нужно поместить код управлением светом
      light = true;
    }
    public String description() {
      return "Light is on";
    }
  }
  private class LightOff extends Event {
    public LightOff(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Сюда для выключения света
      light = false;
    }
    public String description() {
      return "Light is off";
    }
  }
  private class WaterOn extends Event {
    public WaterOn(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // сюда код управления
      water = true;
    }
    public String description() {
      return "Greenhouse water is on";
    }
  }
  private class WaterOff extends Event {
    public WaterOff(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // сюда код управления
      water = false;
    }
    public String description() {
      return "Greenhouse water is off";
    }
  }
  private class ThermostatNight extends Event {
    public ThermostatNight(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Сюда код управления
      thermostat = "Night";
    }
    public String description() {
      return "Thermostat on night setting";
    }
  }
  private class ThermostatDay extends Event {
    public ThermostatDay(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // сюда код управления
      thermostat = "Day";
    }
    public String description() {
      return "Thermostat on day setting";
    }
  }
  private int rings;
  private class Bell extends Event {
    public Bell(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Звонить каждые 2 секунды, 'rings' раз:
      System.out.println("Bing!");
      if(--rings > 0)
        addEvent(new Bell(
          System.currentTimeMillis() + 2000));
    }
    public String description() {
      return "Ring bell";
    }
  }
  private class Restart extends Event {
    public Restart(long eventTime) {
      super(eventTime);
    }
    public void action() {
      long tm = System.currentTimeMillis();
      // Конфигурация из текстового файла
      rings = 5;
      addEvent(new ThermostatNight(tm));
      addEvent(new LightOn(tm + 1000));
      addEvent(new LightOff(tm + 2000));
      addEvent(new WaterOn(tm + 3000));
      addEvent(new WaterOff(tm + 8000));
      addEvent(new Bell(tm + 9000));
      addEvent(new ThermostatDay(tm + 10000));
      // Может быть добавлен объект рестарта
      addEvent(new Restart(tm + 20000));
    }
    public String description() {
      return "Restarting system";
    }
  }
  public static void main(String[] args) {
    GreenhouseControls gc = 
      new GreenhouseControls();
    long tm = System.currentTimeMillis();
    gc.addEvent(gc.new Restart(tm));
    gc.run();
  } 
} ///:~

Заметьте, что light, water, thermostat и rings связаны с внешним классом GreenhouseControls и поэтому внутренние классы могут получать доступ к его полям без каких либо особенностей или специального доступа. Так же многие из методов action( ) осуществляют некоторый вид аппаратного контроля, который лучше всего перевести не в Java код.

Большинство из классов Event выглядят одинаково, но Bell и Restart особенны. Bell звонит и если он не звонит некоторое время, то добавляется новый объект Bell в список, так что он прозвонит позже. Заметьте, что внутренние классы почти что выглядят, как множественное наследование: Bell содержит все методы Event и он так же имеет доступ ко всем метода внешнего класса GreenhouseControls.

Restart ответственен за инициализацию системы, он добавляет все необходимые события. Естественно, лучше было бы отказаться от жестко зашитых событий в программе и читать их из файла. (Упражнение в главе 11 попросит вас сделать данное предположение.) Поскольку Restart( ) это просто другой объект Event, Вы так же можете добавить объект Restart внутрь Restart.action( ) так что система будет периодически рестартовать сама себя. И все, что нужно будет сделать это лишь в main( ) создать объект GreenhouseControls и добавить объект Restart который выполнял бы эту работу.

Резюме

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

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

Упражнения

Решения для этих упражнений доступны в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату с www.BruceEckel.com.

  1. Докажите, что поля в интерфейсе полностью static и final.
  2. Создайте интерфейс, содержащий три метода, в его собственном пакете. Реализуйте этот интерфейс в другом пакете.
  3. Докажите, что все методы в интерфейсе автоматически public.
  4. В c07:Sandwich.java, создайте интерфейс с именем FastFood (с соответствующими методами) и изменит Sandwich так, что бы он также реализовывал FastFood.
  5. Создайте три интерфейса, каждый с двумя методами. Наследуйте новый интерфейс от этих трех, добавьте новый метод. Создайте класс реализующий этот новый интерфейс и так же наследующий от конкретного класса. Теперь напишите четыре метода, каждый из которых получают один из четырех интерфейсов в качестве аргумента. В main( ), создайте объект вашего класса и передайте его каждому из методов.
  6. Измените упражнение 5, создайте abstract класс и наследуйте его в дочернем классе.
  7. Измените Music5.java, добавьте в него интерфейс Playable. Удалите объявление play( ) из Instrument. Добавьте Playable в дочерний класс, путем добавления его в список implements. Измените tune( ) так, что бы он получал Playable вместо Instrument.
  8. Измените упражнение 6 в главе 7, так что бы Rodent был бы интерфейсом.
  9. В Adventure.java добавьте интерфейс CanClimb, такой же, как и другие.
  10. Напишите программу, которая импортирует и использует Month2.java.
  11. Следуя примеру в Month2.java, создайте список дней недели.
  12. Создайте интерфейс с не менее, чем одним методом, в своем собственном пакете. Создайте класс в другом пакете. Добавьте protected внутренний класс, который реализует этот интерфейс. В третьем пакете, наследуйте от вашего класса и внутри метода возвратите объект protected внутреннего класса, приведите к базовому типу во время возврата.
  13. Создайте интерфейс с не менее, чем одним методом и реализуйте его определением во внутреннем классе методом, который возвращает ссылку на этот интерфейс.
  14. Повторите упражнение 13, но определите внутренний класс внутри контекста метода.
  15. Повторите упражнение 13 используя анонимный внутренний класс.
  16. Создайте private внутренний класс, который реализует public интерфейс. Напишите метод, возвращающий ссылку на экземпляр private внутреннего класса, приведите к базовому типу к интерфейсу. Покажите, что внутренний класс полностью скрыт от попыток привести к дочернему классу.
  17. Создайте класс с конструкторами по умолчанию и нет. Создайте второй класс, который возвращает ссылку на первый. Создайте объект из возврата создавая анонимный внутренний класс, наследующий от первого.
  18. Создайте класс с private полем и private методом. Создайте внутренний класс, который модифицирует поля внешнего класса и вызывает методы внешнего класса. Во втором методе внешнего класса, создайте объект внутреннего класса и вызовите его методы, потом покажите происходящий эффект на объекте внешнего класса.
  19. Повторите упражнение 18 используя анонимный внутренний класс.
  20. Создайте класс, содержащий static внутренний класс. В main( ), создайте экземпляр внутреннего класса.
  21. Создайте интерфейс содержащий static внутренний класс. Реализуйте этот интерфейс и создайте экземпляр внутреннего класса.
  22. Создайте класс, содержащий внутренний класс, который в себе содержит внутренний класс. Повторите это используя static внутренний класс. Заметьте, что .class файлы создаются компилятором.
  23. Создайте класс с внутренним классом. В другом классе, создайте экземпляр внутреннего класса.
  24. Создайте класс с внутренним классом, который имеет конструктор не по умолчанию. Создайте второй класс с внутренним классом, который наследует от первого внутреннего класс.
  25. Устраните проблему в WindError.java.
  26. Измените Sequence.java добавление метода getRSelector( ), который предоставляет отличную реализацию интерфейса Selector, которая проходит назад через последовательность от конца к началу.
  27. Создайте интерфейс U с тремя методами. Создайте класс A с методом создающим ссылку на U используя внутренний класс. Создайте класс B, который содержит массив из U. B должен иметь один метод, который поддерживает и сохраняет ссылки на U в массив, а его второй метод должен устанавливать ссылку в массиве в null. И в заключении третий метод, который перемещается по массиву и вызывает методы в U. В main( ), создайте группу из объектов A и одиночный B. Заполните B ссылками U выдаваемыми объектом A. Используйте B для обратного вызова во все объекты A. Удалите некоторые ссылки U из B.
  28. В GreenhouseControls.java, добавьте Event внутренний класс, который включает и выключает вентиляторы.
  29. Покажите, что внутренний класс имеет доступ к private элементам его внешнего класса. Определите, когда обратное предположение истинно.

[38] Этот подход был вдохновлен по имейлу Rich-м Hoffarth-м.

[39] Спасибо, Martin-у Danner-у за задание этого вопроса во время семинара.

[40] Это очень отличается по дизайну вложенных классов в in C++, который есть только механизм скрытия имен. Здесь нет ссылки на окружающий объект и нет предполагаемых ограничений, в C++.

[41] Опять спасибо Martin-у Danner-у.

[42] С другой стороны, "$" это мета-знак в Unix shell и у вас могут иногда встречаться проблемы связанные с просмотром листингов .class файлов. Это немного странно, ведь Sun производитель юниксов.

[43] По некоторым причинам это была всегда приятная проблема для ее решения. Поэтому я опять вернулся к ней (до этого она была в более ранней книге C++ Inside & Out), но в Java существует более элегантное решение.

[ Предыдущая глава ] [ Краткое описание ] [ Содержание ] [ Список ] [ Следующая глава ]