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 января 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
25 декабря 2023
Блин, сейчас jdk 21 использую, 4 запрета вообще нет. Столько сейчас читал понять пытался) А попробовал это проверить, оказывается всё можно
Jotun Уровень 20
11 сентября 2023
Возможно, я что-то не так понял из статьи, но код ниже полностью рабочий. Можете, пожалуйста, пояснить или актуализировать пункт 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();
    }
}
Anatoly Уровень 30
8 сентября 2023
ok
Lexoid Уровень 40
5 сентября 2023
Нашёл одну грубейшую ошибку и скорее недочёт. Автор чётко заявляет, что объект внутреннего класса нельзя создать в статическом методе «внешнего» класса. Очевидно, что это не соответствует действительности. Идею, которую здесь хотели донести я прекрасно понимаю, но в любом статическом методе мы можем без проблем создать экземпляр внутреннего класса при условии существования экземпляра внешнего класса, который, впоследствии, неразрывно будет связан с внутренним. Главное, что необходимо запомнить, так это то, что экземпляры внутренних классов всегда создаются в контексте внешних классов. Точка. Ну и второй момент. Автор утверждает, что внутри внутренних классов нельзя объявлять статические члены класса. Да, это так, но лишь отчасти. С методами соглашусь, а вот static final поля можно объявлять. Если поле static, но не final, то оно действительно запрещено. Вывод: статические поля объявлять можно, но исключительно в виде констант. Так что аккуратнее с этими нюансами! P.S. Что касается ограничений на статические члены, то они существовали вплоть до Java SE 16. Начиная с этой версии и выше никаких ограничений вообще нет. Эти изменения были сделаны в рамках JEP 395, который добавил записи (Records) в язык Java. Соответственно, следующим логическим шагом стало ослабление ограничений на вложенность, что позволяет объявлять статические классы, методы, поля и т. д. внутри внутренних классов.
Vitaly Demchenko Уровень 44
28 августа 2023

"\uD83D\uDC4D"
Alexander Rozenberg Уровень 32
26 июля 2023
fine
Anonymous #??? Уровень 6
2 июля 2023
Объясните кто знает что за модификатора доступа (package private)? Чем он отличается от обычного private? Могу предположить что автор имел ввиду default access modifier, поправьте если не прав.
No Name Уровень 32
22 июня 2023
+ статья в копилке
19 июня 2023
Продолжение про нестатические вложенные (анонимные) классы (2/3 часть): https://javarush.com/groups/posts/2193-anonimnihe-klassih Продолжение про статические вложенные классы (3/3 часть): https://javarush.com/groups/posts/2183-staticheskie-vlozhennihe-klassih