Статьи

Топ 5 вариантов использования для вложенных типов

Был интересный разговор о Reddit, на днях статические внутренние классы. Когда это слишком много?

Во-первых, давайте рассмотрим немного базовых исторических знаний Java. Java-язык предлагает четыре уровня классов вложенности , и под «Java-the-language» я подразумеваю, что эти конструкции являются просто «синтаксическим сахаром». Они не существуют в JVM, которая знает только обычные классы.

(Статические) Вложенные классы

1
2
3
4
class Outer {
    static class Inner {
    }
}

В этом случае Inner полностью независим от Outer , за исключением общего общего пространства имен.

Внутренние классы

1
2
3
4
class Outer {
    class Inner {
    }
}

В этом случае Inner экземпляры имеют неявную ссылку на включающий их Outer экземпляр. Другими словами, не может быть Inner экземпляра без связанного Outer экземпляра.

Java-способ создания такого экземпляра таков:

1
Outer.Inner yikes = new Outer().new Inner();

То, что выглядит совершенно неловко, имеет большой смысл. Подумайте о создании Inner экземпляра где-то внутри Outer :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class Outer {
    class Inner {
    }
 
    void somewhereInside() {
        // We're already in the scope of Outer.
        // We don't have to qualify Inner explicitly.
        Inner aaahOK;
 
        // This is what we're used to writing.
        aaahOK = new Inner();
 
        // As all other locally scoped methods, we can
        // access the Inner constructor by
        // dereferencing it from "this". We just
        // hardly ever write "this"
        aaahOK = this.new Inner();
    }
}

Обратите внимание, что как и public и abstract ключевые слова, ключевое слово static неявно используется для вложенных интерфейсов. Хотя следующий гипотетический синтаксис может показаться знакомым на первый взгляд …:

1
2
3
4
5
6
7
8
9
class Outer {
    <non-static> interface Inner {
        default void doSomething() {
            Outer.this.doSomething();
        }
    }
 
    void doSomething() {}
}

… невозможно написать выше. Помимо отсутствия ключевого слова <non-static> , кажется, нет никаких очевидных причин, по которым «внутренние интерфейсы» не должны быть возможными. Я подозреваю, что обычно — должно быть какое-то предостережение, касающееся обратной совместимости и / или множественного наследования, которое предотвращает это.

Местные занятия

1
2
3
4
5
6
class Outer {
    void somewhereInside() {
        class Inner {
        }
    }
}

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

Анонимные занятия

1
2
3
class Outer {
    Serializable dummy = new Serializable() {};
}

Анонимные классы являются подтипами другого типа только с одним экземпляром.

Лучшие 5 вариантов использования для вложенных классов

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

Однако, часто вы хотите получить прибыль от этого включенного экземпляра. Может быть весьма полезно иметь некоторый объект «сообщения», который вы можете вернуть, не раскрывая фактическую реализацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
class Outer {
 
    // This implementation is private ...
    private class Inner implements Message {
        @Override
        public void getMessage() {
            Outer.this.someoneCalledMe();
        }
    }
 
    // ... but we can return it, being of
    // type Message
    Message hello() {
        return new Inner();
    }
 
    void someoneCalledMe() {}
}

Однако для (статических) вложенных классов не существует охватывающей области видимости, поскольку экземпляр Inner полностью независим от любого экземпляра Outer . Так какой смысл использовать такой вложенный класс, а не тип верхнего уровня?

1. Ассоциация с внешним типом

Если вы хотите общаться со всем миром, эй, этот (внутренний) тип полностью связан с этим (внешним) типом и не имеет смысла сам по себе, тогда вы можете вкладывать типы. Это было сделано с помощью Map и Map.Entry , например:

1
2
3
4
public interface Map<K, V> {
    interface Entry<K, V> {
    }
}

2. Скрытие от внешнего вида

Если видимости пакета (по умолчанию) недостаточно для ваших типов, вы можете создать private static классы, которые доступны только для их вложенного типа и для всех других вложенных типов вложенного типа. Это действительно основной вариант использования для статических вложенных классов.

1
2
3
4
5
6
7
8
class Outer {
    private static class Inner {
    }
}
 
class Outer2 {
    Outer.Inner nope;
}

3. Защищенные типы

Это действительно очень редкий вариант использования, но иногда внутри иерархии классов вам нужны типы, которые вы хотите сделать доступными только для подтипов данного типа. Это пример использования для protected static классов:

01
02
03
04
05
06
07
08
09
10
11
12
class Parent {
    protected static class OnlySubtypesCanSeeMe {
    }
 
    protected OnlySubtypesCanSeeMe someMethod() {
        return new OnlySubtypesCanSeeMe();
    }
}
 
class Child extends Parent {
    OnlySubtypesCanSeeMe wow = someMethod();
}

4. Эмулировать модули

В отличие от Цейлона, Java не имеет первоклассных модулей . С Maven или OSGi можно добавить некоторое модульное поведение в среду сборки Java (Maven) или среды выполнения (OSGi), но если вы хотите выразить модули в коде, это на самом деле невозможно.

Однако вы можете устанавливать модули по соглашению, используя статические вложенные классы. Давайте посмотрим на пакет java.util.stream . Мы могли бы считать его модулем, и в этом модуле у нас есть пара «подмодулей» или группы типов, например внутренний класс java.util.stream.Nodes , который примерно выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
final class Nodes {
    private Nodes() {}
    private static abstract class AbstractConcNode {}
    static final class ConcNode {
        static final class OfInt {}
        static final class OfLong {}
    }
    private static final class FixedNodeBuilder {}
    // ...
}

Некоторые из этих Nodes доступны для всего пакета java.util.stream , поэтому мы можем сказать, что, как это написано, у нас есть что-то вроде:

  • синтетический java.util.stream.nodes , видимый только для «модуля» java.util.stream
  • пара типов java.util.stream.nodes.* , видимых также только для «модуля» java.util.stream
  • пара функций «верхнего уровня» (статические методы) в синтетическом пакете java.util.stream.nodes

Для меня это очень похоже на Цейлон!

5. Косметические причины

Последнее немного скучно. Или некоторые могут найти это интересным . Это касается вкуса или простоты написания вещей. Некоторые классы настолько малы и не важны, что их проще написать в другом классе. Сохраняет файл .java . Почему бы и нет.

Вывод

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

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