Хорошие причины запретить наследование в Java?

178

Каковы веские причины для запрета наследования в Java, например, с использованием конечных классов или классов с использованием одного частного конструктора без параметров? Каковы веские причины сделать метод окончательным?

cretzel
источник
1
Pedantry: конструктор по умолчанию - тот, который сгенерирован для вас компилятором Java, если вы явно не пишете его в своем коде. Вы имеете в виду конструктор без аргументов.
Джон Топли
Разве это не «неявный конструктор по умолчанию»?
критцель
2
«Вам не нужно предоставлять конструкторы для вашего класса, но вы должны быть осторожны при этом. Компилятор автоматически предоставляет конструктор по умолчанию без аргументов для любого класса без конструкторов». java.sun.com/docs/books/tutorial/java/javaOO/constructors.html - Джон Топли
Джон Топли

Ответы:

184

Ваша лучшая ссылка здесь - это пункт 19 из превосходной книги Джошуа Блоха «Эффективная Ява» под названием «Дизайн и документ для наследования, иначе запретите» (Это пункт 17 во втором издании и пункт 15 в первом издании.) Вы действительно должны прочитать его, но я подведу итоги.

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

Поэтому классы должны быть двух видов:

  1. Классы, предназначенные для расширения , и с достаточным количеством документации, чтобы описать, как это должно быть сделано

  2. Классы отмечены как финал

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

DJClayworth
источник
6
По моему опыту, это не излишне, если кто-то, кроме меня, будет использовать код. Я никогда не понимал, почему в Java есть методы, переопределяемые по умолчанию.
Эрик Вейльну
9
Чтобы понять некоторые из эффективных Java, вам нужно понять, как Джош берет на себя дизайн. Он говорит, что вы должны всегда проектировать интерфейсы классов, как если бы они были публичным API. Я думаю, что мнение большинства состоит в том, что этот подход обычно слишком тяжелый и негибкий. (ЯГНИ и т. Д.)
Том Хотин - tackline
9
Хороший ответ. (Не могу не отметить, что это шесть символов, так как есть пробел ... ;-)
joel.neely
36
Пожалуйста, обратите внимание, что окончательное занятие в классе может усложнить тестирование (сложнее
заглушить
10
Если последний класс пишет в интерфейс, то насмешка не должна быть проблемой.
Фред Хаслам
26

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

Билл Ящерица
источник
Не совсем понял этот ответ. Если вы не возражаете, не могли бы вы объяснить, что вы подразумеваете под "переопределением классов, которые не могут изменить поведение, на которое рассчитывают другие методы"? Я спрашиваю, во-первых, потому что я не понял предложение, а во-вторых, потому что Netbeans рекомендует, чтобы одна из моих функций, которые я вызываю из конструктора, должна была быть final.
Nav
1
@Nav Если вы сделаете метод final, тогда переопределяющий класс не сможет иметь свою собственную версию этого метода (или это будет ошибка компиляции). Таким образом, вы знаете, что поведение, которое вы реализуете в этом методе, не изменится позже, когда кто-то еще расширит класс.
Билл Ящерица
19

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

Джон Топли
источник
15

Есть 3 варианта использования, где вы можете пойти на финальные методы.

  1. Во избежание переопределения производного класса определенной функциональности базового класса.
  2. Это делается в целях безопасности, где базовый класс предоставляет некоторые важные базовые функциональные возможности платформы, где производный класс не должен ее изменять.
  3. Финальные методы работают быстрее, чем методы экземпляров, так как для окончательных и закрытых методов не используется концепция виртуальной таблицы. Поэтому, когда есть возможность, попробуйте использовать финальные методы.

Цель сделать финал урока:

Так что никто не может расширить эти классы и изменить их поведение.

Например: класс Wrapper Integer является финальным классом. Если этот класс не является окончательным, то любой может расширить Integer в свой собственный класс и изменить базовое поведение целочисленного класса. Чтобы избежать этого, java сделал все классы-обёртки финальными.

user1923551
источник
Не очень убедительно с вашим примером. Если это так, то почему бы не сделать методы и переменные окончательными, а не сделать весь класс окончательным. Можете ли вы отредактировать свой ответ с деталями.
Раджкиран
4
Даже если вы сделаете все переменные и методы окончательными, третьи могут унаследовать ваш класс, добавить дополнительные функциональные возможности и изменить базовое поведение вашего класса.
user1923551
3
> Если этот класс не является окончательным, то любой может расширить Integer до своего собственного класса и изменить базовое поведение целочисленного класса. Допустим, это было разрешено, и этот новый класс был назван DerivedInteger, он по-прежнему не меняет исходный класс Integer, и тот, кто его использует, DerivedIntegerделает это на свой страх и риск, поэтому я до сих пор не понимаю, почему это проблема.
Сиддхартха
7

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

См. Пункты «Эффективная Java 2-е издание», пункты 16 и 17, или мой пост в блоге «Налог на наследство» .

Джон Скит
источник
Ссылка на пост в блоге не работает. Кроме того, как вы думаете, вы могли бы подробнее рассказать об этом?
@MichaelT: Исправлена ​​ссылка, спасибо - следуйте по ней для более детальной проработки :) (Боюсь, у меня нет времени, чтобы добавить это прямо сейчас)
Джон Скит,
@JonSkeet, ссылка на пост в блоге не работает.
Люцифер
@Kedarnath: Это работает для меня ... оно какое-то время было сломано, но теперь оно указывает на правильную страницу на codeblog.jonskeet.uk ...
Джон Скит
4

Хммм ... я могу думать о двух вещах:

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

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

 String blah = someOtherString;

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

Mecki
источник
2

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

DavidG
источник
2

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

seanalltogether
источник
1
Я считаю, что это миф, поскольку виртуальные машины текущего поколения должны иметь возможность оптимизировать и деоптимизировать по мере необходимости. Я помню, как видел это и сравнивал с мифом.
Мигель Пинг
1
Разница в генерации кода - не миф, но, возможно, выигрыш в производительности. Как я уже сказал, это небольшой выигрыш, один сайт упомянул о повышении производительности на 3,5%, а некоторые выше, что в большинстве случаев не стоит того, чтобы весь код был нацистским.
целом
1
Это не миф. Фактически, компилятор может только встроить V в финальные методы. Я не уверен, что какие-либо JIT-файлы «встроены» во время выполнения, но я сомневаюсь, что это возможно, так как вы можете загружать производный класс позже.
Uri
1
Microbenchmark == зерна из-соли. Тем не менее, после прогрева цикла 10B, средние вызовы более 10B не показывают никакой разницы в Eclipse на моем ноутбуке. Два метода, возвращающие String, final были 6.9643us, не финальные - 6.9641us. Эта дельта является фоновым шумом, ИМХО.
joel.neely
1

Чтобы люди не могли делать то, что могло бы сбить их с толку и окружающих. Представьте себе физическую библиотеку, в которой у вас есть определенные константы или расчеты. Без использования ключевого слова final кто-то может прийти и переопределить базовые вычисления или константы, которые НИКОГДА не должны меняться.

Энсон Смит
источник
2
Есть что-то, чего я не понимаю в этом аргументе - если кто-то изменил те вычисления / константы, которые не должны меняться, - не являются ли какие-либо сбои из-за этого на 100% их ошибкой? Другими словами, как изменения принесут пользу?
Мэтт б
Да, технически это «их» вина, но представьте, что кто-то еще, использующий библиотеку, который не был осведомлен об изменениях, может привести к путанице. Я всегда думал, что именно по этой причине многие базовые классы Java были помечены как окончательные, чтобы люди не могли изменять базовую функциональность.
Энсон Смит
@matt b - Похоже, вы предполагаете, что разработчик, который внес изменение, сделал это сознательно, или что им все равно, что это их вина. Если кто-то, кроме меня, будет использовать мой код, я отмечу его как окончательный, если только он не должен быть изменен.
Эрик Вейльну
Это на самом деле отличается от «окончательного» использования того, о котором спрашивали.
DJClayworth
Этот ответ, кажется, смешивает наследование и изменчивость отдельных полей со способностью писать подкласс. Вы можете пометить отдельные поля и методы как окончательные (предотвращая их переопределение) без запрета наследования.
joel.neely
0

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

Александр Робинсон
источник