неизменяемый класс должен быть окончательным?

В этой статье говорится, что:

Сделать класс final, потому что он неизменяемый, — веская причина для этого.

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


person Dónal    schedule 28.09.2008    source источник


Ответы (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;

   }

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

person Vinoth Kumar C M    schedule 26.09.2012

Следуя принципу замещения Лискова, подкласс может расширять, но никогда не переопределять контракт своего родителя. Если базовый класс неизменяем, то трудно найти примеры того, где его функциональность можно было бы с пользой расширить, не нарушая контракт.

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

Я полагаю, вы могли бы объявить все поля как частные, а все методы как окончательные, но тогда какой смысл в наследовании?

person Garth Gilmour    schedule 28.09.2008
comment
Скажем, у вас есть неизменяемый класс Shape со свойством area. Возможно, вы захотите создать подкласс Circle с дополнительными свойствами, такими как радиус. Эти новые свойства сами по себе могут быть неизменяемыми, сохраняя, таким образом, свойство неизменности родителя. Что в этом плохого? - person Dónal; 28.09.2008
comment
Я не думаю, что в этом есть что-то неправильное, если вы единственный человек, вышедший из Shape. Но вы должны были бы создать четкие инструкции для других разработчиков и быть уверенными, что они будут им следовать. Поэтому я думаю, что аргумент в том, что в 99,9% случаев это не стоит усилий... - person Garth Gilmour; 28.09.2008
comment
ГГ при деньгах. Если вы даете гарантию, вы должны ее сохранить. - person CurtainDog; 15.02.2010
comment
@Don Разве Shape не будет абстрактным классом в этом сценарии? - person crush; 15.08.2013

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

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

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

person cynicalman    schedule 28.09.2008
comment
Но вы можете захотеть расширить неизменяемый класс, чтобы добавить дополнительные неизменяемые свойства. - person Dónal; 28.09.2008
comment
Потому что, если класс является окончательным, вы не можете его расширить и сделать изменяемым. Но вы не можете расширить неокончательный неизменяемый класс и сделать его изменяемым, вы можете сделать изменяемым только подкласс. - person Dónal; 28.09.2008
comment
Как вызывающий объект вы не знаете, используете ли вы неизменяемый класс или его изменяемый подкласс, если только вы не вызвали конструктор напрямую. Это полиморфизм. - person slim; 28.09.2008

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

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

Теперь у кого-то может возникнуть соблазн написать такой код менеджера безопасности;

void checkUrl(MyUrlClass testurl) throws SecurityException {
    if (illegalDomains.contains(testurl.getDomain())) throw new SecurityException();
}

И вот что они добавили в свой метод DoRequest(MyUrlClass url):

securitymanager.checkUrl(urltoconnect);
Socket sckt = opensocket(urltoconnect);
sendrequest(sckt);
getresponse(sckt);

Но они не могут этого сделать, потому что вы не сделали MyUrlClass final. Причина, по которой они не могут этого сделать, заключается в том, что если бы они это сделали, код мог бы обойти ограничения менеджера безопасности, просто переопределив getDomain() так, чтобы он возвращал "www.google.com" при первом вызове, и "www.evilhackers.org" второй и передать объект своего класса в DoRequest().

Я ничего не имею против evilhackers.org, кстати, если он вообще существует...

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

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

person Steve Jessop    schedule 28.09.2008
comment
Как я уже сказал, можно справедливо утверждать, что неспособность учитывать наследование в ваших сложных классах — это то, чего следует избегать... Но если вы не можете объяснить это, вы можете, по крайней мере, сообщить об этом факте, предотвратив его. И вы спорят об этом. Весьма. - person Steve Jessop; 19.11.2011

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

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

Кроме того, вам не нужно делать класс final. Вы можете сделать конструкторы закрытыми.

person ishmeister    schedule 27.01.2010
comment
Всего несколько заметок. 1) Обычно экземпляр изменяемого класса лучше по соображениям производительности по сравнению с эквивалентным неизменяемым экземпляром. Неизменяемые экземпляры обычно потокобезопасны, но неизменяемость требует затрат, если вы хотите получить более новый объект даже с немного другим состоянием (используя шаблон прототипа или конструктора). 2) Integer.valueOf является примером шаблона Flightweight, и шаблон обычно не связан с вопросом неизменности. 3) Наличие частного конструктора не может предотвратить создание подклассов с использованием внутреннего класса. - person Lyubomyr Shaydariv; 14.01.2013

Итак, почему неизменяемость является хорошей причиной для создания класса final?

Как указано в документах Oracle, необходимо выполнить 4 шага. класс неизменяемый.

Итак, один из пунктов гласит, что

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

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

  1. Не предоставляйте методы-установщики — методы, которые изменяют поля или объекты, на которые ссылаются поля.

  2. Сделайте все поля окончательными и закрытыми.

  3. Не позволяйте подклассам переопределять методы. Самый простой способ сделать это — объявить класс окончательным. Более сложный подход — сделать конструктор закрытым и создавать экземпляры в фабричных методах.

  4. Если поля экземпляра содержат ссылки на изменяемые объекты, не разрешайте изменять эти объекты:

    • Don't provide methods that modify the mutable objects.
    • Не делитесь ссылками на изменяемые объекты. Никогда не храните ссылки на внешние изменяемые объекты, переданные конструктору; при необходимости создайте копии и сохраните ссылки на копии. Точно так же создавайте копии ваших внутренних изменяемых объектов, когда это необходимо, чтобы избежать возврата оригиналов в ваших методах.
person Mateen    schedule 27.05.2018