Привет! Сегодня мы начнем рассматривать важную тему — работу вложенных классов в 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)
Сложновато? :) Ничего страшного, вот тебе схема для наглядности. Возвращайся к ней по ходу лекции, если вдруг почувствуешь, что запутался! На сегодняшней лекции мы поговорим об 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 SteeringWheel {

       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 — это классы SteeringWheel (руль) и Seat (сиденье). Это полноценные классы: как видишь, у каждого из них есть собственные методы. На этом моменте у тебя мог возникнуть вопрос: а зачем мы вообще засунули одни классы внутрь другого? Зачем делать их внутренними? Ну ладно, допустим, нам нужны в программе отдельные классы для руля и сидения. Но ведь необязательно делать их вложенными! Можно же сделать обычные классы. Например, вот так:
public class SteeringWheel {
   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) {
       SteeringWheel steeringWheel = new SteeringWheel();
       steeringWheel.right();
   }
}
Эммм… Смысл этого кода даже объяснить сложно. У нас есть какой-то непонятный велосипедный руль (зачем он нужен? Без понятия, если честно). И этот руль поворачивает вправо...сам по себе, без велосипеда...зачем-то. Отделив сущность руля от сущности велосипеда, мы потеряли логику нашей программы. С использованием внутреннего класса код смотрится совсем иначе:
public class Main {

   public static void main(String[] args) {

       Bicycle peugeot = new Bicycle("Peugeot", 120);
       Bicycle.SteeringWheel wheel = peugeot.new SteeringWheel();
       Bicycle.Seat seat = peugeot.new Seat();

       seat.up();
       peugeot.start();
       wheel.left();
       wheel.right();
   }
}
Вывод в консоль: Сиденье поднято выше! Поехали! Руль влево! Руль вправо! Происходящее внезапно обрело смысл! :) Мы создали объект велосипеда. Создали два его «подобъекта» — руль и сиденье. Подняли сиденье повыше для удобства — и поехали: катимся и рулим, куда надо! :) Нужные нам методы вызываются у нужных объектов. Все просто и удобно. В данном примере выделение руля и сидения усиливает инкапсуляцию (мы скрываем данные о частях велосипеда внутри соответствующего класса), и позволяет создать более подробную абстракцию. Теперь давай рассмотрим другую ситуацию. Допустим, мы хотим создать программу, моделирующую магазин велосипедов и их запчастей. В этой ситуации наше предыдущее решение будет неудачным. В рамках магазина запчастей каждая отдельная часть велосипеда имеет смысл даже отдельно от сущности велосипеда. Например, нам понадобятся методы типа «продать покупателю педали», «купить новое сидение» и т.д. Здесь использовать внутренние классы было бы ошибкой — каждая отдельная часть велосипеда в рамках нашей новой программы имеет собственный смысл: она отделима от сущности велосипеда, никак не привязана к нему. Именно на это тебе следует обращать внимание, если ты задумался, нужно ли тебе использовать внутренние классы, или разнести все сущности по отдельным классам. Объектно-ориентированное программирование хорошо тем, что позволяет легко моделировать сущности реального мира. Именно этим ты можешь руководствоваться, решая, нужно ли использовать внутренние классы. В реальном магазине запчасти отдельно от велосипедов — это нормально. Значит, и при проектировании программы это будет правильно. Ладно, с «философией» разобрались :) Теперь давай познакомимся с важными «техническими» особенностями внутренних классов. Вот что тебе обязательно нужно помнить и понимать:
  1. Объект внутреннего класса не может существовать без объекта «внешнего» класса.

    Это логично: для того мы и сделали Seat и SteelingWheel внутренними классами, чтобы в нашей программе не появлялись то тут, то там бесхозные рули и сиденья.

    Этот код не скомпилируется:

    public static void main(String[] args) {
    
       SteeringWheel wheel = SteeringWheel();
    }

    Из этого вытекает следующая важная особенность:

  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.SteeringWheel wheel = peugeot.new SteeringWheel();
           Bicycle.Seat seat = peugeot.new Seat();
    
           seat.up();
           peugeot.start();
           wheel.left();
           wheel.right();
       }
    }

    Мы легко получили доступ к внутреннему классу SteeringWheel из класса Main.

    Если же мы объявим внутренний класс как private, доступ к созданию объектов у нас будет только внутри «внешнего» класса.

    Создать объект Seat снаружи мы уже не сможем:

    private class Seat {
    
       //методы
    }
    
    public class Main {
    
       public static void main(String[] args) {
    
           Bicycle bicycle = new Bicycle("Peugeot", 120, 10, 20);
    
           //Bicycle.Seat has a private access in 'Bicycle'
           Bicycle.Seat seat = bicycle.new Seat();
       }
    }

    Наверное, ты уже понял логику :)

  6. Модификаторы доступа для внутренних классов работают так же, как и для обычных переменных.

    Модификатор protected предоставляет доступ к переменной класса в его классах-наследниках и в классах, которые находятся в том же пакете.

    Так же protected работает и для внутренних классов. Объекты protected внутреннего класса можно создавать:

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

    Если у внутреннего класса нет модификатора доступа (package private), объекты внутреннего класса можно создавать

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

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

На этом пока все :) Но не расслабляйся! Внутренние вложенные классы — довольно обширная тема, с которой мы продолжим знакомиться на следующих занятиях. Сейчас ты можешь освежить в памяти лекцию о внутренних классах из нашего курса. А в следующий раз поговорим о статических вложенных классах.