Вложенный (иначе — внутренний) класс в объектно-ориентированных языках программирования — это такой класс,объявленный внутри другого класса.
Это позволяет объединять логически связанные между собой классы и увеличивать таким образом инкапсуляцию, а заодно делать код более поддерживаемым и лаконичным.
Далее рассмотрим четыре разновидности вложенных классов.
Вложенный класс определяется так же, как и любой другой класс:
package com.mypackage;
public class Outer {
public static class Nested {
// ...
}
}
Как и всякий статический элемент, статический класс привязан к самому классу, а не к его экземпляру. Это означает, что мы можем создать экземпляр вложенного класса, не создавая промежуточный экземпляр внешнего класса Outer
.
Такой класс ничем не отличается от любого другого и работает по тем же принципам. В нем:
Для простоты можно даже импортировать вложенный класс, чтобы избавиться от префикса внешнего класса:
import com.mypackage.Outer.Nested; Nested instance = new Nested(); ПрименениеСтатические вложенные классы ведут себя так же, как и прочие классы верхнего уровня, и должны рассматриваться в качестве таковых. Главное преимущество здесь — удобство упаковки.
Нестатические вложенные классы или внутренние классы:
public class Outer {
public class Nested {
// ...
}
}
Внутренний вложенный класс (Nested
) привязан не к внешнему (Outer
) классу, а к экземплярам своего заключающего класса.
Благодаря этому виду отношений он имеет доступ ко всем членам заключающего класса, а не только к статическим. Но внутренний класс не может сам определять статические члены.
Для создания экземпляра внутреннего класса требуется экземпляр его заключающего класса:
Outer outer = new Outer();
Outer.Nested nested = outer.new Nested();
// ТАК ДЕЛАТЬ НЕЛЬЗЯ!
Outer.Nested nested = new Outer.Nested();
Больше нет какого-то одного типа, который “просто” вложен в другой класс. Внутренний класс тесно связан с реальным экземпляром своего заключающего класса и больше не может существовать сам по себе.
СериализацияВложенный класс не становится сериализуемым автоматически только потому, что заключающий класс является сериализуемым.
Как и в случае с любыми другими членами, нам следует убедиться, что в нем также реализован java.io.Serializable
. Или в конечном счете мы получим исключение java.io.NotSerializableException
.
Преимущество внутренних классов — более глубокая связь с их заключающим классом, в том числе полный доступ ко всем его членам. Но эта связь может привести к неочевидному удержанию памяти. Заключающий класс не может быть обработан сборщиком мусора раньше, чем экземпляр вложенного класса.
РесурсыВнутренние классы и заключающие экземпляры (JLS).
Локальные классы — особая разновидность внутренних классов.
Локальный класс можно определить внутри любого блока кода (например, метода):
public class MyClass {
public void run() {
class MyLocalClass {
// ...
}
}
}
По аналогии с внутренним классом, локальный класс получает доступ ко всем членам заключающего класса, но при этом мы не можем предоставить локальному классу модификатор, потому что он используется исключительно “локально”.
ПрименениеРезультат здесь будет таким же, как с внутренними классами, но внутренний класс не способен привязать логику к определенному блоку настолько сильно, как локальный класс.
С помощью локальных классов можно легко группировать логически связанные элементы, и при их применении футпринт будет наименьшим из возможных.
РесурсыОбъявление локальных классов (JLS).
Анонимные классы создаются не при объявлении вложенного класса, а при создании экземпляра уже существующего типа:
Runnable runnable = new Runnable() {
@Override
public void run() {
// ...
}
};
Мы только что создали новый класс на основе интерфейса Runnable
, и этот класс анонимный — у него нет имени.
Для создания анонимных классов используются не только интерфейсы. Таким же образом можно расширить другие классы, не имеющие свойства final
:
List<String> customStringBuilder = new ArrayList<>(10) {
public boolean add(String value) {
System.out.println("Adding value: " + value);
return super.add(value);
}
// ...
};
Специализированная реализация List<String>
— для которой вообще не нужно создавать отдельный класс. Красота!
Синтаксис создания всегда следует одной и той же структуре:
new <<Тип>>(<<аргументы конструктора>>) {
// объявления / переопределения
};
Анонимные классы объявляются через выражения, и выражения обязательно должны быть частью оператора — либо в блоке, либо в самом объявлении члена.
Анонимные классы против лямбдС появлением лямбда-выражений у нас, наконец, появился способ более простой реализации типов “на месте»:
// АНОНИМНЫЙ КЛАСС
Predicate<String> anonymous = new Predicate<String>() {
@Override
public boolean test(String t) {
return t != null;
}
};
// ЛЯМБДА-ВЫРАЖЕНИЕ
Predicate<String> lambda = (input) -> input != null;
Функционально оба предиката идентичны, поэтому легко подумать, что лямбды — просто синтаксический сахар для анонимных классов. Однако отличия в сгенерированном байт-коде показывают, что этот код может вести себя так же, но при этом устроен иначе:
// АНОНИМНЫЙ КЛАСС
0: new #2 // class Anonymous$1
3: dup
4: invokespecial #3 // Method Anonymous$1."<init>":()V
7: astore_1
8: return
// ЛЯМБДА
0: invokedynamic #2, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
5: astore_1
6: return
Лямбды используют операционный код invokedynamic
, который позволяет JVM вызывать методы более динамическим образом.
Анонимные классы отлично подходят для небольших, конкретных реализаций “на месте”, даже если лямбда-выражения тоже с этим бы справились.
Также из-за своей простоты у анонимных классов много недостатков по сравнению с локальными или внутренними:
Затенение (shadowing) в разработке программного обеспечения означает повторное объявление члена в более глубокой области видимости. Это означает, что во вложенных классах мы можем переиспользовать имена переменных, а также получать доступ к затемненному члену с помощью префикса вызова:
public class Outer {
String stringVal = "Outer class";
public class Nested {
String stringVal = "Nested Class";
public void run() {
System.out.println("Nested stringVal = " + this.stringVal);
System.out.println("Outer stringVal = " + Outer.this.stringVal);
}
}
}
Логическое группирование классов и создание анонимных экземпляров на месте — отличная возможность. Но следует тщательно взвешивать, какой тип вложенного класса необходим в конкретной ситуации. Особенно это актуально для функциональных интерфейсов, где более грамотным решением является применение лямбда-выражений.
Перевод статьи Ben Weidig: “Nested Classes in Java”
Комментарии