$ sudo teach IT
МОДУЛЬ 9 · УРОК 4

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

Статический вложенный, inner class, локальный, анонимный классы, замыкания и лямбда-выражения вместо анонимных классов

~30 минут Для новичков Java

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

Представьте, что вы создаёте класс 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 вопросов

Анонимный класс

Premium