Почему предпочитают нестатические внутренние классы статическим?

37

Вопрос в том, сделать ли вложенный класс в Java статическим вложенным классом или внутренним вложенным классом. Я искал здесь и на Stack Overflow, но не смог найти никаких вопросов, касающихся последствий этого решения для дизайна.

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

Вот мое понимание эффекта от использования статических вложенных классов:

  • Меньше связывания: мы обычно получаем меньше связывания, так как класс не может напрямую получить доступ к атрибутам своего внешнего класса. Меньшее связывание обычно означает лучшее качество кода, более легкое тестирование, рефакторинг и т. Д.
  • Одиночный Class: загрузчик классов не должен заботиться о новом классе каждый раз, когда мы создаем объект внешнего класса. Мы просто получаем новые объекты для одного и того же класса снова и снова.

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

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

Или другими словами: существует ли убедительная причина, по которой человек предпочитает вложенный внутренний класс?

Фрэнк
источник
2
из руководства по Java : «Используйте нестатический вложенный класс (или внутренний класс), если вам требуется доступ к непубличным полям и методам вмещающего экземпляра. Используйте статический вложенный класс, если вам не нужен этот доступ».
комнат
5
Спасибо за учебную цитату, но это просто повторяет разницу, а не то, почему вы хотели бы получить доступ к полям экземпляра.
Фрэнк
это скорее общее руководство, намекающее на то, что предпочитать. Я добавил более подробное объяснение того, как я интерпретирую это в этом ответе
комнат
1
Если внутренний класс не тесно связан с включающим классом, то, вероятно, он не должен быть внутренним классом.
Кевин Крумвиде

Ответы:

23

Джошуа Блох в пункте 22 своей книги «Эффективное Java, второе издание» рассказывает, когда использовать, какой тип вложенного класса и почему. Ниже приведены некоторые цитаты:

Одним из распространенных применений статического класса-члена является открытый вспомогательный класс, полезный только в сочетании с его внешним классом. Например, рассмотрим перечисление, описывающее операции, поддерживаемые калькулятором. Перечисление Operation должно быть открытым классом статического члена Calculatorкласса. Клиенты Calculatorмогут затем ссылаться на операции, используя такие имена, как Calculator.Operation.PLUSи Calculator.Operation.MINUS.

Одним из распространенных применений нестатического класса-члена является определение адаптера, который позволяет рассматривать экземпляр внешнего класса как экземпляр некоторого несвязанного класса. Например, реализация Mapинтерфейса обычно использует нестатические классы членов реализовать свои взгляды сбора , которые возвращаются на Map«S keySet, entrySetи valuesметоде. Точно так же реализации интерфейсов коллекции, такие как Setи List, обычно используют нестатические классы-члены для реализации своих итераторов:

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

Если вы объявляете класс-член, который не требует доступа к включающему экземпляру, всегда помещайте staticмодификатор в его объявление, делая его статическим, а не нестатическим классом-членом.

erakitin
источник
1
Я не совсем уверен, как / может ли этот адаптер изменить представление MySetэкземпляров внешнего класса ? Я мог бы представить что-то вроде зависимого от пути типа, являющегося разницей, то есть myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), но опять же, в Java это фактически невозможно, так как класс был бы одинаковым для каждого.
Фрэнк
@Frank Это довольно типичный класс адаптера - он позволяет вам просматривать набор как поток его элементов (итератор).
user253751
@immibis спасибо, я хорошо знаю, что делает итератор / адаптер, но этот вопрос был о том, как это реализовано.
Франк
@ Фрэнк Я говорил, как это меняет представление о внешнем экземпляре. Это именно то, что делает адаптер - он меняет способ просмотра какого-либо объекта. В этом случае этот объект является внешним экземпляром.
user253751
10

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

Конструктивные последствия решения сделать внутренний класс нестатичным изложены в Java Puzzlers , Puzzle 90 (жирный шрифт в приведенной ниже цитате - мой):

Всякий раз, когда вы пишете класс-член, спрашивайте себя, действительно ли этому классу нужен включающий экземпляр? Если ответ нет, сделайте это static. Внутренние классы иногда полезны, но они могут легко внести сложности, которые затрудняют понимание программы. Они имеют сложные взаимодействия с генериками (головоломка 89), отражением (головоломка 80) и наследованием (эта головоломка) . Если вы объявляете Inner1быть static, эта проблема уходит. Если вы также заявите Inner2о себе static, вы действительно сможете понять, что делает программа: действительно, хороший бонус.

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

Если вам интересно, более подробный анализ головоломки 90 представлен в этом ответе в Stack Overflow .


Стоит отметить , что выше, по существу , является расширенной версией руководящих указаний в Java классов и объектов учебник :

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

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

Формулировка учебника несколько широка (это может быть причиной того, что Java Puzzlers пытается усилить и сузить ее). В частности, в моем опыте никогда не требовался непосредственный доступ к вмещающим полям экземпляров - в том смысле, что альтернативные способы, такие как передача их в качестве параметров конструктора / метода, всегда оказывались проще в отладке и обслуживании.


В целом, мои (довольно болезненные) встречи с отладочными внутренними классами, непосредственно обращающимися к полям включающего экземпляра, создали сильное впечатление, что эта практика напоминает использование глобального состояния вместе с известными пороками, связанными с ним.

Конечно, Java делает так, чтобы повреждение такого «квази-глобального» было заключено во включающем классе, но когда мне пришлось отлаживать определенный внутренний класс, я чувствовал, что такая помощь от группы не помогает уменьшить боль: у меня все еще было иметь в виду «чужую» семантику и детали, вместо того чтобы полностью сосредоточиться на анализе конкретного проблемного объекта.


Для полноты картины могут быть случаи, когда приведенные выше рассуждения не применяются. Например, согласно моему прочтению javadocs map.keySet , эта функция предполагает тесную связь и в результате делает недействительными аргументы против нестатических классов:

Возвращает Setвид ключей, содержащихся в этой карте. Набор опирается на карту, поэтому изменения в карте отражаются в наборе, и наоборот ...

Не то чтобы вышесказанное каким-то образом сделало бы сложный код более легким в обслуживании, тестировании и отладке, но это, по крайней мере, позволило бы утверждать, что сложность соответствует / оправдана предполагаемой функциональностью.

комар
источник
Я поделюсь вашим опытом в отношении проблем, вызванных этим высоким сцеплением, но я не согласен с использованием в руководстве учебного пособия require . Вы сами говорите, что вам никогда не требовался доступ к полю, поэтому, к сожалению, это также не полностью отвечает на мой вопрос.
Фрэнк
@ Хорошо, как вы можете видеть, моя интерпретация руководств по учебнику (которые, похоже, поддерживаются Java Puzzlers) похожа на использование нестатических классов только тогда, когда вы можете доказать, что это требуется, и, в частности, доказать, что альтернативные способы хуже . Стоит отметить, что по моему опыту альтернативные способы всегда оказывались лучше
комнат
В этом-то и дело. Если это всегда лучше, то почему мы беспокоимся?
Фрэнк
@ Другой ответ дает убедительные примеры, что это не всегда так. Согласно моему прочтению javadocs map.keySet , эта функция предполагает тесную связь и, в результате, делает недействительными аргументы против нестатических классов. «Набор опирается на карту, поэтому изменения в карте отражаются в наборе, и наоборот ... "
комнат
1

Некоторые заблуждения:

Меньше связывания: мы обычно получаем меньше связывания, так как класс [static inner] не может напрямую обращаться к атрибутам своего внешнего класса.

IIRC - На самом деле, это возможно. (Конечно, если они являются полями объекта, у него не будет внешнего объекта для просмотра. Тем не менее, если он получит такой объект из любого места, он сможет напрямую получить доступ к этим полям).

Одиночный класс: загрузчику классов не нужно каждый раз заботиться о новом классе, мы создаем объект внешнего класса. Мы просто получаем новые объекты для одного и того же класса снова и снова.

Я не совсем уверен, что вы имеете в виду здесь. Загрузчик классов задействуется только дважды, один раз для каждого класса (когда класс загружен).


Рамблинг следует:

В Javaland мы, кажется, немного боимся создавать классы. Я думаю, что это должно быть значимой концепцией. Как правило, принадлежит в своем собственном файле. Это нуждается в значительном шаблоне. Нужно обратить внимание на его дизайн.

По сравнению с другими языками - например, Scala или, может быть, C ++: абсолютно нет драмы, создающей крошечный держатель (класс, структура и т. Д.) Каждый раз, когда два бита данных принадлежат друг другу. Гибкие правила доступа помогают вам, сокращая шаблон, позволяя вам физически приблизить соответствующий код. Это уменьшает ваше «умственное расстояние» между двумя классами.

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

(... Если бы вы спросили «почему нестатический публичный внутренний класс?», Это очень хороший вопрос - я не думаю, что я когда-либо нашел оправданное обоснование для таких случаев).

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


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

Iain
источник
0

Может использоваться для создания фабричного шаблона. Заводской шаблон часто используется, когда созданным объектам для работы нужны дополнительные параметры конструктора, но каждый раз предоставлять их утомительно:

Рассмотреть возможность:

public class QueryFactory {
    @Inject private Database database;

    public Query create(String sql) {
        return new Query(database, sql);
    }
}

public class Query {
    private final Database database;
    private final String sql;

    public Query(Database database, String sql) {
        this.database = database;
        this.sql = sql;
    } 

    public List performQuery() {
        return database.query(sql);
    }
}

Используется как:

Query q = queryFactory.create("SELECT * FROM employees");

q.performQuery();

То же самое может быть достигнуто с помощью нестатического внутреннего класса:

public class QueryFactory {
    @Inject private Database database;

    public class Query {
        private final String sql;

        public Query(String sql) {
            this.sql = sql;
        }

        public List performQuery() {
            return database.query(sql);
        }
    }
}

Использовать как:

Query q = queryFactory.new Query("SELECT * FROM employees");

q.performQuery();

Фабрики выигрывают от того, что имеют специально названные методы, например create, createFromSQLили createFromTemplate. То же самое можно сделать и здесь в виде нескольких внутренних классов:

public class QueryFactory {

    public class SQL { ... }
    public class Native { ... }
    public class Builder { ... }

}

Требуется меньше передачи параметров от фабрики к сконструированному классу, но читаемость может немного пострадать.

Для сравнения:

Queries.Native q = queries.new Native("select * from employees");

против

Query q = queryFactory.createNative("select * from employees");
john16384
источник