Вложенные и внутренние классы в Java
Напишите в статическом методе main строку Street street = new Street();, где Street — внутренний класс Town, и компилятор ответит: non-static variable this cannot be referenced from a static context. Причина в главной особенности внутренних классов: их объект не может существовать без объекта внешнего класса.
Вложенный класс (nested class) в Java — это класс, объявленный внутри другого класса. Внутренний класс (inner class) — это нестатический вложенный класс: каждый его объект связан с объектом внешнего класса и имеет доступ ко всем его членам, включая private. Вложенные классы группируют логически связанный код, усиливают инкапсуляцию и часто используются для реализации интерфейсов «на месте».
В Java есть четыре вида вложенных классов:
- Обычный внутренний класс (inner class, member class).
- Локальный класс (local class).
- Анонимный класс (anonymous class).
- Статический вложенный класс (static nested class).
Первые три вида — внутренние классы (inner). Статический вложенный класс внутренним по спецификации JLS §8.1.3 не считается: он не привязан к объекту внешнего класса.
1. Обычный внутренний класс
Обычный внутренний класс (member class) объявляется непосредственно в теле внешнего класса, на одном уровне с полями и методами, без модификатора static.
- Чтобы создать объект внутреннего класса, должен существовать объект внешнего класса.
- Внутренний и внешний классы имеют доступ ко всем членам друг друга, даже
private.
Пример объявления обычного внутреннего класса:
public class Town {
private String postCode = "33333";
public class Street {
private int house;
public void printAddress() {
System.out.println("Город " + Town.this);
System.out.println("Индекс " + postCode);
System.out.println("Улица " + this);
System.out.println("Дом " + house);
}
}
public void createStreet() {
Street street = new Street();
street.house = 78;
street.printAddress();
}
public static void main(String[] args) {
Town town = new Town();
town.createStreet();
Town.Street street1 = town.new Street();
Town.Street street2 = new Town().new Street();
street1.printAddress();
street2.printAddress();
}
} Внутри нестатического метода внешнего класса объект внутреннего класса создаётся как обычно — неявно используется текущий объект this:
Street street = new Street(); Если объект внутреннего класса создаётся вне внешнего класса или в его статическом методе (например, в main), нужен явный объект внешнего класса и синтаксис outer.new Inner():
Town.Street street1 = town.new Street();
Town.Street street2 = new Town().new Street(); Ссылка на текущий объект внутреннего класса — это обычный this:
System.out.println("Street is " + this); Чтобы получить ссылку на объект внешнего класса, пишут имя внешнего класса, точку и this. Это особенно нужно, когда поле внутреннего класса скрывает одноимённое поле внешнего:
System.out.println("Town is " + Town.this); Обычный внутренний класс является таким же членом внешнего класса, как поля и методы. К нему можно применить модификаторы:
finalabstractpublicprivateprotectedstatic— ноstaticпревращает его в статический вложенный классstrictfp
Модификатор strictfp у класса или интерфейса означал, что все вычисления с double и float в его методах дают одинаковый результат на всех платформах.
strictfp устарел
Начиная с Java 17 (JEP 306) вся арифметика с плавающей точкой в Java строгая по умолчанию. Модификатор strictfp по-прежнему допустим синтаксически, но ничего не меняет, и компилятор выдаёт предупреждение о его избыточности.
2. Локальный класс
Локальный класс (local class) — это класс, объявленный внутри блока кода, чаще всего внутри метода. Он виден только в этом блоке. Если локальный класс объявлен в нестатическом методе, он связан с объектом внешнего класса и имеет доступ к его полям и методам.
Локальный класс может обращаться к локальным переменным и параметрам метода, только если они объявлены final или являются effectively final (начиная с Java 8). Effectively final переменная — это переменная без модификатора final, значение которой после инициализации не меняется.
Правила для локальных классов:
- Объект локального класса можно создать только в том же блоке и только ниже объявления класса.
- Локальный класс нельзя объявить как
private,public,protectedилиstatic. - Локальный класс может быть
abstractилиfinal.
Пример локального класса:
public class Town2 {
private String postCode = "33333";
public void createAddress() {
int houseNumber = 34; // effectively final, можно без final
class Street {
public void printAddress() {
System.out.println("PostCode is " + postCode);
System.out.println("House Number is " + houseNumber);
}
}
Street street = new Street();
street.printAddress();
}
public static void main(String[] args) {
Town2 town = new Town2();
town.createAddress();
}
} Если локальный класс объявлен внутри статического метода, объекта внешнего класса у него нет. Поэтому он видит только статические поля внешнего класса и (effectively) final локальные переменные метода:
public class Town3 {
private static String postCode = "33333";
public static void createAddress() {
final int houseNumber = 34;
class Street {
public void printAddress() {
System.out.println("PostCode is " + postCode);
System.out.println("House Number is " + houseNumber);
}
}
Street street = new Street();
street.printAddress();
}
public static void main(String[] args) {
Town3.createAddress();
}
} Что изменилось в Java 16
До Java 16 внутренние и локальные классы не могли содержать статических членов, кроме констант static final. Начиная с Java 16 (JEP 395, вместе с записями record) это ограничение снято: статические поля и методы разрешены, а внутри метода можно объявлять также локальные record, enum и interface. В старых учебниках и тестах по Java 8/11 правило «только константы» всё ещё встречается.
3. Анонимный класс
Анонимный класс (anonymous class) в Java — это локальный класс без имени, который объявляется и создаётся одним выражением. Он либо наследует класс, либо реализует интерфейс и обычно переопределяет один-два метода прямо в месте использования.
Синтаксис анонимного класса:
ТипРодителя переменная = new ТипРодителя(аргументы конструктора) {
// поля, методы, блоки инициализации
}; Особенности анонимных классов:
- У анонимного класса нет имени, поэтому нет и конструктора. Аргументы в
new Parent(...)передаются конструктору родительского класса, а собственную инициализацию выполняют в блоке инициализации{ ... }. - Анонимный класс может либо расширить один класс, либо реализовать один интерфейс — но не то и другое одновременно.
- Его можно объявить в методе, в инициализаторе поля или прямо в аргументе вызова метода.
- Как и локальный класс, он видит только final или effectively final локальные переменные.
Анонимный класс, наследующий класс
// Potato.java
public class Potato {
public void peel() {
System.out.println("Чистим картошку.");
}
}
// Food.java
public class Food {
public static void main(String[] args) {
Potato potato = new Potato() {
@Override
public void peel() {
System.out.println("Чистим картошку в анонимном классе.");
}
};
potato.peel();
}
} Анонимный класс может не только переопределить методы родительского класса, но и добавить новые. Однако через переменную типа родителя новые методы вызвать нельзя — компилятор знает только тип Potato:
public class AnotherFood {
public static void main(String[] args) {
Potato potato = new Potato() {
public void fry() {
System.out.println("Жарим картошку в анонимном классе.");
}
@Override
public void peel() {
System.out.println("Чистим картошку в анонимном классе.");
fry();
}
};
potato.peel();
// Ошибка компиляции: в классе Potato нет метода fry()
// potato.fry();
}
} Неочевидный момент с var
Если объявить переменную через var (Java 10+), её типом станет сам анонимный класс, и новый метод становится доступен: var potato = new Potato() { void fry() { } }; potato.fry(); компилируется. С явным типом Potato — нет.
Анонимный класс, реализующий интерфейс
Анонимный класс может реализовать интерфейс — тогда он обязан реализовать все его абстрактные методы:
// Moveable.java
public interface Moveable {
void moveRight();
void moveLeft();
}
// MoveableExample.java
public class MoveableExample {
public static void main(String[] args) {
Moveable moveable = new Moveable() {
@Override
public void moveRight() {
System.out.println("MOVING RIGHT!!!");
}
@Override
public void moveLeft() {
System.out.println("MOVING LEFT!!!");
}
};
moveable.moveRight();
moveable.moveLeft();
}
} Анонимный класс как аргумент метода
Анонимный класс можно создать прямо в аргументе вызова. Классический пример — компаратор для сортировки. Блок { ... } внутри класса играет роль конструктора:
List<String> names = new ArrayList<>(List.of("Анна", "Ян", "Кирилл"));
names.sort(new Comparator<>() { // diamond с анонимным классом - с Java 9
{
System.out.println("Блок инициализации вместо конструктора");
}
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
System.out.println(names); // [Ян, Анна, Кирилл] Анонимный класс или лямбда-выражение
Если интерфейс функциональный (один абстрактный метод, например Runnable или Comparator), вместо анонимного класса обычно пишут лямбда-выражение:
// Анонимный класс
Runnable task1 = new Runnable() {
@Override
public void run() {
System.out.println("Работаю");
}
};
// То же самое лямбдой
Runnable task2 = () -> System.out.println("Работаю"); Анонимный класс по-прежнему нужен, когда:
- надо унаследовать класс (в том числе абстрактный), а не интерфейс;
- в интерфейсе несколько абстрактных методов;
- нужны собственные поля (состояние) или дополнительные методы;
thisдолжен указывать на сам объект реализации: в анонимном классеthis— это анонимный объект, а в лямбде — объект внешнего класса.
Анонимный класс уместен, если тело класса короткое, нужен ровно один экземпляр, класс используется только в месте создания и отдельное имя не облегчает понимание кода.
4. Статический вложенный класс
Статический вложенный класс (static nested class) — это вложенный класс, объявленный с модификатором static. Он не связан с объектом внешнего класса, поэтому формально внутренним (inner) классом не является.
Статический вложенный класс напрямую видит только статические члены внешнего класса (в том числе private static). К нестатическим полям и методам он может обратиться лишь через ссылку на объект внешнего класса. По поведению он похож на обычный класс верхнего уровня, который просто «упакован» внутрь другого класса для удобства группировки. Типичный пример — Map.Entry или класс Builder в паттерне Строитель.
Примеры объявления статических вложенных классов:
// Town4.java
public class Town4 {
public static class Street {
public void go() {
System.out.println("Go to the Street.");
}
}
}
// City.java
public class City {
public static class District {
public void go() {
System.out.println("Go to the District.");
}
}
public static void main(String[] args) {
Town4.Street street = new Town4.Street(); // объект Town4 не нужен
street.go();
District district = new District();
district.go();
}
} Может ли внутренний класс существовать без внешнего
Это частый вопрос на тестах и собеседованиях, и ответ зависит от вида вложенного класса:
- Внутренний (нестатический) класс — нет. Объект внутреннего класса не может существовать без объекта класса, в котором он содержится: он хранит на него скрытую ссылку (
Town.this). Поэтому создать его можно только какtown.new Street()или из нестатического метода внешнего класса. - Статический вложенный класс — да. Его объект создаётся без объекта внешнего класса:
new Town4.Street(). Внешний класс для него — фактически пространство имён.
При этом сам исходный код любого вложенного класса всегда находится внутри внешнего класса: без объявления внешнего класса вложенный класс не существует даже как тип.
Сравнение видов вложенных классов
| Вид | Где объявлен | Нужен объект внешнего класса | Доступ к членам внешнего класса | Модификаторы доступа |
|---|---|---|---|---|
| Обычный внутренний | В теле класса, без static | Да | Ко всем, включая private | public, protected, private, по умолчанию |
| Локальный | В методе или блоке | Да, если метод нестатический | Ко всем + к effectively final локальным переменным | Нельзя |
| Анонимный | В выражении new | Да, если создан в нестатическом контексте | Ко всем + к effectively final локальным переменным | Нельзя |
| Статический вложенный | В теле класса, с static | Нет | Только к статическим (к остальным через ссылку) | public, protected, private, по умолчанию |
Как компилируются вложенные классы
Для JVM вложенных классов не существует: компилятор превращает каждый из них в отдельный .class-файл с символом $ в имени:
Town$Street.class— обычный внутренний или статический вложенный классStreet;Town2$1Street.class— локальный классStreet(номер добавляется, потому что в разных методах могут быть локальные классы с одинаковым именем);Food$1.class,Food$2.class— анонимные классы, пронумерованные по порядку.
Ссылку на внешний объект компилятор передаёт во внутренний класс через скрытый параметр конструктора и сохраняет в синтетическом поле.
Типичные промахи с вложенными классами
1. Создание внутреннего класса из статического контекста. new Street() в main не компилируется, если Street нестатический. Нужен объект внешнего класса (town.new Street()) — или сделайте класс static, если ему не нужны поля внешнего объекта.
2. Изменение захваченной локальной переменной. Локальный и анонимный классы видят только effectively final переменные:
int count = 0;
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(count); // ошибка компиляции
}
};
count++; // из-за этой строки count больше не effectively final 3. Утечка памяти через скрытую ссылку. Каждый объект внутреннего или анонимного класса держит ссылку на внешний объект. Если такой объект живёт долго (слушатель событий, задача в пуле потоков, элемент кэша), сборщик мусора не сможет удалить и внешний объект. Правило из «Effective Java»: если вложенному классу не нужен доступ к объекту внешнего класса, объявляйте его static.
Часто задаваемые вопросы
Может ли внутренний класс существовать без класса, в котором он содержится?
Объект внутреннего (нестатического) класса — нет: он всегда связан с объектом внешнего класса и создаётся как outer.new Inner(). Объект статического вложенного класса — да: он создаётся как new Outer.Nested() без объекта внешнего класса.
Можно ли объявить конструктор в анонимном классе?
Нет, у анонимного класса нет имени, а конструктор должен называться как класс. Аргументы в new Parent(args) { ... } передаются конструктору родителя, а свою инициализацию пишут в блоке инициализации { ... } внутри тела анонимного класса.
Когда делать вложенный класс static?
Всегда, когда ему не нужен доступ к полям и методам объекта внешнего класса. Статический вложенный класс не хранит скрытую ссылку на внешний объект, поэтому экономит память, не мешает сборке мусора и создаётся без объекта внешнего класса.
Может ли внутренний класс иметь статические поля и методы?
До Java 16 — только константы static final. Начиная с Java 16 внутренние, локальные и анонимные классы могут объявлять любые статические члены. На экзаменах по старым версиям Java правильным считается первый вариант.
Может ли анонимный класс одновременно наследовать класс и реализовать интерфейс?
Нет. Анонимный класс либо расширяет ровно один класс, либо реализует ровно один интерфейс. Если нужно и то и другое, объявите локальный или обычный внутренний класс с именем.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии