Я читал, что для того, чтобы сделать класс неизменяемым в Java, мы должны сделать следующее:
- Не предоставлять никаких сеттеров
- Отметить все поля как частные
- Сделать класс финальным
Почему требуется шаг 3? Почему я должен отмечать класс final
?
Я читал, что для того, чтобы сделать класс неизменяемым в Java, мы должны сделать следующее:
Почему требуется шаг 3? Почему я должен отмечать класс final
?
BigInteger
, вы не знаете, является ли он неизменным или нет. Это испорченный дизайн. / java.io.File
- более интересный пример.
File
интересен тем, что его можно доверять как неизменяемый, что приводит к атакам TOCTOU (время проверки / время использования). Надежный код проверяет File
наличие допустимого пути, а затем использует его. Подкласс File
может изменять значение между проверкой и использованием.
Make the class final
или Убедитесь, что все подклассы неизменяемы
Ответы:
Если вы не отметите класс final
, я могу внезапно сделать ваш кажущийся неизменным класс действительно изменяемым. Например, рассмотрим этот код:
public class Immutable {
private final int value;
public Immutable(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Теперь предположим, что я делаю следующее:
public class Mutable extends Immutable {
private int realValue;
public Mutable(int value) {
super(value);
realValue = value;
}
public int getValue() {
return realValue;
}
public void setValue(int newValue) {
realValue = newValue;
}
public static void main(String[] arg){
Mutable obj = new Mutable(4);
Immutable immObj = (Immutable)obj;
System.out.println(immObj.getValue());
obj.setValue(8);
System.out.println(immObj.getValue());
}
}
Обратите внимание, что в моем Mutable
подклассе я переопределил поведение, getValue
чтобы прочитать новое изменяемое поле, объявленное в моем подклассе. В результате ваш класс, который изначально выглядит неизменным, на самом деле не является неизменным. Я могу передать этот Mutable
объект везде, где Immutable
ожидается объект, что может сделать очень плохие вещи для кода, если объект действительно неизменяем. Маркировка базового класса final
предотвращает это.
Надеюсь это поможет!
Immutable
аргумент. Я могу передать Mutable
объект этой функции, поскольку Mutable extends Immutable
. Внутри этой функции, хотя вы думаете, что ваш объект неизменен, у меня мог бы быть вторичный поток, который меняет значение при запуске функции. Я также мог бы дать вам Mutable
объект, который хранит функция, а затем изменить его значение извне. Другими словами, если ваша функция предполагает, что значение неизменяемо, она может легко сломаться, так как я могу дать вам изменяемый объект и изменить его позже. Имеет ли это смысл?
Mutable
объект, не изменит объект. Проблема в том, что этот метод может предполагать, что объект неизменен, хотя на самом деле это не так. Например, метод может предполагать, что, поскольку он считает объект неизменным, его можно использовать как ключ в файле HashMap
. Затем я мог бы сломать эту функцию, передав a Mutable
, дождавшись, пока он сохранит объект в качестве ключа, а затем изменил Mutable
объект. Теперь поиск в нем HashMap
не удастся, потому что я изменил ключ, связанный с объектом. Имеет ли это смысл?
Вопреки тому , что многие люди считают, что делает неизменный класс final
является не требуется.
Стандартный аргумент в пользу создания неизменяемых классов final
заключается в том, что если вы этого не сделаете, подклассы могут добавить изменчивость, тем самым нарушив контракт суперкласса. Клиенты класса предполагают неизменность, но будут удивлены, если что-то изменится из-под них.
Если вы доведете этот аргумент до его логической крайности, тогда должны быть созданы все методы final
, поскольку в противном случае подкласс может переопределить метод способом, который не соответствует контракту его суперкласса. Интересно, что большинство программистов на Java считают это нелепым, но почему-то согласны с идеей о том, что неизменяемые классы должны быть такими final
. Я подозреваю, что это как-то связано с Java-программистами, которые в целом не совсем довольны понятием неизменяемости, и, возможно, с каким-то нечетким мышлением, связанным с множественными значениями final
ключевого слова в Java.
Соответствие контракту вашего суперкласса - это не то, что может или всегда должно выполняться компилятором. Компилятор может обеспечить соблюдение определенных аспектов вашего контракта (например: минимальный набор методов и их сигнатуры типов), но есть много частей типичных контрактов, которые не могут быть реализованы компилятором.
Неизменяемость - это часть контракта класса. Это немного отличается от некоторых вещей, к которым люди более привыкли, потому что в нем говорится о том, что класс (и все подклассы) не может делать, в то время как я думаю, что большинство программистов Java (и вообще ООП) склонны думать о контрактах как о связанных с что может делать класс , а не то, что он не может .
Неизменность также влияет больше , чем просто один метод - это влияет на весь экземпляр - но это на самом деле не сильно отличается от пути , equals
и hashCode
в Java работы. У этих двух методов есть конкретный контракт Object
. В этом контракте очень тщательно прописаны вещи, которые эти методы сделать не могут . Этот контракт конкретизируется в подклассах. Это очень легко отменить equals
или hashCode
нарушить договор. Фактически, если вы переопределите только один из этих двух методов без другого, скорее всего, вы нарушите контракт. Так должно equals
и hashCode
были объявлены final
в , Object
чтобы избежать этого? Я думаю, что большинство будет утверждать, что не следует. Точно так же нет необходимости делать неизменяемые классыfinal
.
Тем не менее, большинство ваших классов, неизменяемых или нет, вероятно, должны быть final
. См. Пункт 17 действующего второго издания Java : «Разработайте и задокументируйте наследование, иначе запретите его».
Итак, правильная версия вашего шага 3 будет выглядеть так: «Сделайте класс окончательным или, при проектировании для создания подклассов, четко задокументируйте, что все подклассы должны оставаться неизменными».
X
и Y
значение X.equals(Y)
было неизменным (до тех пор, пока X
и Y
продолжают ссылаться на одни и те же объекты). Он имеет аналогичные ожидания относительно хэш-кодов. Понятно, что никто не должен ожидать, что компилятор обеспечит неизменность отношений эквивалентности и хэш-кодов (поскольку он просто не может). Я не вижу причин, по которым люди должны ожидать, что это будет применяться для других аспектов типа.
double
. Можно получить a, GeneralImmutableMatrix
который использует массив в качестве резервного хранилища, но также может быть, например, ImmutableDiagonalMatrix
который просто хранит массив элементов по диагонали (чтение элемента X, Y даст Arr [X], если X == y и ноль в противном случае) .
final
ограничивает его полезность, особенно когда вы разрабатываете API, предназначенный для расширения. В интересах обеспечения безопасности потоков имеет смысл сделать ваш класс как можно более неизменным, но при этом сохранить его расширяемым. Вы можете сделать поля protected final
вместо private final
. Затем явно установите контракт (в документации) о подклассах, придерживающихся гарантий неизменности и безопасности потоков.
final
. Просто потому, что equals
и hashcode
не может быть окончательным, не означает, что я могу терпеть то же самое для неизменяемого класса, если у меня нет веской причины для этого, и в этом случае я могу использовать документацию или тестирование в качестве линии защиты.
Не отмечайте окончание всего класса.
Существуют веские причины для разрешения расширения неизменяемого класса, как указано в некоторых других ответах, поэтому маркировка класса как окончательного не всегда является хорошей идеей.
Лучше пометить ваши свойства как частные и окончательные, и если вы хотите защитить «контракт», пометьте получателей как окончательные.
Таким образом вы можете разрешить расширение класса (да, возможно, даже изменяемым классом), однако неизменные аспекты вашего класса защищены. Свойства являются частными и недоступны, методы получения этих свойств являются окончательными и не могут быть переопределены.
Любой другой код, который использует экземпляр вашего неизменяемого класса, сможет полагаться на неизменяемые аспекты вашего класса, даже если переданный подкласс является изменяемым в других аспектах. Конечно, поскольку он принимает экземпляр вашего класса, он даже не узнает об этих других аспектах.
Если он не окончательный, то любой может расширить класс и делать все, что ему нравится, например, предоставлять сеттеры, затенять ваши частные переменные и в основном делать его изменяемым.
Это ограничивает другие классы, расширяющие ваш класс.
final класс не может быть расширен другими классами.
Если класс расширяет класс, который вы хотите сделать неизменным, он может изменить состояние класса из-за принципов наследования.
Сразу уточните «может измениться». Подкласс может переопределить поведение суперкласса, например использование переопределения метода (например, templatetypedef / ответ Теда Хопа)
final
, я не понимаю, как этот ответ помогает.
Если вы не сделаете его окончательным, я могу расширить его и сделать неизменяемым.
public class Immutable {
privat final int val;
public Immutable(int val) {
this.val = val;
}
public int getVal() {
return val;
}
}
public class FakeImmutable extends Immutable {
privat int val2;
public FakeImmutable(int val) {
super(val);
}
public int getVal() {
return val2;
}
public void setVal(int val2) {
this.val2 = val2;
}
}
Теперь я могу передать FakeImmutable любому классу, который ожидает Immutable, и он не будет вести себя как ожидаемый контракт.
Для создания неизменяемого класса не обязательно отмечать класс как окончательный.
Позвольте мне взять один из таких примеров из классов java. Сам класс BigInteger является неизменным, но не окончательным.
На самом деле неизменяемость - это концепция, согласно которой созданный объект не может быть изменен.
Давайте подумаем с точки зрения JVM, с точки зрения JVM все потоки должны использовать одну и ту же копию объекта, и он полностью создается до того, как какой-либо поток обращается к нему, и состояние объекта не изменяется после его создания.
Неизменяемость означает, что невозможно изменить состояние объекта после его создания, и это достигается тремя правилами большого пальца, которые заставляют компилятор распознавать, что класс неизменяем, и они следующие:
Для получения дополнительной информации см. URL-адрес ниже
http://javaunturnedtopics.blogspot.in/2016/07/can-we-create-immutable-class-without.html
Допустим, у вас есть следующий класс:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class PaymentImmutable {
private final Long id;
private final List<String> details;
private final Date paymentDate;
private final String notes;
public PaymentImmutable (Long id, List<String> details, Date paymentDate, String notes) {
this.id = id;
this.notes = notes;
this.paymentDate = paymentDate == null ? null : new Date(paymentDate.getTime());
if (details != null) {
this.details = new ArrayList<String>();
for(String d : details) {
this.details.add(d);
}
} else {
this.details = null;
}
}
public Long getId() {
return this.id;
}
public List<String> getDetails() {
if(this.details != null) {
List<String> detailsForOutside = new ArrayList<String>();
for(String d: this.details) {
detailsForOutside.add(d);
}
return detailsForOutside;
} else {
return null;
}
}
}
Затем вы расширяете его и нарушаете его неизменность.
public class PaymentChild extends PaymentImmutable {
private List<String> temp;
public PaymentChild(Long id, List<String> details, Date paymentDate, String notes) {
super(id, details, paymentDate, notes);
this.temp = details;
}
@Override
public List<String> getDetails() {
return temp;
}
}
Вот тестируем:
public class Demo {
public static void main(String[] args) {
List<String> details = new ArrayList<>();
details.add("a");
details.add("b");
PaymentImmutable immutableParent = new PaymentImmutable(1L, details, new Date(), "notes");
PaymentImmutable notImmutableChild = new PaymentChild(1L, details, new Date(), "notes");
details.add("some value");
System.out.println(immutableParent.getDetails());
System.out.println(notImmutableChild.getDetails());
}
}
Результат на выходе будет:
[a, b]
[a, b, some value]
Как видите, в то время как исходный класс сохраняет неизменность, дочерние классы могут быть изменяемыми. Следовательно, в вашем дизайне вы не можете быть уверены, что объект, который вы используете, неизменяемый, если вы не сделаете свой класс final.
Допустим, следующего класса не было final
:
public class Foo {
private int mThing;
public Foo(int thing) {
mThing = thing;
}
public int doSomething() { /* doesn't change mThing */ }
}
По-видимому, он неизменен, потому что даже подклассы не могут быть изменены mThing
. Однако подкласс может быть изменяемым:
public class Bar extends Foo {
private int mValue;
public Bar(int thing, int value) {
super(thing);
mValue = value;
}
public int getValue() { return mValue; }
public void setValue(int value) { mValue = value; }
}
Теперь объект, который Foo
можно присвоить переменной типа , больше не может быть изменен. Это может вызвать проблемы с такими вещами, как хеширование, равенство, параллелизм и т. Д.
Дизайн сам по себе не имеет ценности. Дизайн всегда используется для достижения цели. Какая здесь цель? Хотим ли мы уменьшить количество сюрпризов в коде? Мы хотим предотвратить ошибки? Мы слепо следуем правилам?
Кроме того, дизайн всегда имеет свою цену. Каждый дизайн, заслуживающий такого названия, означает, что у вас конфликт целей .
Имея это в виду, вам необходимо найти ответы на следующие вопросы:
Допустим, в вашей команде много младших разработчиков. Они будут отчаянно пробовать любую глупость только потому, что пока не знают хороших решений своих проблем. Создание финального класса могло бы предотвратить ошибки (хорошо), но также могло бы заставить их придумать «умные» решения, такие как копирование всех этих классов в изменяемые везде в коде.
С другой стороны, будет очень сложно создать класс final
после того, как он будет использоваться повсюду, но легко создать final
класс не final
позднее, если вы обнаружите, что вам нужно его расширить.
Если вы правильно используете интерфейсы, вы можете избежать проблемы «Мне нужно сделать эту изменяемую», всегда используя интерфейс, а затем добавляя изменяемую реализацию, когда в этом возникает необходимость.
Вывод: для этого ответа нет "лучшего" решения. Это зависит от того, какую цену вы готовы заплатить.
Значение по умолчанию equals () такое же, как ссылочное равенство. Для неизменяемых типов данных это почти всегда неверно. Поэтому вам нужно переопределить метод equals (), заменив его собственной реализацией. ссылка
java.math.BigInteger
class является примером, его значения неизменны, но не окончательны.