В Java 1.1 можно определять один класс внутри другого класса. Это называется "внутренним классом". Внутренние классы очень полезны, так как позволяют группировать логически связанные между собой классы и контролировать видимость одного класса в рамках другого.
Однако следует понимать, что внутренние классы существенно отличаются от метода "составления", который был рассмотрен ранее.
Обычно потребность в использовании внутренних классов не является особенно очевидной, по крайней мере, сразу не чувствуется необходимости их применения. В конце этой главы, после того как будут объяснены все синтаксические правила для внутренних классов, будет представлен специальный пример, который поможет лучше понять преимущества использования внутренних классов.
Процесс создания внутреннего класса прост: определение класса помещается внутрь класса, который его содержит (если возникают проблемы при выполнении этого кода, обратитесь к разделу 3.1.2 "Присваивание" в третьей главе):
//: Parcel1.java
// Создание внутренних классов
package c07.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; }
}
// Использование внутренних классов происходит точно таким же образом,
// как и использование любого другого класса, находящегося внутри Parcel1:
public void ship(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship("Танзания");
}
} ///:~
```Если использовать внутренний класс внутри метода ship()
, то его использование выглядит аналогично использованию любого другого класса. Единственным заметным различием здесь является то, что имя внутреннего класса находится вложено внутри `Parcel1`. Однако вскоре станет ясно, что это не единственное различие.
Более типичной ситуацией является то, что внешний класс имеет специальный метод, который возвращает ссылку на внутренний класс. Например:```java //: Parcel2.java // Возвращение ссылки на внутренний класс package c07.parcel2;
публичный класс Parcel2 {
вложенный класс Contents {
приватный целое i = 11;
публичный целое значение() { вернуть i; }
}
вложенный класс Destination {
приватный строка label;
Destination(строка гдеКуда) {
label = гдеКуда;
}
строка читатьМетку() { вернуть label; }
}
публичный Destination до(строка s) {
вернуть новый Destination(s);
}
публичный Contents конт() {
вернуть новый Contents();
}
публичный void отправка(строка dest) {
Contents c = конт();
Destination d = до(dest);
}
публичный статический void основной(строка[] аргументы) {
Parcel2 p = новый Parcel2();
p.отправка("Танзания");
Parcel2 q = новый Parcel2();
// Определение обработчиков внутренних классов:
Parcel2.Contents c = q.конт();
Parcel2.Destination d = q.до("Борнео");
}
} ///:~
```
Чтобы создать объект внутреннего класса в любом месте за пределами внешнего класса и его непубличных методов, тип объекта должен быть указан как `внешний_класс.внутренний_класс`, как показано в методе `main()`.
## 7.6.1 Вложенные классы и приведение типов
До сих пор вложенные классы выглядели довольно обычными. В конце концов, использование их для скрытия информации может показаться излишним. Java уже имеет отличную систему скрытия — возможность ограничить видимость класса только внутри одного пакета, а не делать его вложенным классом.
Однако, когда мы хотим преобразовать тип к базовому классу (особенно к интерфейсу), вложенные классы начинают играть ключевую роль (создание ссылки на интерфейс от объекта, реализующего этот интерфейс, эквивалентно приведению типа к базовому классу). Это потому что вложенный класс может полностью стать недоступным или невидимым для всех — ни для кого он больше не будет доступен. Таким образом, можно легко скрывать детали реализации. Мы получаем всего лишь ссылку на базовый класс или интерфейс, и даже можем не знать точного типа. Вот пример:
```java
//: Parcel3.java
// Возвращение ссылки на вложенный класс
package c07.parcel3;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel3 {
private class PContents extends 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("Тanzania");
// Незаконно -- нельзя получить доступ к приватному классу:
//! Parcel3.PContents c = p.new PContents();
}
} ///:~Теперь, `Contents` и `Destination` представляют собой интерфейсы, доступные для использования клиентским программистом (помните, что интерфейсы делают все свои члены публичными свойствами). Они находятся в отдельных файлах для удобства, но оригинальные `Contents` и `Destination` являются публичными друг для друга в своих собственных файлах. В `Parcel3` были внесены некоторые изменения: внутренний класс `PContents` был объявлен как `private`, поэтому доступ к нему возможен только внутри `Parcel3`. Класс `PDestination` объявлен как `protected`, что позволяет получить доступ к нему из `Parcel3`, других классов в том же пакете (`protected` также предоставляет доступ всем классам в одном пакете; другими словами, `protected` является "дружественным"), а также из потомков `Parcel3`. Это означает, что клиентским программистам будет ограничен доступ к этим членам. В действительности, мы даже не можем преобразовать тип к внутреннему классу, объявленному как `private` (или `protected`, если сам объект не является его наследником), так как мы не имеем доступа к имени, как это видно в `classTest`. Таким образом, используя внутренние классы с уровнем доступа `private`, дизайнеры могут полностью запретить другим лицам зависеть от типа кода и скрыть детали реализации.Кроме того, для клиентских программистов значение области действия интерфейса отсутствует, поскольку они не имеют доступа к любым дополнительным методам, которые не являются частью общего интерфейса класса. Благодаря этому Java-компилятор имеет возможность генерировать более эффективный код.Обычные (не внутренние) классы не могут быть объявлены как `private` или `protected` — допустимы только `public` или "дружественные".
Примечание: `Contents` не обязательно должен быть абстрактным классом. Здесь можно использовать обычный класс, но наиболее типичной точки отсчета остаётся "интерфейс".
## 7.6.2 Методы и области действия внутренних классов
На данный момент мы уже достаточно хорошо понимаем типичное применение внутренних классов. Обычно код, который использует внутренние классы, представляет собой простые и легко понимаемые конструкции. Однако дизайн внутренних классов очень многофункционален, и нередко возникают ситуации, когда требуется создать внутренний класс внутри метода или любой другой области действия. Есть две причины, почему это может потребоваться:
(1) Как было показано ранее, мы готовы реализовать некоторый интерфейс, чтобы иметь возможность создания и возврата ссылки.
(2) Чтобы решить сложную задачу и создать класс, который поможет нашему алгоритму решения проблемы, при этом не желая делать этот класс общедоступным.
В следующем примере будут внесены изменения в предыдущий код, чтобы использовать:
(1) Класс, определённый внутри метода
( Yöntem içinde tanımlanan sınıf )
(2) Класс, определённый в области действия метода
( Yöntem etkisinde tanımlanan sınıf )
(3) Анонимный класс для реализации интерфейса
( Интерфейс için anoniş sınıf )
(4) Анонимный класс, расширяющий класс с неконструктором по умолчанию.
( Anonim sınıf, varsayılan yaratıcı olmayan bir sınıfı genişletmek için )(5) Анонимный класс, используемый для выполнения инициализации полей.
(6) Анонимный класс, созданный через конструктор экземпляра (анонимные внутренние классы не могут иметь конструктор).
Все это происходит внутри пакета `innerscopes`. Сначала, общие интерфейсы из вышеупомянутого кода получают свои определения в отдельных файлах, что позволяет использовать их во всех примерах:
```markdown
//: Destination.java
пакет c07.innerscopes;
интерфейс Destination {
строка readLabel();
} //:~
```
Поскольку мы считаем, что `Contents` может быть абстрактным классом, то можем использовать более естественную форму, как если бы это был интерфейс:
```markdown
//: Contents.java
пакет c07.innerscopes;
интерфейс Contents {
целое значение();
} //:~
```
Несмотря на то, что `Wrapping` является обычным классом с конкретной реализацией, он используется как универсальный "интерфейс" для всех его производных классов:
```markdown
//: Wrapping.java
пакет c07.innerscopes;
публичный класс Wrapping {
приватное целое i;
публичный Wrapping(целое x) { i = x; }
публичное целое значение() { вернуть i; }
} //:~
```
В вышеприведённом коде мы видим, что `Wrapping` имеет конструктор, требующий аргумент, что делает ситуацию ещё интереснее.
Первый пример демонстрирует, как создаётся полный класс в области действия метода (а не класса):
```markdown
//: Parcel4.java
// Вложение класса в метод
пакет c07.innerscopes;
```публичный класс Parcel4 {
публичный Destination dest(строка s) {
класс PDestination
реализует Destination {
приватное строка label;
приватный PDestination(строка где_к) {
label = где_к;
}
публичное строка readLabel() {
вернуть label;
}
}
вернуть новый PDimestone(s);
}
публичный статический void основной(строка[] аргументы) {
Parcel4 p = новый Parcel4();
Destination d = p.dest("Танзания");
}
} //:~
Класс `PDestination` является частью метода `dest()`, но не части класса `Parcel4` (при этом обратите внимание, что можно использовать идентификатор класса `PDestination` для каждого внутреннего класса в одном каталоге, что не вызывает конфликтов названий). Поэтому, `PDestination` недоступен за пределами метода `dest()`. Обратите внимание на преобразование типа при возврате — ничего, кроме ссылки на базовый класс `Destination`, не выходит за пределы метода `dest()`. Конечно, нельзя считать, что объект `PDestination` становится недействительным после возврата из метода `dest()`, просто потому, что имя класса находится внутри метода `dest()`.
Приведенный ниже пример демонстрирует, как можно вложить внутренний класс в любой области видимости:
```java
//: Parcel5.java
// Вложение класса внутри области видимости
package c07.innerscopes;
```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`. Это не означает, что класс создается условно — он компилируется вместе со всем остальным. Однако вне области своего определения этот класс недоступен. Кроме того, он выглядит так же, как обычный класс.
Следующий пример может показаться странным:
```java
//: Parcel6.java
// Метод, который возвращает анонимный внутренний класс
package c07.innerscopes;
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 extends Contents {
private int i = 11;
public int value() { return i; }
}
return new MyContents();
```
В анонимном внутреннем классе `Contents` создаётся с помощью конструктора по умолчанию. Ниже приведён пример кода, который показывает, что происходит, когда базовый класс требует конструктор с параметрами:
```java
//: Parcel7.java
// Анонимный вложенный класс, вызывающий конструктор базового класса
package c07.innerscopes;
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);
}
} ///:~
```
То есть мы просто передаём подходящие параметры конструктору базового класса, как это видно в `new Wrapping(x)`, где `x` передаётся в качестве параметра. Анонимный класс не может иметь конструктор, поэтому здесь нет необходимости использовать обычную практику вызова `super()`.
В вышеупомянутых двух примерах, точка с запятой не указывает на окончание класса (в отличие от C++). Вместо этого она указывает на окончание выражения, которое содержит анонимный класс. Таким образом, она полностью эквивалентна использованию точки с запятой в любом другом месте.
```А что произойдет, если нам нужно выполнить некоторую форму инициализации объекта анонимного внутреннего класса? Поскольку он является анонимным, у него нет имени для конструктора, так что мы не можем иметь конструктор. Однако мы можем выполнить инициализацию своих полей следующим образом:```java
//: Parcel8.java
// Анонимный внутренний класс, выполняющий инициализацию.
// Упрощенная версия Parcel5.java.
package c07.innerscopes;
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("Танзания");
}
} ///:~
```
Если вы пытаетесь определить анонимный внутренний класс и хотите использовать объект, определенный вне его, компилятор требует, чтобы этот внешний объект был объявлен как `final`. Именно поэтому мы делаем параметр `dest()` `final`. Если забудете сделать это, получите ошибку компиляции.
Эта методика работает, если вы просто хотите назначить поле. Но что делать, если вам нужно выполнить какие-то действия, аналогичные работе конструктора? Через инициализацию экземпляров Java 1.1 можно эффективно создать "конструктор" для анонимного внутреннего класса:
```java
//: Parcel9.java
// Использование "экземплярной инициализации" для выполнения
// конструирования анонимного внутреннего класса
package c07.innerscopes;
``````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("Превышено бюджетное ограничение!");
}
private String label = dest;
public String readLabel() { return label; }
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.dest("Танзания", 101.395F);
}
}
///:~
```
```В модуле экземплярной инициализации мы видим, что код не может выполняться как часть инициализации класса (т.е., `if`-выражение). Таким образом, фактически, модуль экземплярной инициализации является конструктором анонимного внутреннего класса. Конечно, его возможности ограничены; мы не можем переопределять модули экземплярной инициализации, поэтому у нас может быть только один такой конструктор.
## 7.6.3 Ссылка на внешний класс
До сих пор внутренние классы казались нам просто способом скрытия имени и организации кода. Хотя эти возможности очень полезны, они кажутся не особенно впечатляющими. Однако мы также пренебрегли важным аспектом. При создании своего внутреннего класса, объект этого класса одновременно имеет ссылку на внешний класс (класс, который оборачивает или создаёт этот внутренний класс). Поэтому он может обращаться к членам этого внешнего класса — без необходимости использования квалификации. Кроме того, внутренний класс имеет доступ ко всем элементам внешнего класса (примечание ②). В следующем примере это показано:
```java
//: Sequence.java
// Хранит последовательность объектов
``````java
public class Sequence {
private Object[] o;
private int next = 0;
public Sequence(int size) {
o = new Object[size];
}
public void add(Object x) {
if(next < o.length) {
o[next] = x;
next++;
}
}
private class SSelector implements Selector {
int i = 0;
public boolean end() {
return i == o.length;
}
public Object current() {
return o[i];
}
public void next() {
if(i < o.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((String)sl.current());
sl.next();
}
}
}
///:~
```
```md
## Примечание
Это значительно отличается от дизайна "вложенного класса" в C++, который представляет собой простую механику скрытия имени. В C++ нет ссылок на упакованный объект, также как и отсутствуют по умолчанию права доступа.
```Здесь `Sequence` представляет собой простой массив объектов фиксированного размера, хранящийся внутри одного класса. Мы вызываем метод `add()`, чтобы добавить новый объект в конец `Sequence` (если есть свободное место). Для получения каждого объекта из `Sequence` используется интерфейс `Selector`, который позволяет узнать, достигнут ли конец (`end()`), просмотреть текущий объект (`current() Object`) и перейти к следующему объекту в `Sequence` (`next() Object`). Так как `Selector` является интерфейсом, множество других классов могут реализовать его по своему усмотрению, и многие методы могут принимать этот интерфейс в качестве параметра, что делает код более универсальным.
Здесь `SSelector` является приватным классом, предоставляющим функциональность `Selector`. В методе `main()` можно увидеть процесс создания `Sequence`, после которого добавляются последовательности строковых объектов. Затем через вызов `getSelector()` создаётся экземпляр `Selector`, который используется для перемещения по `Sequence` и выбора каждого элемента.На первый взгляд, `SSelector` может показаться ещё одним внутренним классом. Однако не позволяйте внешнему виду вводить вас в заблуждение. Обратите внимание на методы `end()`, `current()` и `next()`, каждое из которых ссылается на `o`. `o` — это ссылка, которая не является частью `SSelector`, но является приватным полем упакованного класса. Тем не менее, внутренний класс имеет возможность обращаться к методам и полям упакованного класса так же, как если бы он сам владел ими. Эта особенность очень удобна, как вы можете заметить в вышеупомянутом примере.Поэтому теперь мы знаем, что внутренний класс может иметь доступ к членам упакованного класса. Как это работает? Внутренний класс должен иметь ссылку на конкретный объект упакованного класса, который создает этот внутренний класс. Затем, когда мы обращаемся к члену упакованного класса, используем эту (скрытую) ссылку для выбора этого члена. К счастью, компилятор помогает нам управлять всеми этими деталями. Но теперь мы можем понять, почему объект внутреннего класса может быть создан только вместе с объектом упакованного класса. При этом требуется инициализация ссылки на объект упакованного класса. Если эта ссылка недоступна, компилятор сообщит об ошибке. Все эти действия обычно требуют минимального участия со стороны программиста.
## 7.6.4 `static` вложенные классы
Для правильного понимания значения `static` при применении его к вложенным классам важно помнить, что объекты вложенного класса по умолчанию содержат ссылку на объект внешнего класса, который создал этот вложенный класс. Однако если мы говорим, что вложенный класс является `static`, это утверждение неверно. `Static` вложенный класс означает:
(1) Для создания объекта `static` вложенного класса нам не требуется объект внешнего класса.
(2) Мы не можем получить доступ к объекту внешнего класса через объект `static` вложенного класса.Однако существуют некоторые ограничения: поскольку `static` члены могут находиться только на уровне класса, а не внутри него, вложенные классы не могут иметь `static` данных или `static` вложенных классов.
Если для создания объекта вложенного класса не требуется создание объекта внешнего класса, то всё можно сделать `static`. Чтобы обеспечить корректную работу, также следует сделать сам вложенный класс `static`. Пример представлен ниже:
```java
//: Parcel10.java
// Вложенные классы с ключевым словом static
package c07.parcel10;
abstract class Contents {
abstract public int value();
}
interface Destination {
String readLabel();
}
public class Parcel10 {
private static class PContents
extends 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; }
}
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("Танзания");
}
} ///:~
```
В методе `main()`, нам не требуется объект `Parcel10`; вместо этого мы используем обычную синтаксическую конструкцию для выбора `static` члена, чтобы вызвать методы, которые возвращают ссылки на `Contents` и `Destination`.
Обычно мы не помещаем никакого кода в интерфейсы, но `static` вложенные классы могут быть частью интерфейса. Поскольку класс "статичен", он не нарушает правил интерфейсов — `static` вложенные классы находятся только в пространстве имён интерфейса:```java
//: IInterface.java
// Вложенные классы с ключевым словом static внутри интерфейсов
interface IInterface {
static class Inner {
int i, j, k;
public Inner() {}
void f() {}
}
} ///:~
```
В этой части книги ранее было рекомендовано создавать метод `main()` в каждом классе для использования его как тестовой среды для этого класса. Одним из недостатков такого подхода является увеличение количества кода. Вместо этого можно использовать статический внутренний класс для хранения тестового кода, как показано ниже:
```java
//: TestBed.java
// Размещение тестового кода в статическом внутреннем классе
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`. Этот класс может использоваться для тестирования, но не требуется включать его в окончательную версию продукта.
### 7.6.5 Указание объекта внешнего класса
Для создания ссылки на объект внешнего класса используется точка и ключевое слово `this`. Например, в классе `Sequence.SSelector` все методы могут создавать ссылку на внешний класс `Sequence` следующим образом: `Sequence.this`.
Результатом становится ссылка правильного типа, которая проверяется во время компиляции, что позволяет избежать затрат времени выполнения.
```Иногда возникает необходимость указать другому объекту создать экземпляр одного из внутренних классов. Для достижения этой цели необходимо предоставить ссылку на объект внешнего класса в выражении `new`, как показано ниже:
```java
//: Parcel11.java
// Создание внутренних классов
package c07.parcel11;
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("Танзания");
}
} ///:~
```
Прямое создание экземпляра внутреннего класса таким образом, как это могло бы казаться — используя имя внешнего класса `Parcel11` — невозможно. В этом случае необходимо использовать объект внешнего класса для создания экземпляра внутреннего класса. Поэтому, если нет объекта внешнего класса, невозможно создать объект внутреннего класса. Это связано с тем, что объект внутреннего класса "тайно" связывается с объектом внешнего класса. Однако, если создаётся статический внутренний класс, то ссылка на объект внешнего класса больше не требуется.
## 7.6.6 Наследование от внутренних классовПри наследовании от внутреннего класса ситуация усложняется из-за того, что конструктор внутреннего класса должен быть связан с объектом внешнего класса. Проблема заключается в том, что "скрытая" ссылка на объект внешнего класса должна быть проинициализирована, а в производном классе уже нет такого объекта по умолчанию. Для решения этой проблемы используется специальная синтаксическая конструкция:```java
//: 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` расширяет только внутренний класс, но не внешний. При необходимости создания конструктора, просто передача ссылки на объект внешнего класса недостаточно. Вместо этого, в конструкторе используется следующий синтаксис:
```
enclosingClassHandle.super();
```
Это обеспечивает необходимую ссылку для корректной компиляции программы.
## 7.6.7 Можно ли переопределять внутренние классы?
Что произойдёт, если создать внутренний класс, затем наследовать от внешнего класса и переопределить внутренний класс? Возможно ли переопределение внутреннего класса? Этот подход может показаться полезным, однако "переопределение" внутреннего класса — как будто бы это метод внешнего класса — фактически ничего не делает:
```java
//: 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();
}
}
``````По умолчанию конструктор создается компилятором автоматически и вызывает конструктор базового класса. Возможно, вы полагаете, что поскольку создается объект `BigEgg`, то будет использоваться "перехваченный" вариант класса `Yolk`. Однако это не так. Вывод такой:``````
Новое_Яйцо()
Яйцо.Желток()
```
Этот пример демонстрирует, что при наследовании от внешнего класса нет продолжения внутренних классов. Тем не менее, возможно "явное" наследование от внутреннего класса:
```java
//: Большое_Яйцо2.java
// Правильное наследование вложенного класса
class Яйцо2 {
protected class Желток {
public Желток() {
System.out.println("Яйцо2.Желток()");
}
public void f() {
System.out.println("Яйцо2.Желток.f()");
}
}
private Желток y = new Желток();
public Яйцо2() {
System.out.println("Новое_Яйцо2()");
}
public void вставитьЖелток(Желток yy) { y = yy; }
public void g() { y.f(); }
}
public class Большое_Яйцо2 extends Яйцо2 {
public class Желток extends Яйцо2.Желток {
public Желток() {
System.out.println("Большое_Яйцо2.Желток()");
}
public void f() {
System.out.println("Большое_Яйцо2.Желток.f()");
}
}
public Большое_Яйцо2() { вставитьЖелток(new Желток()); }
public static void main(String[] args) {
Яйцо2 e2 = new Большое_Яйцо2();
e2.g();
}
} ///:~
```
Теперь `Большое_Яйцо2.Желток` явно расширяет `Яйцо2.Желток` и переопределяет его методы. Метод `вставитьЖелток()` позволяет `Большому_Яйцу2` передать свой объект `Желток` в качестве ссылки `y` класса `Яйцо2`. Поэтому когда `g()` вызывает `y.f()`, используется переопределённая версия `f()`. Результат вывода следующий:
```
Яйцо2.Желток()
Новое_Яйцо2()
Яйцо2.Желток()
Большое_Яйцо2.Желток()
Большое_Яйцо2.Желток.f()
```Второй вызов `Yajco2.Zheltok()` — это вызов конструктора базового класса в конструкторе `BolshogoYajca2.Zheltok`. При вызове `g()` видно, что используется переопределенная версия `f()`.## 7.6.8 Идентификаторы вложенных классов
Поскольку каждый класс создаёт `.class` файл для хранения информации о том, как создавать объект этого типа (эта информация создаёт метакласс с именем `Class`), можно предположить, что вложенные классы также должны создавать соответствующие `.class` файлы для хранения информации о их `Class` объектах. Названия этих файлов или классов имеют строгое форматирование: имя окружающего класса, за которым следует символ `$`, а затем имя вложенного класса. Например, `.class` файлы, созданные `InheritInner.java`, включают:
```
InheritInner.class
WithInner$Inner.class
WithInner.class
```
Если внутренний класс является анонимным, компилятор просто генерирует числа, используя их в качестве идентификаторов внутренних классов. В случае если внутренние классы вложены друг в друга, их названия просто добавляются после символа `$` и имени внешнего класса.
Этот метод генерации внутренних имен очень прост и прямолинеен, а также "жёсткий" и способен адаптироваться к большинству ситуаций (примечание ③). Поскольку это стандартное имя Java, созданные таким образом файлы автоматически становятся платформо-независимыми (учитывая, что Java компилятор изменяет внутренние классы в зависимости от ситуации, чтобы они работали правильно на различных платформах).③: Однако с другой стороны, поскольку `$` также является метасимволом Unix-оболочки, иногда возникают проблемы при выводе `.class` файлов. Для компании Sun, основанной на Unix, использование такого подхода кажется странным. Мое мнение заключается в том, что они просто не уделили должного внимания этому вопросу, полагая, что мы будем сосредоточены исключительно на исходных файлах.## 7.6.9 Почему использовать внутренние классы: контроль над фреймворками
До настоящего момента вы уже познакомились со множеством синтаксических и концептуальных аспектов работы внутренних классов. Однако эти знания не объясняют причин использования внутренних классов. Почему компания Sun решила так усложнить базовый язык Java 1.1? Ответ заключается в том, что мы здесь называем "контролем над фреймворками"."Прикладной фреймворк" — это один или несколько классов, специально спроектированных для решения определённых типов задач. Чтобы использовать прикладной фреймворк, можно наследовать от одного или нескольких классов и переопределять некоторые из их методов. Код, который вы пишете в переопределённых методах, используется для настройки общих решений, предлагаемых этими прикладными фреймворками, чтобы решить свои собственные конкретные задачи. "Контроль над фреймворками" представляет собой специальный тип прикладного фреймворка, управляемый необходимостью реагировать на события; система, которая главным образом предназначена для реакции на события, называется "системой событий". Один из самых важных вопросов в области языков программирования — это "графический пользовательский интерфейс" (GUI), который почти полностью управляем событиями. Как вы узнаете в Главе 13, Java 1.1 AWT представляет собой контролируемый фреймворк, который идеально решает проблему GUI с помощью внутренних классов. Для понимания того, как внутренние классы упрощают создание и использование контрольных фреймворков, можно считать, что работа контрольного фреймворка заключается в выполнении событий после того, как они станут "готовыми". Хотя значение слова "готовый" может быть различным, в данном случае мы используем время компьютерного часов.Затем следует осознать, что сам фреймворк не содержит никакой конкретной информации о том, что он контролирует. Во-первых, это специальный интерфейс, который описывает все контрольные события. Он также может быть абстрактным классом вместо реального интерфейса. Поскольку по умолчанию управление осуществляется с использованием времени, некоторые детали реализации могут включать:```markdown
## Реализация
### Интерфейсы
#### `ControlEvent`
Интерфейс, описывающий все контрольные события.
```java
public interface ControlEvent {
void execute();
}
```
### Абстрактный Класс
#### `TimeBasedController`
Абстрактный класс, который предоставляет базовое поведение управления по времени.
```java
public abstract class TimeBasedController implements ControlEvent {
private long readyTime;
public TimeBasedController(long readyTime) {
this.readyTime = System.currentTimeMillis() + readyTime;
}
@Override
public void execute() {
if (System.currentTimeMillis() >= readyTime) {
performAction();
}
}
protected abstract void performAction();
}
```
### Пример использования
```java
public class ExampleUsage extends TimeBasedController {
public ExampleUsage(long delayInMillis) {
super(delayInMillis);
}
@Override
protected void performAction() {
// Выполнение действия при готовности
System.out.println("Событие готово к выполнению.");
}
}
// Создание экземпляра ExampleUsage с задержкой 5000 миллисекунд
ExampleUsage example = new ExampleUsage(5000);
// Вызов метода execute()
example.execute();
```
```:~/c07/controller/Event.java
// Основные методы для любого события управления
package c07.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()` вызывается после того, как событие готово к выполнению, а метод `description()` предоставляет текстовое описание события.
Данный файл содержит реальный контрольный фреймворк для управления и триггеринга событий. Первый класс фактически является лишь "помощником" классом, задачей которого является хранение объектов типа `Event`. Его можно заменить любым подходящим набором данных. Кроме того, после прохождения главы 8 вы узнаете о других наборах данных, которые могут упростить нашу работу, не требуя написания этих дополнительных классов:
```java
//: Controller.java
// Вместе с Event, универсальная
// основа для всех систем управления:
package c07.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;
}
}
``````java
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` может содержать до 100 событий (если использовать здесь "реальный" набор из главы 8, то размер будет автоматически изменяться в зависимости от ситуации). `index` используется для отслеживания следующего доступного места, а `next` помогает найти следующее событие в списке и узнать, достигнут ли конец списка. Это особенно важно при вызовах метода `getNext()`, так как после выполнения события объект `Event` удаляется из списка (с помощью метода `removeCurrent()`). Поэтому при каждом вызове `getNext()` мы сталкиваемся с "дырами" в списке.```
Перевод:
```java
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` может содержать до 100 событий (если использовать здесь "реальный" набор из главы 8, то размер будет автоматически изменяться в зависимости от ситуации). Индекс используется для отслеживания следующего доступного места, а `next` помогает найти следующее событие в списке и узнать, достигнут ли конец списка. Это особенно важно при вызовах метода `getNext()`, так как после выполнения события объект `Event` удаляется из списка (с помощью метода `removeCurrent()`). Поэтому при каждом вызове `getNext()` мы сталкиваемся с "дырами" в списке.```Обратите внимание, что `removeCurrent()` не просто указывает на какой-то флаг, который показывает, что объект больше не используется. Вместо этого он устанавливает ссылку равной `null`. Это очень важно, потому что если сборщик мусора обнаруживает, что ссылка все ещё активна, он не удалит объект. Если вы считаете, что ваша ссылка может быть временно заблокирована, лучше установить её значение равным `null`, чтобы сборщик мусора смог корректно удалить объекты.
`Controller` является местом, где происходит реальная работа. Он использует `EventSet` для хранения своих объектов `Event`, и метод `addEvent()` позволяет нам добавлять новые события в этот список. Однако самым важным методом является `run()`. Этот метод проходит через `EventSet`, ищет готовое к выполнению событие (`ready()`), и для каждого найденного события вызывает метод `action()`, выводит описание (`description()`), а затем удаляет событие из списка.
Обратите внимание, что до настоящего момента мы всё ещё не знаем точно, что делает одно конкретное "событие". Именно это является ключевой частью всего дизайна; способность "отличать меняющиеся вещи от незменяемых". Или другими словами, "намерение изменения" приводит к различиям в действиях различных объектов `Event`. Мы можем выразить различные действия путём создания разных подклассов `Event`.Здесь внутренние классы действительно могут помочь:
(1) Они позволяют полностью описать реализацию контрольного фреймворка внутри одного класса, тем самым обеспечивая полное заключение всех связанных деталей. Внутренние классы используются для представления множества различных типов действий, которые решают реальные проблемы. Кроме того, последующие примеры используют приватные внутренние классы, поэтому детали реализации будут скрыты, что позволит безопасно модифицировать код.
(2) Внутренние классы делают нашу конкретную реализацию более гибкой, поскольку они предоставляют удобный доступ ко всем членам внешнего класса. Без этой возможности код мог бы выглядеть менее приятно, и пришлось бы искать альтернативные способы решения задач. Теперь попросите участников рассмотреть конкретную реализацию контрольного фреймворка, который предназначен для управления функциями теплицы (`Greenhouse`, примечание ④). Каждое действие является уникальным: управление светом, поливом и автоматическим регулированием температуры включением/выключением, управление звуковым сигналом и перезапуск системы. Однако цель дизайна контрольного фреймворка заключается в удобной изоляции различных кодовых частей. Для каждого типа действия следует наследовать новый внутренний класс `Event` и реализовать соответствующий контролирующий код внутри метода `action()`.④: Из-за некоторых специфических причин это представляет собой часто возникающую и очень интересную задачу для меня; оригинальный пример также приведён в книге "C++ Inside & Out", но Java предлагает более удобное решение.
Как типичное поведение приложения-фреймворка, класс `GreenhouseControls` наследуется от класса `Controller`:```java
//: GreenhouseControls.java
// This creates a specific application of the control system.
package c07.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() {
// Insert equipment-control code here to turn on the lights physically.
light = true;
}
public String description() {
return "Light turned on";
}
}
private class LightOff extends Event {
public LightOff(long eventTime) {
super(eventTime);
}
public void action() {
// Insert equipment-control code here to turn off the lights physically.
light = false;
}
public String description() {
return "Light turned off";
}
}
private class WaterOn extends Event {
public WaterOn(long eventTime) {
super(eventTime);
}
public void action() {
// Insert equipment-control code here
water = true;
}
public String description() {
return "Water in greenhouse turned on";
}
}
private class WaterOff extends Event {
public WaterOff(long eventTime) {
super(eventTime);
}
public void action() {
// Insert equipment-control code here
water = false;
}
}
``````java
public class WaterShutoff extends Event {
public WaterShutoff(long eventTime) {
super(eventTime);
}
public void action() {
// Insert equipment control code here
waterSystem = "Water in greenhouse disconnected";
}
public String description() {
return "Water in greenhouse disconnected";
}
}
private class ThermostatNight extends Event {
public ThermostatNight(long eventTime) {
super(eventTime);
}
public void action() {
// Insert equipment control code here
thermostat = "Night";
}
public String description() {
return "Thermostat set to night mode";
}
}
private class ThermostatDay extends Event {
public ThermostatDay(long eventTime) {
super(eventTime);
}
public void action() {
// Insert equipment control code here
thermostat = "Day";
}
public String description() {
return "Thermostat set to day mode";
}
}
// Example of an action() that inserts itself into the list of events:
private int rings;
private class Bell extends Event {
public Bell(long eventTime) {
super(eventTime);
}
public void action() {
// Ringing bell every 2 seconds, rings times:
System.out.println("Bong!");
if (--rings > 0)
addEvent(new Bell(System.currentTimeMillis() + 2000));
}
}
```
```markdown
public method description() {
returning value "Bell ringing";
}
private class Restart extends Event {
public Restart(long timeEvent) {
super(timeEvent);
}
public void action() {
long tm = System.currentTimeMillis();
// Instead of hard-coded connection, it could be parsed
```
```markdown
// конфигурационную информацию из текстового файла здесь:
колокола = 5;
добавитьСобытие(новый ТермостатНочь(tm));
добавитьСобытие(новый ВключениеСвета(tm + 1000));
добавитьСобытие(новый ВыключениеСвета(tm + 2000));
добавитьСобытие(новый ВключениеВоды(tm + 3000));
добавитьСобытие(новый ВыключениеВоды(tm + 8000));
добавитьСобытие(новый Колокол(tm + 9000));
добавитьСобытие(новый ТермостатДень(tm + 10000));
// Можно даже добавить объект Перезапуск!
добавитьСобытие(новый Перезапуск(tm + 20000));
}
публичный метод описание() {
возвращаемое значение "Перезапуск системы";
}
}
публичный статический void основной(строка[] аргументы) {
ГринхаусКонтролс gc =
новый ГринхаусКонтролс();
долгое tm = Система.текущееВремяМиллис();
gc.добавитьСобытие(gc.новый Перезапуск(tm));
gc.выполнить();
}
} ///:~
```
Обратите внимание, что имена классов и методов были переведены на русский язык, но остались понятными и логичными в контексте программы. Также были переведены строки внутри методов `описание()` и комментарии внутри метода `action()`. Остальные части кода остались без изменений согласно правилам перевода. Обратите внимание, что поля `light` (освещение), `water` (поставка воды), `thermostat` (регулирование температуры) и `sprinkler` принадлежат внешнему классу `GreenhouseControls`, поэтому внутренние классы могут свободно обращаться к этим полям.Кроме того, большинство методов `action()` также связано с управлением некоторым оборудованием, что обычно требует вызова независимого от Java кода.Большинство классов `Event` выглядят одинаково, но `Bell` (колокол) и `Restart` (перезапуск) являются особыми случаями. Класс `Bell` производит звуковой сигнал; если колокол ещё не прозвучал достаточное количество раз, он добавляет новый объект `Bell` в список событий, чтобы колокол снова прозвучал позже. Обратите внимание, почему внутренний класс всегда кажется таким, будто он использует множественное наследование: `Bell` имеет все методы класса `Event`, а также все методы внешнего класса `GreenhouseControls`.
Класс `Restart` отвечает за инициализацию системы, добавляя все необходимые события. Конечно, более гибкий подход заключается в том, чтобы избежать "жёстко закодированного" программирования и вместо этого читать эти данные из файла (упражнение 10.10 требует модификации данного примера для достижения этой цели). Поскольку `Restart()` является ещё одним объектом типа `Event`, можно добавить объект `Restart` в метод `Restart.action()`, чтобы система могла регулярно перезапускаться. В методе `main()` нам нужно всего лишь создать объект `GreenhouseControls` и добавить объект `Restart`, чтобы запустить систему.Этот пример должен помочь вам лучше понять ценность внутренних классов, особенно при их использовании в контексте управления. Кроме того, во второй половине главы 13 вы узнаете, как использовать внутренние классы для описания поведения графического пользовательского интерфейса. После завершения этих разделов ваше понимание внутренних классов достигнет нового уровня.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )