JavaRush /Курси /JAVA 25 SELF /Внутрішні класи (non-static inner)

Внутрішні класи (non-static inner)

JAVA 25 SELF
Рівень 16 , Лекція 0
Відкрита

1. Що таке внутрішній клас (inner class)?

У Java клас може бути оголошений не лише на верхньому рівні (у файлі), а й усередині іншого класу. Такий клас називають вкладеним (nested class). Якщо його оголошено без модифікатора static, то це внутрішній (non-static inner class, або просто inner class).

Внутрішній клас — це клас, який оголошено всередині іншого класу й який повʼязаний із екземпляром цього зовнішнього класу. Він може звертатися до всіх полів і методів зовнішнього класу, навіть якщо вони private. Це ніби в обʼєкта є свій «секретний помічник», якому дозволено все!

Це приблизно як будинок із кімнатами. Будинок — це зовнішній клас, а кімната — внутрішній клас. Кімната не може існувати без будинку, проте має доступ до його ресурсів: світла, опалення, меблів. Якщо будинок зникне, зникне й кімната. Так само працюють внутрішні класи в Java.

Навіщо потрібні внутрішні класи?

  • Логічний зв’язок: коли один клас потрібен лише для роботи з іншим класом і не має сенсу поза ним.
  • Інкапсуляція: дає змогу приховати деталі реалізації, не засмічуючи простір імен пакета.
  • Доступ до приватних членів: внутрішній клас може звертатися до приватних полів і методів зовнішнього класу.
  • Компактність: зменшує кількість «зайвих» класів на рівні пакета.

Синтаксис оголошення внутрішнього класу

Оголосити внутрішній клас дуже просто: він оголошується всередині тіла іншого класу, без модифікатора static.

class Outer {
    // поля і методи зовнішнього класу

    class Inner {
        // поля і методи внутрішнього класу
        void printHello() {
            System.out.println("Привіт від Inner!");
        }
    }
}

Тут Inner — внутрішній клас щодо Outer.

Схема: візуалізація

Outer
│
├─ поля/методи
│
└─ Inner (внутрішній клас)
      └─ власні поля/методи

2. Як створити екземпляр внутрішнього класу

Саме тут криється одна з найпоширеніших пасток для новачків! Екземпляр внутрішнього класу завжди повʼязаний із конкретним обʼєктом зовнішнього класу.

Приклад:

Outer outer = new Outer();             // створюємо обʼєкт зовнішнього класу
Outer.Inner inner = outer.new Inner(); // створюємо внутрішній клас через обʼєкт зовнішнього!
inner.printHello();                    // Привіт від Inner!

Спроба створити просто new Inner() призведе до помилки компіляції, оскільки Java не знає, з яким обʼєктом зовнішнього класу повʼязати цей внутрішній.

Чому так?

Внутрішній клас може звертатися до полів і методів зовнішнього класу. Тому він має «знати», із яким саме обʼєктом зовнішнього класу працює.

3. Приклад використання внутрішнього класу

Розгляньмо приклад, де внутрішній клас справді корисний.

Приклад 1: Модель «Рюкзак і предмети»

Припустімо, у нас є клас Backpack, який може зберігати предмети (Item). Хочемо, щоб клас Item був доступний лише всередині Backpack, оскільки поза рюкзаком предмети нас не цікавлять.

public class Backpack {
    private String owner;

    public Backpack(String owner) {
        this.owner = owner;
    }

    // Внутрішній клас — предмет може існувати лише в рюкзаку!
    class Item {
        private String name;

        public Item(String name) {
            this.name = name;
        }

        public void printInfo() {
            // Магія! Бачимо приватне поле зовнішнього класу
            System.out.println(owner + " має предмет: " + name);
        }
    }
}

Використання:

Backpack bp = new Backpack("Василь");
Backpack.Item item = bp.new Item("Підручник Java");
item.printInfo(); // Василь має предмет: Підручник Java

Зверніть увагу: Item може звертатися до поля owner, навіть якщо воно private!

Приклад 2: Ітератор для власної колекції

У Java ітератори колекцій часто реалізують як внутрішні класи. Спробуймо створити власну просту колекцію з внутрішнім класом-ітератором.

public class IntList {
    private int[] data = new int[10];
    private int size = 0;

    public void add(int value) {
        data[size++] = value;
    }

    // Внутрішній клас — ітератор!
    class Iterator {
        private int index = 0;

        public boolean hasNext() {
            return index < size;
        }

        public int next() {
            return data[index++];
        }
    }
}

Використання:

IntList list = new IntList();
list.add(10);
list.add(20);

IntList.Iterator it = list.new Iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

4. Особливості внутрішніх класів

Доступ до членів зовнішнього класу

Внутрішній клас може звертатися до будь-яких (навіть private) полів і методів зовнішнього класу. Це зручно, але потребує обережності: якщо зовнішній клас зміниться, внутрішній може раптово «зламатися».

class Outer {
    private int secret = 42;

    class Inner {
        void showSecret() {
            System.out.println("Таємниця: " + secret);
        }
    }
}

Внутрішній клас не може містити статичні члени

Внутрішній (non-static inner) клас не може містити статичні поля або методи, окрім статичних констант (наприклад, public static final int MY_CONST = 123;). Це повʼязано з тим, що внутрішній клас завжди «привʼязаний» до екземпляра зовнішнього класу.

Якщо вам потрібно оголосити статичний вкладений клас — використовуйте модифікатор static (про це — у наступній лекції).

Видимість внутрішнього класу

Внутрішній клас може бути private, protected, public або мати видимість рівня пакета (package-private).

public class Outer {
    private class Inner { /* ... */ }
}

5. Корисні нюанси

Коли використовувати внутрішні класи

Внутрішні класи — не для краси, а для логічної організації коду. Використовуйте їх, коли:

  • Клас потрібен лише в одному місці (наприклад, допоміжний ітератор, обробник, частина складної структури).
  • Клас тісно повʼязаний із зовнішнім класом і не має сенсу поза ним.
  • Хочете приховати деталі реалізації від інших класів пакета.

Не використовуйте внутрішні класи лише заради моди! Іноді краще винести клас на верхній рівень, якщо він може бути корисним в інших місцях.

Внутрішній клас і this

Всередині внутрішнього класу можна звернутися до полів і методів зовнішнього класу за допомогою OuterClassName.this.

class Car {
    private String model = "Tesla";

    class Engine {
        void printModel() {
            // Явно звертаємося до зовнішнього обʼєкта
            System.out.println("Модель: " + Car.this.model);
        }
    }
}

Такий синтаксис особливо корисний, якщо внутрішній і зовнішній класи мають поля з однаковими іменами.

Коли НЕ варто використовувати внутрішні класи

Не використовуйте, якщо:

  • Внутрішній клас не звертається до полів/методів зовнішнього класу.
  • Клас може бути корисним в інших частинах програми.
  • Зовнішній клас стає надто великим і складним.

У таких випадках краще:

  • Зробити клас статичним вкладеним (static nested class).
  • Винести клас в окремий файл.
  • Використати звичайні методи замість цілого класу.

6. Типові помилки під час роботи з внутрішніми класами

Помилка № 1: Спроба створити внутрішній клас без обʼєкта зовнішнього класу.
Якщо написати new Inner(), компілятор видасть помилку: «No enclosing instance of type Outer is accessible». Завжди створюйте внутрішній клас через обʼєкт зовнішнього класу: outer.new Inner().

Помилка № 2: Спроба оголосити статичні поля або методи у внутрішньому класі.
Внутрішній клас не може містити статичні члени (окрім констант). Якщо потрібно — використовуйте статичний вкладений клас, тобто оголосіть клас із модифікатором static.

Помилка № 3: Надмірне використання внутрішніх класів.
Якщо внутрішній клас не використовує поля/методи зовнішнього класу, імовірно, він має бути static або взагалі винесений назовні. Не створюйте внутрішні класи «про всяк випадок».

Помилка № 4: Плутанина зі зверненням до полів зовнішнього класу.
Якщо внутрішній і зовнішній класи мають поля з однаковими іменами, використовуйте OuterClassName.this.field для явного звернення до поля зовнішнього класу.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ