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

   }
}
Фух, лекция получилась немаленькой :) Но зато ты узнал много нового! А теперь — самое время решить несколько задач! :)