Неизменный класс должен быть окончательным?
В этой статье говорится, что:
Хорошим основанием для этого является окончательный урок, потому что он неизменный.
Я немного озадачен этим... Я понимаю, что неизменность - это хорошая вещь от POV безопасности потоков и простоты, но кажется, что эти проблемы несколько ортогональны расширяемости. Итак, почему неизменность является хорошей причиной для того, чтобы сделать урок финальным?
6 ответов
Объяснение этому дано в книге "Эффективная Java"
Рассматривать BigDecimal
а также BigInteger
занятия на Java.
Не было широко понято, что неизменные классы должны были быть эффективно окончательными, когда BigInteger
а также BigDecimal
были написаны, поэтому все их методы могут быть переопределены. К сожалению, это не могло быть исправлено после факта при сохранении обратной совместимости.
Если вы пишете класс, безопасность которого зависит от неизменности аргумента BigInteger или BigDecimal от ненадежного клиента, вы должны убедиться, что аргумент является "настоящим" BigInteger или BigDecimal, а не экземпляром недоверенного подкласса., Если это последнее, вы должны защищенно скопировать его в предположении, что оно может быть изменяемым.
public static BigInteger safeInstance(BigInteger val) {
if (val.getClass() != BigInteger.class)
return new BigInteger(val.toByteArray());
return val;
}
Если вы разрешите подклассификацию, это может нарушить "чистоту" неизменяемого объекта.
Потому что, если класс будет последним, вы не сможете расширить его и сделать его изменчивым.
Даже если вы сделаете поля окончательными, это только означает, что вы не можете переназначить ссылку, это не значит, что вы не можете изменить объект, на который ссылается.
Я не вижу большой пользы в дизайне для неизменяемого класса, который также должен быть расширен, поэтому final помогает сохранить неизменность неизменной.
Следуя принципу подстановки Лискова, подкласс может расширяться, но никогда не переопределять контракт своего родителя. Если базовый класс является неизменяемым, то трудно найти примеры того, где его функциональность могла бы быть полезна для расширения без нарушения контракта.
Обратите внимание, что в принципе возможно расширить неизменный класс и изменить базовые поля, например, если базовый класс содержит ссылку на массив, элементы в массиве не могут быть объявлены как окончательные. Очевидно, что семантика методов также может быть изменена путем переопределения.
Я полагаю, вы могли бы объявить все поля как частные и все методы как финальные, но тогда какой смысл использовать наследование?
В основном безопасность, я думаю. По той же причине, что String является окончательным, все, что любой код, связанный с безопасностью, хочет рассматривать как неизменяемое, должно быть окончательным.
Предположим, у вас есть класс, определенный как неизменяемый, назовите его MyUrlClass, но вы не помечаете его как окончательный.
Теперь у кого-то может возникнуть желание написать код менеджера безопасности, подобный этому;
void checkUrl(MyUrlClass testurl) throws SecurityException {
if (illegalDomains.contains(testurl.getDomain())) throw new SecurityException();
}
И вот что они поместили бы в метод DoRequest (URL-адрес MyUrlClass):
securitymanager.checkUrl(urltoconnect);
Socket sckt = opensocket(urltoconnect);
sendrequest(sckt);
getresponse(sckt);
Но они не могут этого сделать, потому что вы не сделали MyUrlClass финальным. Причина, по которой они не могут этого сделать, заключается в том, что если бы они это сделали, код мог бы избежать ограничений диспетчера безопасности, просто переопределив getDomain() для возврата "www.google.com" при первом вызове и "www.evilhackers.org". второй, и передача объекта своего класса в DoRequest().
Я ничего не имею против evilhackers.org, кстати, если он вообще существует...
При отсутствии проблем с безопасностью все сводится к тому, чтобы избежать ошибок программирования, и, конечно, от вас зависит, как вы это сделаете. Подклассы должны соблюдать контракт своих родителей, а неизменность является лишь частью контракта. Но если предполагается, что экземпляры класса являются неизменяемыми, то сделать его окончательным - это хороший способ убедиться, что все они действительно неизменны (то есть, что не существует изменяемых экземпляров подклассов, которые можно использовать везде, где родитель класс называется).
Я не думаю, что статья, на которую вы ссылались, должна восприниматься как инструкция о том, что "все неизменяемые классы должны быть окончательными", особенно если у вас есть веская причина создать свой неизменяемый класс для наследования. Это говорит о том, что защита неизменности является веской причиной для окончательного решения, когда мнимые проблемы производительности (о чем на самом деле идет речь в этот момент) недопустимы. Обратите внимание, что он дал "сложный класс, не предназначенный для наследования" в качестве одинаково веской причины. Можно справедливо утверждать, что отказ от учета наследования в ваших сложных классах - это то, чего следует избегать, так же, как отказ от учета наследования в ваших неизменяемых классах. Но если вы не можете объяснить это, вы можете, по крайней мере, сигнализировать об этом факте, предотвращая его.
Итак, почему неизменность является хорошей причиной для того, чтобы сделать урок финальным?
Как указано в документах оракула, есть четыре шага, чтобы сделать класс неизменным.
Таким образом, один из пунктов утверждает, что
чтобы сделать класс неизменяемым класс должен быть помечен как окончательный или иметь приватный конструктор
Ниже приведены 4 шага, чтобы сделать класс неизменным (прямо из документов оракула).
Не предоставляйте "установочные" методы - методы, которые изменяют поля или объекты, на которые ссылаются поля.
Сделайте все поля окончательными и приватными.
Не позволяйте подклассам переопределять методы. Самый простой способ сделать это - объявить класс как final. Более сложный подход - сделать конструктор частным и создать экземпляры в фабричных методах.
Если поля экземпляра содержат ссылки на изменяемые объекты, не допускайте изменения этих объектов:
- Не предоставляйте методы, которые изменяют изменяемые объекты.
- Не делитесь ссылками на изменяемые объекты. Никогда не храните ссылки на внешние изменяемые объекты, передаваемые в конструктор; при необходимости создайте копии и сохраните ссылки на копии. Аналогичным образом, при необходимости создайте копии ваших внутренних изменяемых объектов, чтобы избежать возврата оригиналов в ваши методы.
Хорошая идея - сделать класс неизменным и по соображениям производительности. Возьмите Integer.valueOf например. Когда вы вызываете этот статический метод, ему не нужно возвращать новый экземпляр Integer. Он может вернуть ранее созданный экземпляр в безопасности, зная, что когда он передал вам ссылку на этот экземпляр в прошлый раз, когда вы его не модифицировали (я думаю, это тоже хорошая аргументация с точки зрения соображений безопасности).
Я согласен с точкой зрения, принятой в Effective Java по этим вопросам, заключающейся в том, что вы должны либо разработать классы для расширяемости, либо сделать их нерасширяемыми. Если вы хотите создать что-то расширяемое, возможно, рассмотрите интерфейс или абстрактный класс.
Кроме того, вам не нужно делать урок финальным. Вы можете сделать конструкторы частными.