Привіт! Сьогодні ми почнемо розглядати важливу тему — роботу вкладених класів у Java. Англійською вони називаються nested classes. Java дозволяє створювати одні класи всередині інших:

class OuterClass {
    ...
    static class StaticNestedClass {
        ...
    }
    class InnerClass {
        ...
    }
}
Саме такі класи і називаються вкладеними. Вони діляться на 2 види:
  1. Non-static nested classes — нестатичні вкладені класи. Їх ще називають inner classes — внутрішні класи.
  2. Static nested classes — статичні вкладені класи.
В свою чергу, внутрішні класи (inner classes) мають два особливих підвиди. Окрім того, що внутрішній клас може бути просто внутрішнім, він ще буває:
  • локальним класом (local class)
  • анонімним класом (anonymous class)
Складнувато? :) Нічого страшного, ось тобі схема для наочності. Повертайся до неї під час лекції, якщо раптом відчуєш, що заплутався! Вкладені внутрішні класи - 2На сьогоднішній лекції ми поговоримо про Inner classes — внутрішні класи (вони ж — non static nested classes, нестатичні вкладені класи). Вони спеціально виділені на загальній схемі, щоб ти не загубився :) Почнемо з очевидного питання: чому ці класи називаються «внутрішніми»? Відповідь досить проста: тому що вони створюються всередині інших класів. Ось приклад:

public class Bicycle {

   private String model;
   private int weight;

   public Bicycle(String model, int weight) {
       this.model = model;
       this.weight = weight;
   }
  
   public void start() {
       System.out.println("Поїхали!");
   }

   public class HandleBar {

       public void right() {
           System.out.println("Кермо вправо!");
       }

       public void left() {

           System.out.println("Кермо вліво!");
       }
   }

   public class Seat {
      
       public void up() {

           System.out.println("Сидіння піднято вище!");
       }
      
       public void down() {

           System.out.println("Сидіння опущено нижче!");
       }
   }
}
Тут у нас є клас Bicycle — велосипед. У нього є 2 поля та 1 метод — start(). Вкладені внутрішні класи - 3Його відмінність від звичайного класу в тому, що у нього є два класи, код яких написаний всередині Bicycle — це класи HandleBar (кермо) і Seat (сидіння). Це повноцінні класи: як бачиш, у кожного з них є власні методи. На цьому моменті у тебе міг виникнути питання: а навіщо ми взагалі засунули одні класи всередину іншого? Навіщо робити їх внутрішніми? Ну гаразд, допустимо, нам потрібні в програмі окремі класи для керма і сидіння. Але ж не обов'язково робити їх вкладеними! Можна ж зробити звичайні класи. Наприклад, ось так:

public class HandleBar {
   public void right() {
       System.out.println("Кермо вправо!");
   }

   public void left() {

       System.out.println("Кермо вліво");
   }
}

public class Seat {

   public void up() {

       System.out.println("Сидіння піднято вище!");
   }

   public void down() {

       System.out.println("Сидіння опущено нижче!");
   }
}
Дуже хороше питання! Звісно, технічних обмежень у нас немає — можна зробити і так. Тут справа швидше у правильному проектуванні класів з точки зору конкретної програми і в сенсі цієї програми. Внутрішні класи — це класи для виділення у програмі якоїсь сутності, яка нерозривно пов'язана з іншою сутністю. Кермо, сидіння, педалі — це складові частини велосипеда. Окремо від велосипеда вони не мають сенсу. Якби ми зробили всі ці класи окремими публічними класами, у нашій програмі міг би з'явитися, наприклад, такий код:

public class Main {

   public static void main(String[] args) {
       HandleBar handleBar = new HandleBar();
       handleBar.right();
   }
}
Еммм… Сенс цього коду навіть пояснити складно. У нас є якесь незрозуміле велосипедне кермо (навіщо воно потрібне? Без поняття, якщо чесно). І це кермо повертає вправо...саме по собі, без велосипеда...навіщо-то. Відокремивши сутність керма від сутності велосипеда, ми втратили логіку нашої програми. З використанням внутрішнього класу код виглядає зовсім інакше:

public class Main {

   public static void main(String[] args) {

       Bicycle peugeot = new Bicycle("Peugeot", 120);
       Bicycle.HandleBar handleBar = peugeot.new HandleBar();
       Bicycle.Seat seat = peugeot.new Seat();

       seat.up();
       peugeot.start();
       handleBar.left();
       handleBar.right();
   }
}
Вивід у консоль:

Сидіння піднято вище!
Поїхали!
Кермо вліво!
Кермо вправо!
Відбувається несподівано обрело сенс! :) Ми створили об'єкт велосипеда. Створили два його «підоб'єкти» — кермо і сидіння. Підняли сидіння вище для зручності — і поїхали: катимося і керуємо, куди треба! :) Потрібні нам методи викликаються у потрібних об'єктів. Все просто і зручно. У цьому прикладі виділення керма і сидіння посилює інкапсуляцію (ми ховаємо дані про частини велосипеда всередині відповідного класу) і дозволяє створити більш детальну абстракцію. Тепер давай розглянемо іншу ситуацію. Припустимо, ми хочемо створити програму, що моделює магазин велосипедів і їх запчастин. Вкладені внутрішні класи - 4У цій ситуації наше попереднє рішення буде невдалим. У межах магазину запчастин кожна окрема частина велосипеда має сенс навіть окремо від сутності велосипеда. Наприклад, нам знадобляться методи типу «продати покупцеві педалі», «купити нове сидіння» тощо. Тут використовувати внутрішні класи було б помилкою — кожна окрема частина велосипеда в рамках нашої нової програми має власний сенс: вона відокремлена від сутності велосипеда, жодним чином не прив'язана до нього. Саме на це тобі слід звертати увагу, якщо ти задумався, чи потрібно тобі використовувати внутрішні класи, чи рознести всі сутності по окремим класам. Об'єктно-орієнтоване програмування добре тим, що дозволяє легко моделювати сутності реального світу. Саме цим ти можеш керуватися, вирішуючи, чи потрібно використовувати внутрішні класи. У реальному магазині запчастини окремо від велосипедів — це нормально. Значить, і при проектуванні програми це буде правильно. Гаразд, з «філософією» розібралися :) Тепер давай познайомимося з важливими «технічними» особливостями внутрішніх класів. Ось що тобі обов'язково потрібно пам'ятати і розуміти:
  1. Об'єкт внутрішнього класу не може існувати без об'єкта «зовнішнього» класу.

    Це логічно: для того ми й зробили Seat і HandleBar внутрішніми класами, щоб у нашій програмі не з'являлися то тут, то там безгосподарні рули і сідла.

    Цей код не скомпілюється:

    
    public static void main(String[] args) {
    
       HandleBar handleBar = new HandleBar();
    }
    

    З цього випливає наступна важлива особливість:

  2. Об'єкт внутрішнього класу має доступ до змінних «зовнішнього» класу.

    Для прикладу давай додамо у наш клас Bicycle змінну int seatPostDiameter — діаметр підсідельного штиря.

    Тоді у внутрішньому класі Seat ми можемо створити метод getSeatParam(), який повідомить нам параметр сідла:

    
    public class Bicycle {
    
       private String model;
       private int weight;
    
       private int seatPostDiameter;
    
       public Bicycle(String model, int weight, int seatPostDiameter) {
           this.model = model;
           this.weight = weight;
           this.seatPostDiameter = seatPostDiameter;
    
       }
    
       public void start() {
           System.out.println("Поїхали!");
       }
    
       public class Seat {
    
           public void up() {
    
               System.out.println("Сідло піднято вище!");
           }
    
           public void down() {
    
               System.out.println("Сідло опущено нижче!");
           }
    
           public void getSeatParam() {
    
               System.out.println("Параметр сідла: діаметр підсідельного штиря = " + Bicycle.this.seatPostDiameter);
           }
       }
    }
    

    І тепер ми можемо отримати цю інформацію у нашій програмі:

    
    public class Main {
    
       public static void main(String[] args) {
    
           Bicycle bicycle = new Bicycle("Peugeot", 120, 40);
           Bicycle.Seat seat = bicycle.new Seat();
    
           seat.getSeatParam();
       }
    }
    

    Вивід у консоль:

    
    Параметр сідла: діаметр підсідельного штиря = 40
    

    Зверни увагу: нова змінна оголошена з найсуворішим модифікатором — private. І все одно у внутрішнього класу є доступ!

  3. Об'єкт внутрішнього класу не можна створити у статичному методі «зовнішнього» класу.

    Це пояснюється особливостями пристрою внутрішніх класів. У внутрішнього класу можуть бути конструктори з параметрами або тільки конструктор за замовчуванням. Але незалежно від цього, коли ми створюємо об'єкт внутрішнього класу, у нього непомітно передається посилання на об'єкт «зовнішнього» класу. Адже наявність такого об'єкта — обов'язкова умова. Інакше ми не зможемо створювати об'єкти внутрішнього класу.

    Але якщо метод зовнішнього класу статичний, значить, об'єкт зовнішнього класу може взагалі не існувати! А значить, логіка роботи внутрішнього класу буде порушена. У такій ситуації компілятор видасть помилку:

    
    public static Seat createSeat() {
      
       //Bicycle.this cannot be referenced from a static context
       return new Seat();
    }
    
  4. Внутрішній клас не може містити статичні змінні та методи.

    Логіка тут та ж: статичні методи і змінні можуть існувати та викликатися навіть за відсутності об'єкта.

    Але без об'єкта «зовнішнього» класу доступу до внутрішнього класу у нас не буде.

    Очевидна суперечність! Тому наявність статичних змінних і методів у внутрішніх класах заборонено.

    Компілятор видасть помилку при спробі їх створити:

    
    public class Bicycle {
    
       private int weight;
    
    
       public class Seat {
          
           //inner class cannot have static declarations
           public static void getSeatParam() {
    
               System.out.println("Параметр сідла: діаметр підсідельного штиря = " + Bicycle.this.seatPostDiameter);
           }
       }
    }
    
  5. При створенні об'єкта внутрішнього класу важливу роль відіграє його модифікатор доступу.

    Внутрішній клас можна позначити стандартними модифікаторами доступу — public, private, protected і package private.

    Чому це важливо?

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

    Якщо наш клас Seat оголошено як public, ми можемо створювати його об'єкти у будь-якому іншому класі. Єдина вимога — об'єкт «зовнішнього» класу теж обов'язково має існувати.

    До речі, ми вже це робили ось тут:

    
    public class Main {
    
       public static void main(String[] args) {
    
           Bicycle peugeot = new Bicycle("Peugeot", 120);
           Bicycle.HandleBar handleBar = peugeot.new HandleBar();
           Bicycle.Seat seat = peugeot.new Seat();
    
           seat.up();
           peugeot.start();
           handleBar.left();
           handleBar.right();
       }
    }
    

    Ми легко отримали доступ до внутрішнього класу HandleBar з класу Main.

    Якщо ж ми оголосимо внутрішній клас як private, доступ до створення об'єктів у нас буде тільки всередині «зовнішнього» класу.

    Створити об'єкт Seat ззовні ми вже не зможемо:

    
    private class Seat {
    
       //методи
    }
    
    public class Main {
    
       public static void main(String[] args) {
    
           Bicycle bicycle = new Bicycle("Peugeot", 120, 40);
    
           //Bicycle.Seat has a private access in 'Bicycle'
           Bicycle.Seat seat = bicycle.new Seat();
       }
    }
    

    Мабуть, ти вже зрозумів логіку :)

  6. Модифікатори доступу для внутрішніх класів працюють так само, як і для звичайних змінних.

    Модифікатор protected надає доступ до змінної класу у його класах-нащадках і у класах, які знаходяться у тому ж пакеті.

    Так само protected працює і для внутрішніх класів. Об'єкти protected внутрішнього класу можна створювати:

    • всередині «зовнішнього» класу;
    • у його класах-нащадках;
    • у тих класах, які знаходяться у тому ж пакеті.

    Якщо у внутрішнього класу немає модифікатора доступу (package private), об'єкти внутрішнього класу можна створювати

    • всередині «зовнішнього» класу;
    • у класах, які знаходяться у тому ж пакеті.

    З модифікаторами ти вже давно знайомий, так що тут проблем не буде.

На цьому поки все :) Але не розслабляйся! Внутрішні вкладені класи — досить обширна тема, з якою ми продовжимо знайомитися на наступних заняттях. Зараз ти можеш освіжити у пам'яті лекцію про внутрішні класи з нашого курсу. А наступного разу поговоримо про статичні вкладені класи.