Чому віддають перевагу нестатичним внутрішнім класам над статичним?


37

Це питання стосується того, щоб зробити вкладений клас у Java статичним класом вкладених або внутрішнім вкладеним класом. Я шукав тут і в Stack Overflow, але насправді не міг знайти жодних питань щодо дизайнерських наслідків цього рішення.

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

Ось моє розуміння ефекту від використання статичних вкладених класів:

  • Менше з'єднання: ми зазвичай отримуємо менше з’єднання, оскільки клас не може безпосередньо отримати доступ до атрибутів зовнішнього класу. Менше з'єднання зазвичай означає кращу якість коду, простіше тестування, рефакторинг тощо.
  • Одномісний Class: Навантажувач класів не повинен опікуватися новим класом щоразу, ми створюємо об'єкт зовнішнього класу. Ми просто отримуємо нові об'єкти для одного класу знову і знову.

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

Отже, моє запитання зводиться до цього: чи помиляюсь я, вважаючи, що доступ до атрибутів, доступних нестатичним внутрішнім класам, призводить до високої зв'язку, отже, до нижчої якості коду, і що я випливаю з цього, що (неанонімні) вкладені класи повинні взагалі бути статичним?

Або іншими словами: Чи є переконлива причина, чому можна віддати перевагу вкладеному внутрішньому класу?


2
з підручника Java : "Використовуйте нестатичний вкладений клас (або внутрішній клас), якщо вам потрібен доступ до непублічних полів і методів вкладеного екземпляра. Використовуйте статичний вкладений клас, якщо вам цього не потрібно."
гнат

5
Дякую за цитату підручника, але це лише повторює, в чому різниця, а не чому ви хочете отримати доступ до полів екземпляра.
Френк

це скоріше загальне керівництво, яке натякає на те, що віддавати перевагу. Я додав більш детальне пояснення того, як я трактую це у цій відповіді
гнат

1
Якщо внутрішній клас не є щільно пов'язаним із класом, що вкладається, він, мабуть, не повинен бути внутрішнім класом в першу чергу.
Кевін Крумвіеде

Відповіді:


23

Джошуа Блох у пункті 22 своєї книги "Ефективне друге видання Java" розповідає, коли використовувати тип вкладеного класу та чому. Нижче наведено кілька цитат:

Одне поширене використання статичного класу-члена - як загальнодоступний клас-помічник, корисний лише у поєднанні з його зовнішнім класом. Наприклад, розглянемо перелік, який описує операції, підтримувані калькулятором. Операція enum повинна бути публічним статичним класом члена 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модифікатор у свою декларацію, роблячи його статичним, а не нестатичним класом-членом.


1
Я не зовсім впевнений, як / чи цей адаптер змінює вигляд MySetекземплярів зовнішнього класу ? Я міг би передбачити щось на зразок типу, залежного від шляху, який є різницею, тобто myOneSet.iterator.getClass() != myOtherSet.iterator.getClass(), але знову ж таки, у Java це насправді неможливо, оскільки клас був би однаковий для кожного.
Френк

@Frank Це досить типовий клас адаптерів - він дозволяє розглядати набір як потік його елементів (Ітератор).
користувач253751

@immibis дякую, я добре знаю, що робить ітератор / адаптер, але це питання стосувалося того, як це реалізується.
Френк

@Frank Я говорив, як це змінює погляд на зовнішній екземпляр. Саме це робить адаптер - він змінює спосіб перегляду якогось об'єкта. У цьому випадку цей об'єкт є зовнішнім екземпляром.
користувач253751

10

Ви вірно припускаєте, що доступ до атрибутів, доступний нестатичним внутрішнім класам, призводить до високої зв'язку, отже, до нижчої якості коду, а внутрішні класи (неанонімні та не локальні ), як правило, статичні.

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

Щоразу, коли ви пишете клас-член, запитайте себе: чи справді цей клас потребує прикладу, що додається? Якщо відповідь - ні, зробіть це static. Внутрішні заняття іноді корисні, але вони легко запроваджують ускладнення, що ускладнюють розуміння програми. Вони мають складну взаємодію з дженериками (Головоломка 89), рефлексією (Головоломка 80) та успадкуванням (ця загадка) . Якщо ви заявляєте , що проблема Inner1є static, проблема відходить. Якщо ви також заявите , що ви Inner2є static, ви можете зрозуміти, що робить програма: приємний бонус.

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

Якщо вас цікавить, більш детальний аналіз Puzzle 90 наведено у цій відповіді на сторінці переповнення стека .


Варто зазначити, що вище - це, по суті, розширена версія вказівок, наведених у навчальному посібнику з класів та об’єктів Java :

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

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

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


В цілому мої (досить болісні) зустрічі з налагодженням внутрішніх класів, що безпосередньо отримують доступ до полів примірника, що склалися, справили сильне враження, що ця практика нагадує використання глобальної держави, а також відомі злу, пов'язані з нею.

Звичайно, Java робить це так, що пошкодження такого "квазі глобального" міститься в закритому класі, але коли мені довелося налагоджувати особливий внутрішній клас, здавалося, що така допомога групи не допомогла зменшити біль: у мене все одно було пам’ятати про «іноземні» семантики та деталі замість того, щоб повністю зосередитись на аналізі конкретного проблемного об’єкта.


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

Повертає Setперегляд клавіш, що містяться на цій карті. Набір підтримується картою, тому зміни на карті відображаються у наборі, і навпаки ...

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


Я поділяю ваш досвід на проблемах , викликаних цією сильним зв'язком, але я не в порядку з використанням навчальної програми про потрібна . Ви кажете самі, що ніколи не вимагали доступу до поля, тож, на жаль, це також не повністю відповідає моєму питанню.
Френк

@Frank добре, як ви бачите, моє тлумачення навчальних посібників (які, здається, підтримуються Java Puzzlers) схоже, використовуйте нестатичні класи лише тоді, коли ви зможете довести, що це потрібно, і, зокрема, довести, що альтернативні способи є гірше . Варто зазначити, що, на мій досвід, альтернативні способи завжди виявлялися кращими
гнат

В тім-то й річ. Якщо це завжди краще, то чому ми турбуємось?
Френк

@Frank Інша відповідь наводить переконливі приклади, що це не завжди так. Згідно з моїм читанням map.keySet javadocs , ця функція пропонує чітке з'єднання і, як наслідок, недійсні аргументи проти нестатичних класів "Набір підтримується картою, тому зміни на карті відображаються у наборі, і навпаки ... »
гнат

1

Деякі помилкові уявлення:

Менше з'єднання: ми, як правило, отримуємо менше з'єднання, оскільки [статичний внутрішній] клас не може безпосередньо отримати доступ до атрибутів зовнішнього класу.

IIRC - Власне, може. (Звичайно, якщо вони є об'єктними полями, у нього не буде зовнішнього об’єкта для перегляду. Тим не менш, якщо він отримає такий об’єкт з будь-якого місця, то він може безпосередньо отримати доступ до цих полів).

Однокласний клас: Навантажувач класів не повинен щоразу доглядати за новим класом, ми створюємо об'єкт зовнішнього класу. Ми просто отримуємо нові об'єкти для одного класу знову і знову.

Я не зовсім впевнений, що ви тут маєте на увазі. Завантажувач класу бере участь лише два рази, один раз для кожного класу (коли клас завантажений).


Рамбінг наступним чином:

У Javaland ми, здається, трохи боїмося створювати класи. Я думаю, що це має відчувати себе як важлива концепція. Зазвичай він належить до власного файлу. Тут потрібна значна котельня. Вона потребує уваги до свого дизайну.

На відміну від інших мов - наприклад, Scala або, можливо, C ++: Не існує абсолютно жодної драми, яка б створювала крихітний власник (клас, структура, будь-який інший), коли два біти даних належать разом. Гнучкі правила доступу допомагають вам, скорочуючи котельну плиту, дозволяючи вам підтримувати відповідний код фізично ближче. Це зменшує «відстань розуму» між двома класами.

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

(... Якби ви запитали «чому нестатичних громадський внутрішній клас?» Це дуже хороше запитання - я не думаю , що я коли - або ще не знайшов виправдане справу для тих , хто).

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


Статичні внутрішні класи все ще є добрим підходом за замовчуванням , btw. Нестатичне має додаткове приховане поле, сформоване через яке внутрішній клас посилається на зовнішнє.


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");
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.