Привіт! Давай поговоримо про ще один різновид вкладених класів. А саме — про локальні класи (Method local inner classes). Перше, що треба пригадати перед вивченням — їх місце в структурі вкладених класів. Внутрішні класи в локальному методі - 2Виходячи з нашої схеми ми можемо зрозуміти, що локальні класи — це підвид внутрішніх класів, про які ми говорили детально в одному з попередніх матеріалів. Проте, у локальних класів є ряд важливих особливостей і відмінностей від внутрішніх класів. Головне полягає в їх оголошенні: Локальний клас оголошується тільки у блоці коду. Найчастіше — всередині якогось методу зовнішнього класу. Наприклад, це може виглядати так:

public class PhoneNumberValidator {

   public void validatePhoneNumber(String number) {

        class PhoneNumber {

           private String phoneNumber;

           public PhoneNumber() {
               this.phoneNumber = number;
           }

           public String getPhoneNumber() {
               return phoneNumber;
           }

           public void setPhoneNumber(String phoneNumber) {
               this.phoneNumber = phoneNumber;
           }
       }

       //...код валідації номера
   }
}
ВАЖЛИВО! Цей код не скомпілюється при вставці в IDEA, якщо у тебе встановлена Java 7. Про причини цього ми поговоримо в кінці лекції. Коротко кажучи — робота локальних класів сильно залежить від версії мови. Якщо цей код у тебе не компілюється, ти можеш або перемкнути версію мови в IDEA на Java 8, або додати слово final до параметру методу, щоб вийшло так: validatePhoneNumber(final String number). Після цього все запрацює. Це невелика програма — валідатор телефонних номерів. Її метод validatePhoneNumber() приймає на вхід рядок і визначає, чи є вона номером телефону. І всередині цього методу ми оголосили наш локальний клас PhoneNumber. У тебе міг виникнути логічне питання: навіщо? Навіщо оголошувати клас саме всередині методу? Чому не використати звичайний внутрішній клас? Дійсно, можна було б зробити і так: зробити клас PhoneNumber внутрішнім. Інша справа, що кінцевий вибір залежить від структури і призначення твоєї програми. Давай пригадаємо наш приклад з лекції про внутрішні класи:

public class Bicycle {

   private String model;
   private int mawWeight;

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

   public class HandleBar {

       public void right() {
           System.out.println("Руль вправо!");
       }

       public void left() {

           System.out.println("Руль вліво!");
       }
   }
}
У ньому ми зробили HandleBar (руль) внутрішнім класом велосипеда. У чому ж різниця? Передусім, у використанні класу. Клас HandleBar з другого прикладу — сутність більш складна, ніж PhoneNumber з першого. По-перше, у HandleBar є публічні методи right і left (не є сеттером і геттером). По-друге, неможливо заздалегідь передбачити, де він і його зовнішній клас Bicycle можуть нам знадобитися — це можуть бути десятки різних місць і методів навіть в рамках однієї програми. А ось з класом PhoneNumber все набагато простіше. Програма у нас зовсім проста. У неї всього одна функція — перевірити, чи є число номером телефону. У більшості випадків наш PhoneNumberValidator буде навіть не самостійною програмою, а просто частиною в логіці авторизації для основної програми. Наприклад, на різних сайтах при реєстрації часто просять ввести номер телефону. І якщо надрукувати якусь нісенітницю замість цифр, сайт видасть помилку: «Це не номер телефону!». Для роботи такого сайту (а точніше, механізму авторизації користувача) його розробники можуть включити в код аналог нашого PhoneNumberValidator. Іншими словами, у нас є один зовнішній клас з одним методом, який буде використаний в одному місці програми і більше ніде. А якщо і буде, то в ньому нічого не зміниться: один метод робить свою роботу — і все. У цьому випадку, раз вже вся логіка роботи зібрана в одному методі, буде набагато зручніше і правильніше інкапсулювати там і додатковий клас. Своїх методів, крім геттера і сеттера, у нього немає. Нам, по суті, потрібні тільки дані з нього конструктора. В інших методах він не задіяний. Тому немає причин виносити інформацію про нього за межі єдиного методу, де він використовується. Ми і привели приклад з оголошенням локального класу в методі, але це не єдина можливість. Його можна оголосити просто в блоці коду:

public class PhoneNumberValidator {
  
   {
       class PhoneNumber {

           private String phoneNumber;

           public PhoneNumber(String phoneNumber) {
               this.phoneNumber = phoneNumber;
           }
       }

   }

   public void validatePhoneNumber(String phoneNumber) {

      
       //...код валідації номера
   }
}
Або навіть в циклі for!

public class PhoneNumberValidator {
  

   public void validatePhoneNumber(String phoneNumber) {

       for (int i = 0; i < 10; i++) {

           class PhoneNumber {

               private String phoneNumber;

               public PhoneNumber(String phoneNumber) {
                   this.phoneNumber = phoneNumber;
               }
           }
          
           //...якась логіка
       }

       //...код валідації номера
   }
}
Але такі випадки вкрай рідко зустрічаються. У більшості випадків оголошення буде відбуватися все ж усередині методу. Отже, з оголошенням ми розібралися, про «філософію» теж поговорили :) Які ж у локальних класів є ще особливості і відмінності від внутрішніх класів? Об'єкт локального класу не може створюватися за межами методу або блоку, в якому його оголосили. Уяви, що нам потрібен метод generatePhoneNumber(), який би генерував випадковий номер телефону і повертав об'єкт PhoneNumber. Ми не зможемо створити такий метод у нашому класі-валідаторі в поточній ситуації:

public class PhoneNumberValidator {

   public void validatePhoneNumber(String number) {

        class PhoneNumber {

           private String phoneNumber;

           public PhoneNumber() {
               this.phoneNumber = number;
           }

           public String getPhoneNumber() {
               return phoneNumber;
           }

           public void setPhoneNumber(String phoneNumber) {
               this.phoneNumber = phoneNumber;
           }
       }

       //...код валідації номера
   }

   //помилка! компілятор не розуміє, що це за клас - PhoneNumber
   public PhoneNumber generatePhoneNumber() {

   }

}
Ще одна важлива особливість локальних класів — можливість доступу до локальних змінних і параметрів методу. Якщо ти раптом забув, «локальною» називають змінну, оголошену всередині методу. Тобто, якщо ми створимо для якихось своїх цілей локальну змінну String russianCountryCode всередині методу validatePhoneNumber(), ми можемо отримати до неї доступ із локального класу PhoneNumber. Проте тут є дуже багато нюансів, які залежать від версії мови, що використовується в програмі. На початку лекції ми зробили позначку про те, що код одного з прикладів може не компілюватися в Java 7, пам'ятаєш? Зараз розглянемо причини цього :) У Java 7 локальний клас може отримати доступ до локальної змінної або параметру методу, тільки якщо вони оголошені в методі як final:

public void validatePhoneNumber(String number) {

   String russianCountryCode = "+7";

   class PhoneNumber {

       private String phoneNumber;

       //помилка! параметр методу має бути оголошений як final!
       public PhoneNumber() {
           this.phoneNumber = number;
       }

       public void printRussianCountryCode() {

           //помилка! локальна змінна має бути оголошена як final!
           System.out.println(russianCountryCode);
       }

   }

   //...код валідації номера
}
Тут компілятор викинув дві помилки. А ось тут все гаразд:

public void validatePhoneNumber(final String number) {

   final String russianCountryCode = "+7";

    class PhoneNumber {

       private String phoneNumber;

       
       public PhoneNumber() {
           this.phoneNumber = number;
       }

       public void printRussianCountryCode() {

           System.out.println(russianCountryCode);
       }

    }

   //...код валідації номера
}
Тепер ти знаєш причину, через яку код з початку лекції не компілювався: локальний клас у версії Java 7 має доступ тільки до final-параметрів методу і до final-локальних змінних. У Java 8 поведінка локальних класів була змінена. У цій версії мови локальний клас має доступ не тільки до final-локальних змінних і параметрів, але й до effective-final. Effective-final називають змінну, значення якої не змінювалось після ініціалізації. Наприклад, у Java 8 ми без проблем можемо вивести в консоль змінну russianCountryCode, навіть якщо вона — не final. Головне, щоб вона не змінювала свого значення. Ось у цьому прикладі все працює як треба:

public void validatePhoneNumber(String number) {

  String russianCountryCode = "+7";

    class PhoneNumber {

       public void printRussianCountryCode() {

           //в Java 7 тут була б помилка
           System.out.println(russianCountryCode);
       }

    }

   //...код перевірки номера
}
А ось якщо ми змінимо значення змінної одразу після ініціалізації, код не скомпілюється.

public void validatePhoneNumber(String number) {

  String russianCountryCode = "+7";
  russianCountryCode = "+8";

    class PhoneNumber {

       public void printRussianCountryCode() {

           //помилка!
           System.out.println(russianCountryCode);
       }

    }

   //...код перевірки номера
}
Але недарма локальний клас — підвид внутрішнього класу! У них є й спільні моменти. У локального класу є доступ до всіх (навіть приватних) полів і методів зовнішнього класу: і до статичних, і до нестатичних. Для прикладу, додамо до нашого класу-валідатора статичне поле String phoneNumberRegex:

public class PhoneNumberValidator {

   private static String phoneNumberRegex = "[^0-9]";

   public void validatePhoneNumber(String phoneNumber) {
       class PhoneNumber {
          
           //......
       }
   }
}
За допомогою цієї статичної змінної і буде виконуватись перевірка. Метод перевіряє, чи є в рядку, який йому передали, символи, що не відповідають регулярному виразу "[^0-9]" (тобто, символ не є цифрою від 0 до 9). Ми легко можемо отримати доступ до цієї змінної з локального класу PhoneNumber. Наприклад, написати геттер:

public String getPhoneNumberRegex() {
  
   return phoneNumberRegex;
}
Локальні класи схожі на внутрішні класи, тому що вони не можуть визначати або оголошувати будь-які статичні члени. Локальні класи у статичних методах можуть посилатись тільки на статичні члени включаючого класу. Наприклад, якщо ти не визначив змінну (поле) включаючого класу як статичну, компілятор Java генерує помилку: «Зі статичного контексту не можна посилатись на нестатичну змінну». Локальні класи не статичні, тому що у них є доступ до членів екземпляра вміщуючого блоку. Відповідно, вони не можуть містити більшість видів статичних оголошень. Не можна оголосити інтерфейс всередині блоку; інтерфейси за своєю природою статичні. Цей код не скомпілюється:

public class PhoneNumberValidator {
   public static void validatePhoneNumber(String number) {
       interface I {}
      
       class PhoneNumber implements I{
           private String phoneNumber;

           public PhoneNumber() {
               this.phoneNumber = number;
           }
       }

       //...код перевірки номера
   }
}
Але якщо інтерфейс оголошений всередині зовнішнього класу, клас PhoneNumber може його реалізувати:

public class PhoneNumberValidator {
   interface I {}
  
   public static void validatePhoneNumber(String number) {
      
       class PhoneNumber implements I{
           private String phoneNumber;

           public PhoneNumber() {
               this.phoneNumber = number;
           }
       }

       //...код перевірки номера
   }
}
У локальних класах не можна оголошувати статичні ініціалізатори (блоки ініціалізації) чи інтерфейси. Але у локальних класів можуть бути статичні члени за умови, що вони постійні змінні (static final). Ось такі вони, локальні класи! Як бачиш, у них чимало відмінностей від внутрішніх класів. Нам навіть довелось заглибитись в особливості версії мови, щоб розібратись в їхній роботі :) На наступній лекції поговоримо про анонімні внутрішні класи — останню групу вкладених класів. Удачі в навчанні! :)