Я читал, что для того, чтобы сделать класс неизменным на Java, мы должны сделать следующее:
- Не предоставлять никаких сеттеров
- Отметить все поля как частные
- Сделать окончательный класс
Почему требуется шаг 3? Почему я должен отмечать класс final
?
Я читал, что для того, чтобы сделать класс неизменным на Java, мы должны сделать следующее:
Почему требуется шаг 3? Почему я должен отмечать класс 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
предотвращает это.
Надеюсь, это поможет!
Вопреки тому, что многие считают, создание обязательного класса final
не требуется.
Стандартный аргумент для создания неизменяемых классов final
заключается в том, что если вы этого не сделаете, то подклассы могут добавлять изменчивость, тем самым нарушая контракт суперкласса. Клиенты класса возьмут на себя неизменность, но будут удивлены, когда что-то выйдет из-под них.
Если вы примете этот аргумент в свою логическую крайность, тогда все методы должны быть сделаны final
, так как в противном случае подкласс может переопределить метод таким образом, который не соответствует контракту его суперкласса. Интересно, что большинство программистов Java считают это смешным, но как-то хорошо с идеей, что неизменяемые классы должны быть final
. Я подозреваю, что это связано с тем, что Java-программисты вообще не совсем поняли понятие неизменности и, возможно, какое-то нечеткое мышление, связанное с множественными значениями ключевого слова final
в Java.
Соответствие контракту вашего суперкласса не является тем, что может или должно всегда выполняться компилятором. Компилятор может обеспечить выполнение определенных аспектов вашего контракта (например: минимальный набор методов и их типа), но существует множество частей типичных контрактов, которые не могут быть реализованы компилятором.
Неизменяемость является частью договора класса. Это немного отличается от некоторых вещей, к которым люди больше привыкли, потому что в нем говорится, что класс (и все подклассы) не могут сделать, хотя я думаю, что большинство программистов на Java (и вообще ООП) склонны думать о контрактах как относящихся к что класс может сделать, а не то, что он не может сделать.
Неизменяемость также влияет не только на один метод; это влияет на весь экземпляр; но это не сильно отличается от способа equals
и hashCode
в работе Java. Эти два метода имеют конкретный контракт, изложенный в Object
. В этом контракте очень тщательно излагаются те вещи, которые эти методы не могут сделать. Этот контракт более определен в подклассах. Очень легко переопределить equals
или hashCode
таким образом, который нарушает контракт. Фактически, если вы переопределите только один из этих двух методов без другого, есть вероятность, что вы нарушите контракт. Значит, equals
и hashCode
были объявлены final
в Object
, чтобы избежать этого? Я думаю, что большинство из них утверждают, что они не должны. Точно так же нет необходимости создавать неизменяемые классы final
.
Тем не менее, большинство ваших классов, неизменяемых или нет, вероятно, должно быть final
. См. "Эффективное Java Second Edition", пункт 17: "Дизайн и документ для наследования или запрет".
Таким образом, правильная версия вашего шага 3 будет: "Сделать окончательный класс или, при разработке для подкласса, четко документировать, что все подклассы должны оставаться неизменными".
Не отмечайте окончание всего класса.
Имеются веские причины, позволяющие продлить неизменяемый класс, как указано в некоторых других ответах, поэтому обозначение класса как окончательного не всегда является хорошей идеей.
Лучше пометить ваши свойства как частными, так и окончательными, и если вы хотите защитить "контракт", отметьте ваши получатели окончательными.
Таким образом, вы можете разрешить расширение класса (да, возможно, даже с помощью изменяемого класса), однако неизменные аспекты вашего класса защищены. Свойства являются частными и не могут быть доступны, getters для этих свойств являются окончательными и не могут быть переопределены.
Любой другой код, который использует экземпляр вашего неизменяемого класса, сможет полагаться на непреложные аспекты вашего класса, даже если подчиненный класс, который он передал, изменен в других аспектах. Конечно, поскольку он принимает экземпляр вашего класса, он даже не знает об этих других аспектах.
Если это не окончательно, кто-то может расширить класс и делать все, что ему нравится, например, предоставлять сеттеры, скрывать ваши частные переменные и в основном сделать его изменчивым.
Это ограничивает другие классы, расширяющие ваш класс.
окончательный класс не может быть расширен другими классами.
Если класс расширяет класс, который вы хотите сделать неизменным, он может изменить состояние класса из-за принципов наследования.
Просто уточните, "это может измениться". Подкласс может переопределять поведение суперкласса, как использование переопределения метода (например, templatetypedef/Ted Hop)
Если вы не сделаете это окончательным, я могу расширить его и сделать его не изменяемым.
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, и он не будет вести себя как ожидаемый контракт.
Для создания неизменяемого класса необязательно отмечать класс как final.
Позвольте мне взять один из таких примеров из классов java сам класс "BigInteger" неизменен, но не является окончательным.
На самом деле неизменяемость - это концепция, согласно которой объект создан, а затем не может быть изменен.
Предположим, что с точки зрения JVM, с точки зрения JVM, все потоки должны совместно использовать одну и ту же копию объекта, и она полностью построена до того, как любой поток обратится к ней, и состояние объекта не изменится после его построения.
Неизменяемость означает, что невозможно изменить состояние объекта после его создания, и это достигается тремя правилами большого пальца, которые заставляют компилятор распознавать этот класс неизменным, и они следующие: -
Для получения дополнительной информации обратитесь к приведенному ниже URL
http://javaunturnedtopics.blogspot.in/2016/07/can-we-create-immutable-class-without.html
Предположим, что следующий класс не был 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
, более не гарантируется. Это может вызвать проблемы с такими вещами, как хэширование, равенство, concurrency и т.д.
Дизайн сам по себе не имеет значения. Дизайн всегда используется для достижения цели. Какова цель здесь? Хотим ли мы уменьшить количество сюрпризов в коде? Мы хотим предотвратить ошибки? Мы слепо следуем правилам?
Кроме того, дизайн всегда стоит дорого. Каждый дизайн, который заслуживает имени, означает, что у вас есть конфликт целей.
С учетом этого вам нужно найти ответы на следующие вопросы:
Скажем, у вас в вашей команде много младших разработчиков. Они будут отчаянно пробовать любую глупую вещь только потому, что они не знают хороших решений для своих проблем. Создание финала класса может предотвратить ошибки (хорошие), но также может заставить их придумать "умные" решения, такие как копирование всех этих классов в изменчивые в коде.
С другой стороны, будет очень сложно сделать класс final
после его использования повсюду, но легко сделать класс final
не final
позже, если вы узнаете, что вам нужно его расширить.
Если вы правильно используете интерфейсы, вы можете избежать проблемы "Мне нужно сделать эту изменчивую", всегда используя интерфейс, а затем позже добавляя изменчивую реализацию, когда возникнет такая необходимость.
Заключение: для этого ответа нет "лучшего" решения. Это зависит от того, какую цену вы желаете и которую вы должны заплатить.