Внутренние классы
Статический вложенный, inner class, локальный, анонимный классы, замыкания и лямбда-выражения вместо анонимных классов
Зачем нужны внутренние классы?
Представьте, что вы создаёте класс Engine, который используется только внутри класса Car. Стоит ли выносить Engine в отдельный файл? Если он нужен только для Car — нет. Внутренние классы позволяют объявлять класс внутри другого класса, сохраняя инкапсуляцию и логическую связанность.
В Java существует четыре типа внутренних классов:
- Статический вложенный класс (static nested class)
- Внутренний класс (inner class / non-static nested class)
- Локальный класс (local class)
- Анонимный класс (anonymous class)
1. Статический вложенный класс
Статический вложенный класс — это класс, объявленный внутри другого класса с модификатором static. Он не имеет доступа к нестатическим полям и методам внешнего класса.
public class Car {
private String brand;
private Engine engine;
public Car(String brand, int horsepower) {
this.brand = brand;
this.engine = new Engine(horsepower);
}
public String getInfo() {
return brand + " с " + engine.getHorsepower() + " л.с.";
}
static class Engine {
private int horsepower;
Engine(int horsepower) {
this.horsepower = horsepower;
}
public int getHorsepower() {
return horsepower;
}
public String describe() {
return "Двигатель мощностью " + horsepower + " л.с.";
}
}
}
public class CarDemo {
public static void main(String[] args) {
Car car = new Car("Toyota", 150);
System.out.println(car.getInfo());
Car.Engine engine = new Car.Engine(200);
System.out.println(engine.describe());
}
}
Вывод программы:
Toyota с 150 л.с.
Двигатель мощностью 200 л.с.
Как создать экземпляр статического вложенного класса: new Car.Engine(200) — через имя внешнего класса.
Аналогия: Статический вложенный класс — это как гараж в доме. Гараж физически внутри дома, но он не зависит от того, кто находится в комнатах. Вы можете использовать гараж независимо.
2. Внутренний класс (inner class)
Внутренний класс (non-static) имеет доступ к нестатическим полям и методам внешнего класса. Это ключевое отличие от статического вложенного класса.
public class ShoppingCart {
private String customerName;
private int itemCount = 0;
public ShoppingCart(String customerName) {
this.customerName = customerName;
}
public void addItem(String name, double price) {
itemCount++;
Item item = new Item(name, price);
System.out.println(customerName + " добавил: " + item);
}
public int getItemCount() {
return itemCount;
}
class Item {
private String name;
private double price;
Item(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return name + " (的商品 #" + itemCount + ")";
}
public double getPrice() {
return price;
}
}
}
public class CartDemo {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart("Алиса");
cart.addItem("Ноутбук", 75000);
cart.addItem("Мышь", 1500);
cart.addItem("Клавиатура", 3000);
System.out.println("Всего товаров: " + cart.getItemCount());
}
}
Вывод программы:
Алиса добавил: Ноутбук (的商品 #1)
Алиса добавил: Мышь (的商品 #2)
Алиса добавил: Клавиатура (的商品 #3)
Всего товаров: 3
Обратите внимание: Item обращается к полю itemCount внешнего класса — это возможно только для non-static внутренних классов.
Аналогия: Inner class — это как комната в квартире. Она не может существовать без квартиры и знает всё, что происходит в квартире: сколько людей, какая мебель, какой порядок.
Как inner class связан с внешним
Каждый экземпляр inner class хранит неявную ссылку на экземпляр внешнего класса. Это означает:
- Создать inner class можно только из экземпляра внешнего класса
- Inner class может обращаться к полям и методам внешнего экземпляра
- Пока существует inner class, не может быть собран внешний объект (утечка памяти!)
public class Outer {
private String message = "Привет от внешнего класса!";
class Inner {
void showMessage() {
System.out.println(message);
System.out.println("Я — inner класс!");
}
}
}
public class OuterDemo {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.showMessage();
}
}
Вывод программы:
Привет от внешнего класса!
Я — inner класс!
Как создать экземпляр inner class: outer.new Inner() — через экземпляр внешнего класса.
3. Локальный класс
Локальный класс объявляется внутри метода. Он виден только в этом методе и может обращаться к переменным метода (если они effectively final).
public class LocalClassDemo {
private String outerField = "Поле внешнего класса";
public void process(final String methodParam) {
final String localVar = "Локальная переменная";
class Processor {
void run() {
System.out.println(outerField);
System.out.println(methodParam);
System.out.println(localVar);
}
}
Processor p = new Processor();
p.run();
}
public static void main(String[] args) {
LocalClassDemo demo = new LocalClassDemo();
demo.process("Параметр метода");
}
}
Вывод программы:
Поле внешнего класса
Параметр метода
Локальная переменная
Локальный класс полезен, когда вам нужен класс仅 для одного метода и он обращается к локальным переменным.
4. Анонимный класс
Анонимный класс — это класс без имени. Он объявляется и создаётся одновременно. Чаще всего используется для реализации интерфейсов или расширения классов «на ходу»:
public interface Greeting {
String greet(String name);
}
public class AnonymousDemo {
public static void main(String[] args) {
Greeting hello = new Greeting() {
@Override
public String greet(String name) {
return "Привет, " + name + "!";
}
};
Greeting farewell = new Greeting() {
@Override
public String greet(String name) {
return "До свидания, " + name + "!";
}
};
System.out.println(hello.greet("Алиса"));
System.out.println(farewell.greet("Алиса"));
}
}
Вывод программы:
Привет, Алиса!
До свидания, Алиса!
Анонимный класс может также наследовать класс:
abstract class Animal {
abstract String sound();
}
Animal cat = new Animal() {
@Override
String sound() {
return "Мяу!";
}
};
Animal dog = new Animal() {
@Override
String sound() {
return "Гав!";
}
};
System.out.println(cat.sound());
System.out.println(dog.sound());
Вывод программы:
Мяу!
Гав!
Аналогия: Анонимный класс — это как одноразовая маска. Вы надеваете её, делаете дело, и больше она не нужна. Никто не знает, кто был под маской — главное, что функция выполнена.
Замыкание (closure)
Внутренние классы могут захватывать переменные из окружающего контекста. Это называется замыканием (closure). Переменные должны быть final или effectively final (не изменяться после инициализации):
public class Counter {
public Runnable createCounter(int start) {
int[] count = {start};
return new Runnable() {
@Override
public void run() {
count[0]++;
System.out.println("Счётчик: " + count[0]);
}
};
}
}
public class ClosureDemo {
public static void main(String[] args) {
Counter counter = new Counter();
Runnable r = counter.createCounter(0);
r.run();
r.run();
r.run();
}
}
Вывод программы:
Счётчик: 1
Счётчик: 2
Счётчик: 3
Мы использовали массив int[] вместо простой переменной, потому что массив — объект, и его можно изменять (менять содержимое), хотя сама ссылка на массив остаётся effectively final.
Лямбда вместо анонимного класса
Начиная с Java 8, для интерфейсов с одним методом (functional interfaces) можно использовать лямбда-выражения вместо анонимных классов:
public interface Greeting {
String greet(String name);
}
public class LambdaDemo {
public static void main(String[] args) {
Greeting hello = name -> "Привет, " + name + "!";
Greeting farewell = name -> "До свидания, " + name + "!";
System.out.println(hello.greet("Алиса"));
System.out.println(farewell.greet("Алиса"));
}
}
Сравните:
// Анонимный класс — 5 строк:
Greeting hello = new Greeting() {
@Override
public String greet(String name) {
return "Привет, " + name + "!";
}
};
// Лямбда — 1 строка:
Greeting hello = name -> "Привет, " + name + "!";
Лямбда-выражения — это компактный способ создания анонимных классов для functional interfaces.
Сравнение типов внутренних классов
| Тип | Где объявлен | Доступ к внешнему | Создание |
|---|---|---|---|
| Static nested | В классе | Только static | new Outer.Inner() |
| Inner class | В классе | Все | outer.new Inner() |
| Local class | В методе | Все + final переменные | Внутри метода |
| Anonymous class | Внутри выражения | Все + final переменные | new Interface() {...} |
Практический пример: сортировка
Внутренние классы часто используются для компараторов и обработчиков событий:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class SortDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Борис");
names.add("Алиса");
names.add("Виктор");
names.add("Григорий");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
System.out.println("По длине: " + names);
names.sort((a, b) -> a.compareTo(b));
System.out.println("По алфавиту: " + names);
}
}
Вывод программы:
По длине: [Алиса, Борис, Виктор, Григорий]
По алфавиту: [Алиса, Борис, Виктор, Григорий]
Когда использовать?
- Static nested — логически связанные классы, не требующие доступа к состоянию внешнего
- Inner class — когда класс должен работать с данными внешнего объекта
- Local class — редко, когда нужен класс仅 для одного метода
- Anonymous class — для интерфейсов с одним методом (лучше лямбда)
- Лямбда — для functional interfaces (предпочтительный способ)
Итоги урока
Итоги урока
- Статический вложенный класс — не зависит от экземпляра внешнего класса
- Inner class — имеет доступ ко всем полям и методам внешнего класса
- Локальный класс — объявляется внутри метода, виден только там
- Анонимный класс — класс без имени, создаётся и используется одновременно
- Замыкание — захват переменных из окружающего контекста (effectively final)
- Лямбда — компактная замена анонимного класса для functional interfaces
- Внутренние классы улучшают инкапсуляцию и организацию кода
Следующий уrok: Исключения: try-catch-finally →
Тест: Внутренние классы
5 вопросов