Я прочитав, що, щоб зробити клас незмінним у Java, нам слід зробити наступне:
- Не надайте жодних установок
- Позначити всі поля як приватні
- Зробіть клас остаточним
Чому потрібен крок 3? Чому я повинен позначити клас final
?
Я прочитав, що, щоб зробити клас незмінним у Java, нам слід зробити наступне:
Чому потрібен крок 3? Чому я повинен позначити клас final
?
BigInteger
, ви не знаєте, незмінний він чи ні. Це зіпсований дизайн. / java.io.File
є більш цікавим прикладом.
File
цікавий тим, що йому, ймовірно, можна довіряти, що він незмінний, а отже, призводить до атак TOCTOU (Time Of Check / Time Of Use). Довірений код перевіряє, чи 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 (і загалом OOP) думають про контракти, що стосуються що може зробити клас , а не те, що він не може зробити.
Незмінність також впливає не лише на один метод - вона впливає на весь екземпляр, - але це насправді не сильно відрізняється від способу equals
та роботи hashCode
в Java. Ці два методи мають конкретний контракт, викладений у Object
. Цей контракт дуже ретельно викладає речі, які ці методи не можуть зробити. Цей контракт є більш конкретним у підкласах. Це дуже легко замінити equals
або hashCode
таким чином, що порушує договір. Насправді, якщо ви заміните лише один із цих двох методів без іншого, швидше за все, ви порушите контракт. Так повинно equals
і hashCode
були оголошені final
в , Object
щоб уникнути цього? Думаю, більшість заперечує, що вони не повинні. Так само не потрібно робити незмінні класиfinal
.
Тим не менш, більшість ваших занять, незмінними чи ні, мабуть, повинні бути final
. Див. Ефективний другий випуск Java, пункт 17: "Дизайн та документ для успадкування або заборона".
Тож правильною версією вашого кроку 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
не може бути остаточним, не означає, що я можу терпіти те саме для незмінного класу, якщо у мене немає вагомих причин для цього, і в цьому випадку я можу використовувати документацію або тестування як захисну лінію.
Не відзначайте весь фінал класу.
Існують вагомі причини, що дозволяють продовжувати незмінний клас, як зазначено в деяких інших відповідях, тому позначати клас як остаточний не завжди є гарною ідеєю.
Краще позначити свою власність приватною та остаточною, і якщо ви хочете захистити "контракт", позначте свої майстри як остаточні.
Таким чином ви можете дозволити розширення класу (так, можливо, навіть змінним класом), проте незмінні аспекти вашого класу захищені. Властивості є приватними та не можуть бути доступними, геттери для цих властивостей остаточні і не можуть бути замінені.
Будь-який інший код, який використовує екземпляр вашого незмінного класу, зможе покладатися на незмінні аспекти вашого класу, навіть якщо переданий ним підклас є змінним в інших аспектах. Звичайно, оскільки він бере екземпляр вашого класу, він навіть не знав би про ці інші аспекти.
Якщо він не остаточний, тоді кожен може розширити клас і робити все, що йому подобається, наприклад, надавати сетери, затінювати ваші приватні змінні і, в основному, робити його змінним.
Це обмежує інші класи, що розширюють ваш клас.
фінальний клас не можна продовжувати іншими класами.
Якщо клас розширить клас, який ви хочете зробити незмінним, він може змінити стан класу через принципи успадкування.
Просто поясніть "це може змінитися". Підклас може замінити поведінку суперкласу, як використання методу перевизначення (як відповідь templatetypedef / Ted Hop)
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
:
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 є прикладом, його значення незмінні, але він не остаточний.