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 для явного звернення до поля зовнішнього класу.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ