Зачем объявлять неизменный класс final в Java?
Я прочитал это, чтобы сделать класс неизменным в Java, мы должны сделать следующее,
- Не предоставляйте сеттеров
- Отметить все поля как частные
- Сделать финал класса
Почему необходим шаг 3? Почему я должен отметить класс final
?
10 ответов
Если вы не отметили класс 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
, См. Пункт 17 " Эффективное Java, второе издание": "Дизайн и документация для наследования или иного запрета".
Таким образом, правильной версией вашего шага 3 будет: "Сделайте класс окончательным или, при разработке для подклассов, четко документируйте, что все подклассы должны оставаться неизменными".
Не отмечайте весь финал класса.
Существуют веские причины для расширения неизменяемого класса, как указано в некоторых других ответах, поэтому пометить класс как окончательный не всегда хорошая идея.
Лучше пометить вашу собственность как частную и окончательную, а если вы хотите защитить "контракт", пометьте своих получателей как окончательные.
Таким образом, вы можете разрешить расширение класса (да, возможно, даже изменяемым классом), однако неизменные аспекты вашего класса защищены. Свойства являются частными и недоступны, получатели для этих свойств являются окончательными и не могут быть переопределены.
Любой другой код, который использует экземпляр вашего неизменяемого класса, сможет полагаться на неизменяемые аспекты вашего класса, даже если передаваемый им подкласс является изменчивым в других аспектах. Конечно, поскольку он занимает экземпляр вашего класса, он даже не узнает об этих других аспектах.
Если вы не сделаете его окончательным, я могу расширить его и сделать его не изменяемым.
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, и он не будет вести себя как ожидаемый контракт.
Если он не окончательный, тогда любой может расширить класс и делать все, что ему нравится, например, предоставлять сеттеры, скрывать ваши личные переменные и в основном делать его изменчивым.
Это ограничивает другие классы, расширяющие ваш класс.
последний класс не может быть расширен другими классами.
Если класс расширяет класс, который вы хотите сделать неизменным, он может изменить состояние класса из-за принципов наследования.
Просто уточните "это может измениться". Подкласс может переопределять поведение суперкласса, например, используя переопределение методов (например, templatetypedef/ Ted Hop answer)
Для создания неизменяемого класса не обязательно отмечать класс как окончательный.
Позвольте мне взять один из таких примеров из классов 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
больше не гарантируется быть неизменным. Это может вызвать проблемы с такими вещами, как хеширование, равенство, параллелизм и т. Д.
Значение по умолчанию equals() совпадает со ссылочным равенством. Для неизменяемых типов данных это почти всегда неправильно. Поэтому вам нужно переопределить метод equals(), заменив его собственной реализацией. ссылка на сайт
Дизайн сам по себе не имеет значения. Дизайн всегда используется для достижения цели. Какова цель здесь? Мы хотим уменьшить количество сюрпризов в коде? Мы хотим предотвратить ошибки? Мы слепо следуем правилам?
Кроме того, дизайн всегда стоит денег. Каждый дизайн, который заслуживает названия, означает, что у вас есть конфликт целей.
Имея это в виду, вам нужно найти ответы на следующие вопросы:
- Сколько очевидных ошибок это предотвратит?
- Сколько тонких ошибок это предотвратит?
- Как часто это сделает другой код более сложным (= более подверженным ошибкам)?
- Это делает тестирование легче или сложнее?
- Насколько хороши разработчики в вашем проекте? Сколько руководства с кувалдой им нужно?
Скажем, в вашей команде много младших разработчиков. Они будут отчаянно пробовать любую глупость только потому, что пока не знают хороших решений своих проблем. Завершение работы над классом может предотвратить ошибки (хорошо), но также может заставить их придумать "умные" решения, такие как копирование всех этих классов в изменяемые повсюду в коде.
С другой стороны, будет очень сложно сделать урок final
после того, как он используется везде, но это легко сделать final
класс неfinal
позже, если вы узнаете, что вам нужно продлить его.
Если вы правильно используете интерфейсы, вы можете избежать проблемы "Мне нужно сделать эту изменяемую", всегда используя интерфейс, а затем добавляя изменяемую реализацию, когда возникает необходимость.
Вывод: "лучшего" решения для этого ответа не существует. Это зависит от того, какую цену вы готовы и какую вы должны заплатить.