Внутренний класс (inner class) в Java это нестатический класс, объявленный внутри другого класса. Его объект существует только вместе с объектом внешнего класса и создается через ссылку на него: outer.new Inner(). Взамен внутренний класс получает доступ ко всем полям внешнего, включая private. Внутренние классы выделяют сущности, которые отдельно от внешнего класса не имеют смысла.

Кратко

  • Вложенные классы (nested classes) делятся на два вида: нестатические, они же внутренние (inner), и статические (static nested).
  • У внутренних классов есть два особых подвида: локальный класс и анонимный класс.
  • Объект внутреннего класса не создать без объекта внешнего: Bicycle.Seat seat = peugeot.new Seat();
  • Внутренний класс видит поля внешнего, даже приватные, и обращается к ним через Bicycle.this.поле.
  • Внутренний класс уместен, когда сущность бессмысленна сама по себе (руль без велосипеда). Если часть самоценна (магазин запчастей), нужен обычный класс.
  • Модификатор доступа внутреннего класса определяет, откуда можно создавать его объекты: от public (отовсюду) до private (только внутри внешнего класса).
Привет! Сегодня мы начнем рассматривать важную тему: работу вложенных классов в 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)
Сложновато? :) Ничего страшного, вот тебе схема для наглядности. Возвращайся к ней по ходу лекции, если вдруг почувствуешь, что запутался! Схема видов вложенных классов: Nested classes делятся на Inner class и Static nested classes, а Inner class на обычный внутренний, method-local и anonymousНа сегодняшней лекции мы поговорим об Inner classes, то есть о внутренних классах (они же non static nested classes, нестатические вложенные классы). Они специально выделены на общей схеме, чтобы ты не потерялся :)

Что такое внутренний класс (inner class)

Начнем с очевидного вопроса: почему эти классы называются «внутренними»? Ответ достаточно прост: потому что они создаются внутри других классов. Вот пример:

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(). Силуэт велосипедиста на фоне заката: разбираем пример с классом BicycleЕго отличие от обычного класса в том, что у него есть два класса, код которых написан внутри 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();
   }
}
Вывод в консоль:

Сиденье поднято выше!
Поехали!
Руль влево!
Руль вправо!
Происходящее внезапно обрело смысл! :) Мы создали объект велосипеда. Создали два его «подобъекта»: руль и сиденье. Подняли сиденье повыше для удобства и поехали: катимся и рулим, куда надо! :) Нужные нам методы вызываются у нужных объектов. Все просто и удобно. В данном примере выделение руля и сидения усиливает инкапсуляцию (мы скрываем данные о частях велосипеда внутри соответствующего класса) и позволяет создать более подробную абстракцию.

Когда внутренние классы не нужны

Теперь давай рассмотрим другую ситуацию. Допустим, мы хотим создать программу, моделирующую магазин велосипедов и их запчастей. Магазин велозапчастей: колеса, покрышки и седла на стене, человек с ноутбуком осматривает велосипедВ этой ситуации наше предыдущее решение будет неудачным. В рамках магазина запчастей каждая отдельная часть велосипеда имеет смысл даже отдельно от сущности велосипеда. Например, нам понадобятся методы типа «продать покупателю педали», «купить новое сидение» и т.д. Здесь использовать внутренние классы было бы ошибкой: каждая отдельная часть велосипеда в рамках нашей новой программы имеет собственный смысл: она отделима от сущности велосипеда, никак не привязана к нему. Именно на это тебе следует обращать внимание, если ты задумался, нужно ли тебе использовать внутренние классы, или разнести все сущности по отдельным классам. Объектно-ориентированное программирование хорошо тем, что позволяет легко моделировать сущности реального мира. Именно этим ты можешь руководствоваться, решая, нужно ли использовать внутренние классы. В реальном магазине запчасти отдельно от велосипедов это нормально. Значит, и при проектировании программы это будет правильно. Ладно, с «философией» разобрались :)

Шесть особенностей внутренних классов

Теперь давай познакомимся с важными «техническими» особенностями внутренних классов. Вот что тебе обязательно нужно помнить и понимать:
  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. До Java 16 внутренний класс не мог содержать статические переменные и методы.

    Логика здесь та же: статические методы и переменные могут существовать и вызываться даже при отсутствии объекта.

    Но без объекта «внешнего» класса доступа к внутреннему классу у нас не будет.

    Явное противоречие! Поэтому наличие статических переменных и методов во внутренних классах запрещено.

    Компилятор выбросит ошибку при попытке их создать:

    
    public class Bicycle {
    
       private int weight;
       private int seatPostDiameter;
    
       public class Seat {
          
           //до Java 16: inner class cannot have static declarations
           public static void getSeatParam() {
    
               System.out.println("Параметр сиденья: диаметр подседельного штыря = " + Bicycle.this.seatPostDiameter);
           }
       }
    }
    

    Начиная с Java 16 это ограничение сняли: внутренний класс может объявлять статические поля и методы. Но обращаться к полям внешнего объекта из статического метода все равно нельзя, объекта может и не быть.

  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), объекты внутреннего класса можно создавать:

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

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

Модификаторы доступа внутреннего класса: сводка

Соберем правила из двух последних пунктов в одну таблицу:
Модификатор внутреннего классаГде можно создавать его объекты
publicв любом классе, лишь бы существовал объект «внешнего» класса
protectedвнутри «внешнего» класса, в его классах-наследниках и в классах того же пакета
без модификатора (package private)внутри «внешнего» класса и в классах того же пакета
privateтолько внутри «внешнего» класса
На этом пока все :) Но не расслабляйся! Внутренние вложенные классы это довольно обширная тема, с которой мы продолжим знакомиться на следующих занятиях. Сейчас ты можешь освежить в памяти лекцию о внутренних классах из нашего курса. А в следующий раз поговорим о статических вложенных классах.

Вопросы и ответы

Чем внутренний класс отличается от статического вложенного?

Объект внутреннего (нестатического) класса существует только вместе с объектом внешнего класса и имеет доступ к его полям, включая private. Статический вложенный класс от объекта внешнего класса не зависит и обращаться к его нестатическим полям напрямую не может.

Как создать объект внутреннего класса?

Через объект внешнего класса, конструкцией outer.new Inner(). Например: Bicycle peugeot = new Bicycle("Peugeot", 120); а затем Bicycle.Seat seat = peugeot.new Seat();

Почему new HandleBar() не компилируется?

Потому что при создании объекта внутреннего класса в него неявно передается ссылка на объект внешнего класса. Без нее компилятор не знает, к какому велосипеду относится руль. Писать нужно peugeot.new HandleBar().

Может ли внутренний класс обращаться к private-полям внешнего класса?

Да, и это одна из главных его особенностей. Даже поле с модификатором private доступно изнутри. Обратиться к нему можно как Bicycle.this.seatPostDiameter или просто по имени, если оно не перекрыто своим полем внутреннего класса.

Какие модификаторы доступа можно поставить внутреннему классу?

Все четыре: public, protected, package private (без модификатора) и private. У классов верхнего уровня выбор беднее, там доступны только public и package private. От модификатора зависит, из каких классов получится создавать объекты внутреннего класса.

Когда внутренний класс это плохое решение?

Когда выделяемая сущность имеет смысл сама по себе. В программе магазина велозапчастей педаль или сиденье продают отдельно от велосипеда, поэтому там нужны обычные самостоятельные классы, а не внутренние. Источник: Oracle Java Tutorials: Nested Classes.

Читайте также