JavaRush /Блог /Java Developer /Внутренние классы в локальном методе
Автор
Александр Мяделец
Руководитель команды разработчиков в CodeGym

Внутренние классы в локальном методе

Статья из группы Java Developer
Привет! Давай поговорим о еще одной разновидности вложенных классов. А именно — о локальных классах (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). Вот такие они, локальные классы! Как видишь, у них немало отличий от внутренних классов. Нам даже пришлось погрузиться в особенности версии языка, чтобы разобраться в их работе :) На следующей лекции поговорим об анонимных внутренних классах — последней группе вложенных классов. Удачи в обучении! :)
Комментарии (48)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Light_Day :) Уровень 43
22 июля 2024
ок!
Private Joker Уровень 36
4 июля 2024
/* Комментарий удален */
Anonymous #3380648 Уровень 30
6 января 2024
Серия статей (4) о вложенных классах: 1. Нестатические. Вложенные внутренние классы или Inner Class в Java: https://javarush.com/groups/posts/2181-vlozhennihe-vnutrennie-klassih 2. Нестатические. Внутренние классы в локальном методе (Method local inner classes): https://javarush.com/groups/posts/2190-vnutrennie-klassih-v-lokaljhnom-metode 3. Нестатические. Анонимные классы в Java (Anonymous Inner Class): https://javarush.com/groups/posts/2193-anonimnihe-klassih 4. Статические. Статические вложенные классы (Static Nested Classes): https://javarush.com/groups/posts/2183-staticheskie-vlozhennihe-klassih
Suzuya Jūzō Уровень 46
5 апреля 2023
Локальные классы: 1. Объект локального класса не может создаваться за пределами метода или блока, в котором его объявили. 2. Возможность доступа к локальным переменным и параметрам метода: - в Java 7 локальный класс может получить доступ к локальной переменной или параметру метода, только если они объявлены в методе как final. - начиная с Java 8 локальный класс имеет доступ Effective-final т.е к переменной, значение которой не менялось после инициализации. 3. У локального класса есть доступ ко всем (даже приватным) полям и методам внешнего класса: и к статическим, и к нестатическим. 4. Локальные классы не могут определять или объявлять какие-либо статические члены. 5. Локальные классы созданные в статических методах внешнего класса могут ссылаться только на статические члены этого внешнего класса, так нельзя: а вот так можно: 6. Нельзя объявить интерфейс внутри блока - интерфейсы по своей природе статичны, но если интерфейс объявлен внутри внешнего класса, то локальный класс может его реализовать.
Anonymous #2502407 Уровень 2
3 апреля 2023
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); } } //...код валидации номера }
Vlad Уровень 31
17 марта 2023
Главное что я понял, это то что у локального класса есть доступ к полям и методам внешнего класса - ценное наследие от внутреннего класса.
Андрей Уровень 42
16 марта 2023
трудная тема для понимания, но не для реализации. с материалом было бы много проще тезисно основное, но это роскошь.
Aleksey Уровень 41
16 января 2023
Чуть не уснул пока читал это спагетти... А зачем тут что как почему вот это взялось можно сделать ? Спасибо, отличная лекция. Кстати, эта лекция вот прям похожа на лекцию 🙃 Все понятно, очень интересно, но ничего не понятно)
YahveSmerciful Уровень 43 Expert
3 января 2023
ААаа
FutureDev Уровень 42
12 ноября 2022
Эту статью точно профессор писал? Обычно его статьи круто дополняют (а иногда даже весьма удачно заменяют) лекции, но... не в этот раз