JavaRush/Java блог/Java Developer/Вложенные внутренние классы или Inner Class в Java
Автор
John Selawsky
Senior Java-разработчик и преподаватель в LearningTree

Вложенные внутренние классы или Inner Class в Java

Статья из группы Java Developer
участников
Привет! Сегодня мы начнем рассматривать важную тему — работу вложенных классов в 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), объекты внутреннего класса можно создавать

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

    С модификаторами ты уже давно знаком, так что тут проблем не будет.

На этом пока все :) Но не расслабляйся! Внутренние вложенные классы — довольно обширная тема, с которой мы продолжим знакомиться на следующих занятиях. Сейчас ты можешь освежить в памяти лекцию о внутренних классах из нашего курса. А в следующий раз поговорим о статических вложенных классах.
Комментарии (114)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Anonymous #3380648
Уровень 26
6 января, 10:36
Серия статей (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
SobolenkoE Python Developer
16 января, 17:26
Если сделаете ссылки кликабельными будет отлично.
25 декабря 2023, 13:46
Блин, сейчас jdk 21 использую, 4 запрета вообще нет. Столько сейчас читал понять пытался) А попробовал это проверить, оказывается всё можно
Jotun
Уровень 20
11 сентября 2023, 12:44
Возможно, я что-то не так понял из статьи, но код ниже полностью рабочий. Можете, пожалуйста, пояснить или актуализировать пункт 4 - Внутренний класс не может содержать статические переменные и методы. Что я делаю не так?
public class Bicycle {

    private static int d = 36;

    public class Seat {
        private static int r = 18;
        //inner class cannot have static declarations
        public static void getSeatParam() {

            System.out.println("Параметр сиденья: диаметр подседельного штыря = " + d);
            System.out.println("Параметр сиденья: Радиус подседельного штыря = " + r);
        }
    }

    public static void main(String[] args) {
        Bicycle b = new Bicycle();
        Seat s = b.new Seat();
        s.getSeatParam();
        Bicycle.Seat.getSeatParam();
        Seat.getSeatParam();
    }
}
Lexoid
Уровень 40
13 сентября 2023, 20:51
Всё верно делаешь. Дело в том, что начиная с Java SE 16 во внутренних классах разрешено использовать статические члены, как и в случае со статическими вложенными классами. Эти изменения, как уже писал чуть ниже, были сделаны в рамках JEP 395, который добавил записи (Records) в язык Java. До Java SE 16 во внутренних классах можно было объявлять исключительно константы (поля помеченные модификаторами static и final).
Jotun
Уровень 20
14 сентября 2023, 11:25
Спасибо за разъяснения!
Lexoid
Уровень 40
15 сентября 2023, 20:15
Всегда пожалуйста! Если будут ещё какие-то вопросы, то обязательно спрашивай, не стесняйся.
Anatoly Enterprise Java Developer
8 сентября 2023, 11:56
ok
Lexoid
Уровень 40
5 сентября 2023, 17:13
Нашёл одну грубейшую ошибку и скорее недочёт. Автор чётко заявляет, что объект внутреннего класса нельзя создать в статическом методе «внешнего» класса. Очевидно, что это не соответствует действительности. Идею, которую здесь хотели донести я прекрасно понимаю, но в любом статическом методе мы можем без проблем создать экземпляр внутреннего класса при условии существования экземпляра внешнего класса, который, впоследствии, неразрывно будет связан с внутренним. Главное, что необходимо запомнить, так это то, что экземпляры внутренних классов всегда создаются в контексте внешних классов. Точка. Ну и второй момент. Автор утверждает, что внутри внутренних классов нельзя объявлять статические члены класса. Да, это так, но лишь отчасти. С методами соглашусь, а вот static final поля можно объявлять. Если поле static, но не final, то оно действительно запрещено. Вывод: статические поля объявлять можно, но исключительно в виде констант. Так что аккуратнее с этими нюансами! P.S. Что касается ограничений на статические члены, то они существовали вплоть до Java SE 16. Начиная с этой версии и выше никаких ограничений вообще нет. Эти изменения были сделаны в рамках JEP 395, который добавил записи (Records) в язык Java. Соответственно, следующим логическим шагом стало ослабление ограничений на вложенность, что позволяет объявлять статические классы, методы, поля и т. д. внутри внутренних классов.
Anonymous #3336441
Уровень 43
9 сентября 2023, 19:55
ты пытаешься вызвать static метод у внешнего класса, внутри которого создается объект внутреннего класса который не может быть создан без объекта внешнего класса, получается для snanic метода экземпляр класса не нужен, но нужен для создания экземпляра внутреннего класса, ахахаах. -> автор ПРАВ!!!
Anonymous #3336441
Уровень 43
9 сентября 2023, 20:03
и да,) static поля и методы можно использовать во внутренних классах, тут ты прав)
Lexoid
Уровень 40
13 сентября 2023, 09:24
Да, разумеется, что можно использовать. Так было далеко не всегда. Нужно ориентироваться на версию JDK, которую мы используем в процессе написания компьютерных программ. До Java SE 16 во внутренних классах можно было объявлять исключительно константы (то бишь, поля, которые помечены модификаторами static и final), а все остальные статические члены были запрещены. Начиная с версии Java SE 16 и в более поздних выпусках данное ограничение было снято и во внутренних классах можно объявлять любые статические члены (в этом плане внутренние классы теперь соответствуют вложенным статическим классам). Что же касается формулировки относительно того, что объект внутреннего класса нельзя создать в статическом методе «внешнего» класса, как это указывает автор данной публикации, то советую ещё раз вчитаться в каждое слово и задаться вопросом, действительно ли это так? Я прекрасно понимаю задумку, но мне кажется, что данная формулировка может сбить с толку некоторых новичков. Очевидно, что в контексте статического метода мы не можем использовать конструкцию типа:
return new InnerClass();
Но это ни в коем случае не означает, что мы не можем создать внутри статического метода объект внешнего класса и уже в контексте этого экземпляра создать объект внутреннего класса, который будет связан с экземпляром созданного нами внешнего класса. Итак, подытожим для удобства: Нельзя напрямую создать экземпляр внутреннего класса в статическом методе без экземпляра внешнего класса, потому что внутренний класс всегда связан с экземпляром внешнего класса. Однако, если у тебя есть экземпляр внешнего класса, ты без проблем можешь создать экземпляр внутреннего класса, даже внутри статического метода. Таким образом, утверждение "нельзя создать экземпляр внутреннего класса в статическом методе" не совсем корректно без уточнения контекста.
Lexoid
Уровень 40
13 сентября 2023, 09:48
Так что, меня задело само утверждение автора, оно звучит уж очень категорично. Да, в дальнейшем раскрывается суть вопроса, но это всё равно может значительным образом запутать. Мне кажется, что можно было бы заменить формулировку на что-то типа: «Для создания объекта внутреннего класса в статическом методе „внешнего“ класса необходим экземпляр внешнего класса».
Andrey
Уровень 41
3 марта, 19:29
У меня может вопрос будет глупым, разъясните пожалуйста, почему до java 16 было ограничение на статические поля, а в 16 сняли, ведь все классы хранятся в metaspace как и статические поля, поэтому они никак не зависят от экземпляра внешнего класса, который хранится в heap
Lexoid
Уровень 40
8 марта, 13:05
Хороший вопрос, не считаю его каким-то глупым или особенным. Действительно, вся информация о классах, в том числе и статические члены, хранится в области Metaspace, но это не имеет отношения к данному вопросу. Мне видится, что на то были немного другие причины, призванные подчеркнуть семантику внутренних классов, что заставляло рассматривать их в исключительно нестатическом контексте. Тем не менее наличие статических полей не нарушало бы никаких правил языка, так как для любого класса, включая внутренние, генерируется свой отдельный файл с байт-кодом, что делает все классы в какой-то степени равноценными. Да, классы могут иметь разную область действия (область видимости) и налагать свои правила относительно инстанцирования, но это не имеет прямого отношения к статическим членам. Некоторые ограничения, в том числе и то, которое мы сейчас рассматриваем, налагалось с точки зрения согласованности и поддержания общей логики, которую диктовала идея внутренних классов, как таковых, которые зависят от объемлющего (с точки зрения создания экземпляра или даже являются логической частью класса, если говорить о локальных классах). Что же такого произошло начиная с Java 16? Судя по всему, эту идею решили полностью пересмотреть в сторону послабления, чтобы не возникало таких насущных вопросов, которыми мы задаемся и по причине (мне даже кажется, что в большей степени именно за этого) введения новой структуры данных, такой как Records. Записи предназначены для хранения данных и представляют собой подобие некоторого структурного контейнера. Они могут существовать в нестатическом контексте и хранить фактически любые данные, в том числе и статические поля. Я так понимаю, что было бы глупо оставлять старые правила, так как на уровне байт-кода мы бы столкнулись с интересной коллизией, когда для одних классов, которые вроде бы и вложены, вроде бы и не являются статическими, но разрешено объявление статических членов, а для других — нет. Вот так я это себе вижу.
Lexoid
Уровень 40
8 марта, 13:06
Так что хотелось бы ещё раз подытожить и прояснить несколько моментов. Статические поля в нестатических вложенных классах: До Java 16 нестатические вложенные классы (inner classes) не могли иметь статические поля, потому что они логически связаны с экземпляром внешнего класса. Статические поля же, по своей природе, не привязаны к экземпляру и должны быть доступны без создания объекта класса. Это ограничение было снято в Java 16 для улучшения совместимости с записями (records), которые могут быть вложенными и иметь статические поля. Metaspace и хранение классов: Действительно, классы хранятся в области памяти под названием Metaspace, а статические поля — в памяти, выделенной для класса. Однако ограничение на статические поля в нестатических вложенных классах было не из-за особенностей хранения, а из-за семантической связи с экземпляром внешнего класса. Records: Записи (records) в Java были введены как способ создания простых классов данных с автоматически сгенерированными методами (например, equals, hashCode, toString). Они могут быть как статическими, так и нестатическими вложенными классами. Введение записей стало одной из причин для снятия ограничений на статические поля в нестатических вложенных классах.
Vitaly Demchenko
Уровень 44
28 августа 2023, 21:11
"\uD83D\uDC4D"
Alexander Rozenberg
Уровень 32
26 июля 2023, 19:30
fine
Anonymous #???
Уровень 6
2 июля 2023, 01:28
Объясните кто знает что за модификатора доступа (package private)? Чем он отличается от обычного private? Могу предположить что автор имел ввиду default access modifier, поправьте если не прав.
Anonymka
Уровень 17
5 июля 2023, 13:22
да, все так: package private это и есть default access modifier с областью видимости внутри своего пакета
No Name
Уровень 32
22 июня 2023, 14:56
+ статья в копилке
19 июня 2023, 19:40
Продолжение про нестатические вложенные (анонимные) классы (2/3 часть): https://javarush.com/groups/posts/2193-anonimnihe-klassih Продолжение про статические вложенные классы (3/3 часть): https://javarush.com/groups/posts/2183-staticheskie-vlozhennihe-klassih