Java

Вложенный (иначе  —  внутренний) класс в объектно-ориентированных языках программирования  —  это такой класс,объявленный внутри другого класса.

Это позволяет объединять логически связанные между собой классы и увеличивать таким образом инкапсуляцию, а заодно делать код более поддерживаемым и лаконичным.

Далее рассмотрим четыре разновидности вложенных классов.

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

Вложенный класс определяется так же, как и любой другой класс:

package com.mypackage;

public class Outer {

    public static class Nested {
        // ...
    }
}

Как и всякий статический элемент, статический класс привязан к самому классу, а не к его экземпляру. Это означает, что мы можем создать экземпляр вложенного класса, не создавая промежуточный экземпляр внешнего класса Outer.

Outer.Nested instance = new Outer.Nested();

Такой класс ничем не отличается от любого другого и работает по тем же принципам. В нем:

  • поддерживаются все модификаторы доступа;
  • можно определить как статические, так и нестатические члены;
  • недоступны нестатические члены заключающего класса.

Для простоты можно даже импортировать вложенный класс, чтобы избавиться от префикса внешнего класса:

import  com.mypackage.Outer.Nested; 
Nested instance = new Nested();

Применение

Статические вложенные классы ведут себя так же, как и прочие классы верхнего уровня, и должны рассматриваться в качестве таковых. Главное преимущество здесь  —  удобство упаковки.

Нестатические вложенные классы / внутренние классы

Нестатические вложенные классы или внутренние классы:

public class Outer {

     public class Nested {
         // ...
     }
}

Внутренний вложенный класс (Nested) привязан не к внешнему (Outer) классу, а к экземплярам своего заключающего класса.

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

Для создания экземпляра внутреннего класса требуется экземпляр его заключающего класса:

Outer outer = new Outer();
Outer.Nested nested = outer.new Nested();

// ТАК ДЕЛАТЬ НЕЛЬЗЯ!
Outer.Nested nested = new Outer.Nested();

Больше нет какого-то одного типа, который “просто” вложен в другой класс. Внутренний класс тесно связан с реальным экземпляром своего заключающего класса и больше не может существовать сам по себе.

Сериализация

Вложенный класс не становится сериализуемым автоматически только потому, что заключающий класс является сериализуемым.

Как и в случае с любыми другими членами, нам следует убедиться, что в нем также реализован java.io.Serializable. Или в конечном счете мы получим исключение java.io.NotSerializableException.

Применение 

Преимущество внутренних классов  —  более глубокая связь с их заключающим классом, в том числе полный доступ ко всем его членам. Но эта связь может привести к неочевидному удержанию памяти. Заключающий класс не может быть обработан сборщиком мусора раньше, чем экземпляр вложенного класса. 

Ресурсы

Внутренние классы и заключающие экземпляры (JLS).

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

Локальные классы  —  особая разновидность внутренних классов.

Локальный класс можно определить внутри любого блока кода (например, метода):

public class MyClass {

    public void run() {
        class MyLocalClass {
            // ...
        }
    }
}

По аналогии с внутренним классом, локальный класс получает доступ ко всем членам заключающего класса, но при этом мы не можем предоставить локальному классу модификатор, потому что он используется исключительно “локально”.

Применение

Результат здесь будет таким же, как с внутренними классами, но внутренний класс не способен привязать логику к определенному блоку настолько сильно, как локальный класс.

С помощью локальных классов можно легко группировать логически связанные элементы, и при их применении футпринт будет наименьшим из возможных.

Ресурсы

Объявление локальных классов (JLS).

Анонимные классы

Анонимные классы создаются не при объявлении вложенного класса, а при создании экземпляра уже существующего типа:

Runnable runnable = new Runnable() {

    @Override
    public void run() {
        // ...
    }
};

Мы только что создали новый класс на основе интерфейса Runnable, и этот класс анонимный  —  у него нет имени.

Для создания анонимных классов используются не только интерфейсы. Таким же образом можно расширить другие классы, не имеющие свойства final:

List<String> customStringBuilder = new ArrayList<>(10) {

    public boolean add(String value) {
        System.out.println("Adding value: " + value);
        return super.add(value);
    }

    // ...
};

Специализированная реализация List<String>  —  для которой вообще не нужно создавать отдельный класс. Красота!

Синтаксис создания всегда следует одной и той же структуре:

new <<Тип>>(<<аргументы конструктора>>) {
    // объявления / переопределения
};

Анонимные классы объявляются через выражения, и выражения обязательно должны быть частью оператора  —  либо в блоке, либо в самом объявлении члена.

Анонимные классы против лямбд

С появлением лямбда-выражений у нас, наконец, появился способ более простой реализации типов “на месте»:

// АНОНИМНЫЙ КЛАСС
Predicate<String> anonymous = new Predicate<String>() {

    @Override
    public boolean test(String t) {
        return t != null;
    }
};

// ЛЯМБДА-ВЫРАЖЕНИЕ
Predicate<String> lambda = (input) -> input != null;

Функционально оба предиката идентичны, поэтому легко подумать, что лямбды  —  просто синтаксический сахар для анонимных классов. Однако отличия в сгенерированном байт-коде показывают, что этот код может вести себя так же, но при этом устроен иначе:

// АНОНИМНЫЙ КЛАСС
0: new           #2 // class Anonymous$1
3: dup
4: invokespecial #3 // Method Anonymous$1."<init>":()V
7: astore_1
8: return

// ЛЯМБДА
0: invokedynamic #2, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
5: astore_1
6: return

Лямбды используют операционный код invokedynamic, который позволяет JVM вызывать методы более динамическим образом.

Применение

Анонимные классы отлично подходят для небольших, конкретных реализаций “на месте”, даже если лямбда-выражения тоже с этим бы справились.

Также из-за своей простоты у анонимных классов много недостатков по сравнению с локальными или внутренними:

  • отсутствие названий осложняет отслеживание стек-трейсов;
  • можно ввести только один тип  —  никаких дополнительных интерфейсов и тому подобного;
  • более сложный синтаксис.

Ресурсы

Затенение

Затенение (shadowing) в разработке программного обеспечения означает повторное объявление члена в более глубокой области видимости. Это означает, что во вложенных классах мы можем переиспользовать имена переменных, а также получать доступ к затемненному члену с помощью префикса вызова:

public class Outer {

    String stringVal = "Outer class";

     public class Nested {
         String stringVal = "Nested Class";

         public void run() {
             System.out.println("Nested stringVal = " + this.stringVal);
             System.out.println("Outer stringVal = " + Outer.this.stringVal);
         }
     }
}

Заключение

Логическое группирование классов и создание анонимных экземпляров на месте  —  отличная возможность. Но следует тщательно взвешивать, какой тип вложенного класса необходим в конкретной ситуации. Особенно это актуально для функциональных интерфейсов, где более грамотным решением является применение лямбда-выражений. 

Дополнительные ресурсы

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

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Ben Weidig: “Nested Classes in Java”

Предыдущая статьяВстроенная база данных Python
Следующая статьяБезопасность наглядно: CORS