JavaRush/Java блог/Java Developer/Наследование внутренних классов (nested classes)
Автор
Александр Мяделец
Руководитель команды разработчиков в CodeGym

Наследование внутренних классов (nested classes)

Статья из группы Java Developer
участников
Привет! Сегодня мы рассмотрим работу важного механизма — наследование во вложенных классах. Не знаю, задумывался ли ты раньше о том, что будешь делать, когда понадобится унаследовать вложенный класс от какого-то другого. Если нет, поверь: такая ситуация может сбить с толку, ведь нюансов здесь очень много:
  1. Мы наследуем вложенный класс от какого-то класса или мы наследуем другой класс от вложенного?
  2. Является ли наследник/наследуемый обычным публичным классом, или это тоже вложенный класс?
  3. Наконец, какой именно тип вложенных классов мы используем во всех этих ситуациях?
Если отвечать на все эти вопросы, возможных вариантов ответа будет такое множество, что голова пойдет кругом :) Как известно, чтобы решить сложную задачу, нужно разделить ее на части попроще. Мы так и поступим. Рассмотрим по очереди каждую группу вложенных классов с двух точек зрения: кто может наследоваться от этого типа вложенных классов, и от кого может наследоваться он. Начнем со статических вложенных классов (static nested classes).

Статические вложенные классы

Примеры наследования внутренних классов - 2Их правила наследования — самые простые. Здесь можно делать практически все, что душе угодно. Статический вложенный класс может быть унаследован от:
  • обычного класса
  • статического вложенного класса, который объявлен во внешнем классе или его предках
Вспомним пример из лекции про статические вложенные классы.
public class Boeing737 {

   private int manufactureYear;
   private static int maxPassengersCount = 300;

   public Boeing737(int manufactureYear) {
       this.manufactureYear = manufactureYear;
   }

   public int getManufactureYear() {
       return manufactureYear;
   }

   public static class Drawing {

       public static int getMaxPassengersCount() {

           return maxPassengersCount;
       }
   }
}
Попробуем изменить код и создать статический вложенный класс Drawing и его потомка — Boeing737Drawing.
public class Boeing737 {

   private int manufactureYear;
   private static int maxPassengersCount = 300;

   public Boeing737(int manufactureYear) {
       this.manufactureYear = manufactureYear;
   }

   public int getManufactureYear() {
       return manufactureYear;
   }

   public static class Drawing {

   }

   public static class Boeing737Drawing extends Drawing {

       public static int getMaxPassengersCount() {

           return maxPassengersCount;
       }
   }
}
Как видишь, никаких проблем. Мы можем вообще вынести класс Drawing и сделать его обычным публичным классом вместо статического вложенного — ничего не изменится.
public class Drawing {

}

public class Boeing737 {

   private int manufactureYear;
   private static int maxPassengersCount = 300;

   public Boeing737(int manufactureYear) {
       this.manufactureYear = manufactureYear;
   }

   public int getManufactureYear() {
       return manufactureYear;
   }

   public static class Boeing737Drawing extends Drawing {

       public static int getMaxPassengersCount() {

           return maxPassengersCount;
       }
   }
}
С этим разобрались. А какие же классы могут наследоваться от статического вложенного? Практические любые! Вложенные/обычные, статические/нестатические — неважно. Вот здесь мы наследуем внутренний класс Boeing737Drawing от статического вложенного Drawing:
public class Boeing737 {

   private int manufactureYear;
   private static int maxPassengersCount = 300;

   public Boeing737(int manufactureYear) {
       this.manufactureYear = manufactureYear;
   }

   public int getManufactureYear() {
       return manufactureYear;
   }

   public static class Drawing {

   }

   public class Boeing737Drawing extends Drawing {

       public int getMaxPassengersCount() {

           return maxPassengersCount;
       }
   }
}
Создать экземпляр Boeing737Drawing можно вот так:
public class Main {

   public static void main(String[] args) {

      Boeing737 boeing737 = new Boeing737(1990);
      Boeing737.Boeing737Drawing drawing = boeing737.new Boeing737Drawing();
      System.out.println(drawing.getMaxPassengersCount());

   }

}
Хотя наш класс Boeing737Drawing наследован от статического класса, сам он не статический! Поэтому ему всегда будет нужен экземпляр внешнего класса. Мы можем вынести класс Boeing737Drawing из класса Boeing737 и сделать его просто публичным классом. Ничего не изменится — он так же может наследоваться от статического вложенного Drawing.
public class Boeing737 {

   private int manufactureYear;
   public static int maxPassengersCount = 300;

   public Boeing737(int manufactureYear) {
       this.manufactureYear = manufactureYear;
   }

   public int getManufactureYear() {
       return manufactureYear;
   }

   public static class Drawing {

   }
}

public class Boeing737Drawing extends Boeing737.Drawing {

   public int getMaxPassengersCount() {

       return Boeing737.maxPassengersCount;

}
Единственный важный момент: в этом случае нам нужно сделать статическую переменную maxPassengersCount публичной. Если она останется приватной, доступа у обычного публичного класса к ней не будет. Со статическими классами разобрались! :) Теперь перейдем к внутренним классам. Их, как ты помнишь, 3 вида: просто внутренние классы (inner classes), локальные классы и анонимные внутренние классы. Примеры наследования внутренних классов - 3Опять же, давай двигаться от простого к сложному :)

Анонимные внутренние классы

Анонимный внутренний класс не может наследоваться от другого класса. Никакой другой класс не может наследоваться от анонимного класса. Проще некуда! :)

Локальные классы

Локальные классы (если ты вдруг забыл) объявляются внутри блока кода другого класса. Чаще всего — внутри какого-то метода этого внешнего класса. Логично, что от локального класса могут наследоваться только другие локальные классы внутри того же метода (или блока). Вот пример:
public class PhoneNumberValidator {

   public void validatePhoneNumber(final 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;
           }
       }

       class CellPhoneNumber extends PhoneNumber {

       }

       class LandlinePhoneNumber extends PhoneNumber {


       }

       //...код валидации номера
   }
}
Это код из нашей лекции про локальные классы. Внутри класса-валидатора номеров у нас есть локальный класс PhoneNumber — номер телефона. Если для наших целей понадобится выделить из него две отдельные сущности, например, номер мобильника и номер стационарного телефона, сделать это мы можем только внутри этого же метода. Причина проста: область видимости локального класса — в пределах метода (блока), где он объявлен. Поэтому как-то использовать его снаружи (в том числе и для наследования) мы не сможем. Однако, возможности для наследования у самого локального класса более широкие! Локальный класс может наследоваться от:
  1. Обычного класса.
  2. Внутреннего класса (inner class), который объявлен в том же классе, что и local class, либо в его предках.
  3. От другого локального класса, объявленного в том же методе (блоке).
Первый и третий пункт выглядят очевидно, а вот второй —- немного запутанно :/ Рассмотрим два примера. Пример 1 — «наследование локального класса от Внутреннего класса (inner class), который был объявлен в том же классе, что и local class»:
public class PhoneNumberValidator {

   class PhoneNumber {

       private String phoneNumber;

       public PhoneNumber(String phoneNumber) {
           this.phoneNumber = phoneNumber;
       }

       public String getPhoneNumber() {
           return phoneNumber;
       }

       public void setPhoneNumber(String phoneNumber) {
           this.phoneNumber = phoneNumber;
       }
   }

   public void validatePhoneNumber(final String number) {

       class CellPhoneNumber extends PhoneNumber {

           public CellPhoneNumber(String phoneNumber) {
               super(number);
           }
       }

       class LandlinePhoneNumber extends PhoneNumber {

           public LandlinePhoneNumber(String phoneNumber) {
               super(number);
           }
       }

       //...код валидации номера
   }
}
Здесь мы вынесли класс PhoneNumber из метода validatePhoneNumber() и сделали его внутренним вместо локального. Это не мешает нам наследовать 2 наших локальных класса от него. Пример 2 – «...либо в предках этого класса». Вот тут уже интереснее. Мы можем вынести PhoneNumber еще выше по цепочке наследования. Давай объявим абстрактный класс AbstractPhoneNumberValidator, который станет предком для нашего PhoneNumberValidator:
public abstract class AbstractPhoneNumberValidator {

   class PhoneNumber {

       private String phoneNumber;

       public PhoneNumber(String phoneNumber) {
           this.phoneNumber = phoneNumber;
       }

       public String getPhoneNumber() {
           return phoneNumber;
       }

       public void setPhoneNumber(String phoneNumber) {
           this.phoneNumber = phoneNumber;
       }
   }

}
Как видишь, мы не только объявили его, но и перенесли в него внутренний класс PhoneNumber. Тем не менее, в его классе-потомке — PhoneNumberValidator — локальные классы в методах без проблем могут наследоваться от PhoneNumber!
public class PhoneNumberValidator extends AbstractPhoneNumberValidator {

   public void validatePhoneNumber(final String number) {

       class CellPhoneNumber extends PhoneNumber {

           public CellPhoneNumber(String phoneNumber) {
               super(number);
           }
       }

       class LandlinePhoneNumber extends PhoneNumber {

           public LandlinePhoneNumber(String phoneNumber) {
               super(number);
           }
       }

       //...код валидации номера
   }
}
Благодаря связи через наследование локальные классы внутри класса-потомка «видят» внутренние классы внутри предка. И, наконец, переходим к последней группе :)

Внутренние классы (inner classes)

От внутреннего класса может наследоваться другой внутренний класс, объявленный в том же самом внешнем классе (или в его наследнике). Рассмотрим это на нашем примере с велосипедами из лекции про внутренние классы.
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("Поехали!");
   }

   class Seat {

       public void up() {

           System.out.println("Сидение поднято выше!");
       }

       public void down() {

           System.out.println("Сидение опущено ниже!");
       }
   }

   class SportSeat extends Seat {

       //...методы
   }
}
Здесь мы внутри класса Bicycle объявили внутренний класс Seat — сиденье. От него унаследован специальный подвид гоночных сидений — SportSeat. Однако, мы могли создать отдельный тип «гоночных велосипедов» и вынести его в отдельный класс:
public class SportBicycle extends Bicycle {

   public SportBicycle(String model, int mawWeight) {
       super(model, mawWeight);
   }


   class SportSeat extends Seat {

       public void up() {

           System.out.println("Сидение поднято выше!");
       }

       public void down() {

           System.out.println("Сидение опущено ниже!");
       }
   }
}
Так тоже можно. Внутренний класс потомка (SportBicycle.SportSeat) «видит» внутренние классы предка и может от них наследоваться. У наследования от внутренних классов есть одна очень важная особенность! В предыдущих двух примерах у нас SportSeat был внутренним. Но что, если мы решим сделать SportSeat обычным публичным классом, который при этом наследуется от внутреннего класса Seat?
//ошибка! No inclosing instance of  type 'Bicycle' is in scope
class SportSeat extends Bicycle.Seat {

   public SportSeat() {

   }

   public void up() {

       System.out.println("Сидение поднято выше!");
   }

   public void down() {

       System.out.println("Сидение опущено ниже!");
   }
}
Мы получили ошибку! Сможешь догадаться с чем она связана? :) Все просто. Когда мы говорили о внутреннем классе Bicycle.Seat, упоминали, что в конструктор внутреннего класса неявно передается ссылка на объект внешнего класса. Поэтому без создания объекта Bicycle нельзя создать объект Seat. А что же с созданием SportSeat? У него нет такого встроенного механизма неявной передачи ссылки на объект внешнего класса в конструкторе, как в Seat. Тем не менее, без объекта Bicycle мы, так же как в случае с Seat, не можем создать объект SportSeat. Поэтому нам остается только одно — передать в конструктор SportSeat ссылку на объект Bicycle явно. Вот как это делается:
class SportSeat extends Bicycle.Seat {

   public SportSeat(Bicycle bicycle) {

       bicycle.super();
   }

   public void up() {

       System.out.println("Сидение поднято выше!");
   }

   public void down() {

       System.out.println("Сидение опущено ниже!");
   }
}
Для этого мы используем специальное слово super(); Теперь, если мы захотим создать объект SportSeat, нам ничто не помешает это сделать:
public class Main {

   public static void main(String[] args) {

       Bicycle bicycle = new Bicycle("Peugeot", 120);
       SportSeat peugeotSportSeat = new SportSeat(bicycle);

   }
}
Фух, лекция получилась немаленькой :) Но зато ты узнал много нового! А теперь — самое время решить несколько задач! :)
Комментарии (36)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Максим Li Java Developer
15 апреля, 05:33
Хорошая статья!
Алексей
Уровень 40
Expert
18 января, 16:16
Оч хорошая лекция)
Lexoid
Уровень 40
24 сентября 2023, 11:20
Буквально 2 момента, с которыми можно поспорить. Что касается анонимных классов, то очевидно, что он расширяет тот базовый класс, который мы указываем при создании экземпляра анонимного класса. Экземпляр будет анонимным наследником этого класса. Таким образом, он будет иметь функционал указанного базового класса и всех его предков. Поскольку класс не имеет имени стандартной сигнатуры объявления, то такой класс и вправду невозможно никаким образом унаследовать, с этим полностью согласен. В статье есть указание на то, что статические вложенные классы могут наследовать обычные классы, а также статические вложенные классы, которые объявлены во внешнем классе или его предках. Не совсем понял к какому внешнему классу здесь идёт привязка, но любой статический вложенный класс может спокойно наследовать любые другие статические классы, где бы они не находились. Если нет ограничений с точки зрения модификаторов доступа или возможного циклического наследования, то проблем вообще никаких. Такая же история и с наследованием внутренних классов в контексте статических вложенных классов. Почему бы статическому классу не наследовать внутренний класс, если у него определён соответствующий конструктор? Автор как раз привёл замечательный пример на тему того, как это нужно делать. Так вот, это работает не только с внешними классами, как было показано в статье, но и со статическими вложенными.
Дарья
Уровень 39
28 марта 2023, 12:39
какая разница между super(maxSpeed), bicycle.super(maxSpeed) и bicycle.super()? Это все вызов конструктора суперкласса?
kalkulator¹
Уровень 51
19 января 2023, 06:36
Аміго дуже розумний!
Ra
Уровень 51
Student
15 января 2023, 20:36
Внутренние классы что-то для меня сейчас выглядят как сложная тема, с кучей темных мест
Alex Di
Уровень 31
8 августа 2022, 22:01
А зачем в SportSeat который от Bicycle.Seat еще раз реализовывать методы up() и down() Они же реализованы в Bicycle.Seat? или мы их тут переопределяем?
Kurama
Уровень 50
7 ноября 2022, 18:13
Да, мы их типа переопределяем, просто содержимое тут не имеет значения
antlantis
Уровень 41
14 января 2022, 03:13
уважаемые товарищи! Ребята! заинтересовал один вопрос - "А в чем разница - при создании анонимного класса - будет создаваться анонимный класс как реализатор интерфейса, или как потомок обычного класса или как потомок абстрактного класса ? если вы меня поняли)
Ilyas Dzhalilov
Уровень 50
31 мая 2022, 14:26
Разница как между dynamic proxy и CGLib. Почитайте
Maks Panteleev Java Developer в Bell Integrator
18 мая 2021, 08:51
Анонимный внутренний класс не может наследоваться от другого класса. лол, единственный смысл анонимного класса как раз таки в том, что он неявно наследуется от другого класса. Так что тут было бы правильнее сказать Анонимный внутренний класс не может НЕ наследоваться от другого класса.
5 марта 2021, 19:41
ООП сильно тормозит код . имеет ли смысл заводить два метода up и down, а тем болеее подобных класса, если можно воспользоваться if конструкцией? вопрос риторический :) из этой лекции для себя делаю такой вывод, заобъектить можно все иногда теряя полностью связь с реальностью и происходящим, а глубина зависит только от программиста.