Чому можна оголосити незмінний клас фінальним у Java?


83

Я прочитав, що, щоб зробити клас незмінним у Java, нам слід зробити наступне:

  1. Не надайте жодних установок
  2. Позначити всі поля як приватні
  3. Зробіть клас остаточним

Чому потрібен крок 3? Чому я повинен позначити клас final?


java.math.BigIntegerclass є прикладом, його значення незмінні, але він не остаточний.
Нандкумар Текале

@Nandkumar Якщо у вас є BigInteger, ви не знаєте, незмінний він чи ні. Це зіпсований дизайн. / java.io.Fileє більш цікавим прикладом.
Том Хоутін - таклін

1
Не дозволяти підкласам перевизначати методи - найпростіший спосіб зробити це - оголосити клас як остаточний. Більш складний підхід полягає в тому, щоб зробити конструктор приватним і побудувати екземпляри заводськими методами - від - docs.oracle.com/javase/tutorial/essential/concurrency/…
Рауль,

1
@mkobit Fileцікавий тим, що йому, ймовірно, можна довіряти, що він незмінний, а отже, призводить до атак TOCTOU (Time Of Check / Time Of Use). Довірений код перевіряє, чи Fileмає прийнятний шлях, а потім використовує шлях. Підклас Fileможе змінювати значення між перевіркою та використанням.
Том Хоутін - таклін

1
Make the class finalабо Переконайтесь, що всі підкласи незмінні
О. Бадр,

Відповіді:


138

Якщо ви не позначите клас 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запобігає цьому.

Сподіваюся, це допомагає!


13
Якщо я роблю Mutable m = new Mutable (4); m.setValue (5); Тут я граюся з об’єктом класу Mutable, а не з об’єктом незмінюваного класу, тому мене все ще бентежить, чому клас Immutable не є незмінним
Ананд

18
@ anand- Уявіть, що у вас є функція, яка приймає Immutableаргумент. Я можу передати Mutableоб'єкт цій функції, оскільки Mutable extends Immutable. Усередині цієї функції, хоча ви думаєте, що ваш об’єкт незмінний, я міг би мати вторинний потік, який переходить і змінює значення під час запуску функції. Я також міг би дати вам Mutableоб'єкт, який функція зберігає, а потім змінити його значення зовні. Іншими словами, якщо ваша функція вважає значення незмінним, воно може легко зламатися, оскільки я міг би дати вам змінний об'єкт і змінити його пізніше. Чи має це сенс?
templatetypedef

6
@ anand- Метод, в який ви передаєте Mutableоб'єкт, не змінить об'єкт. Проблема полягає в тому, що цей метод може припустити, що об’єкт незмінний, коли він насправді не є. Наприклад, метод може припустити, що, оскільки він вважає об'єкт незмінним, його можна використовувати як ключ у HashMap. Потім я міг зламати цю функцію, передавши a Mutable, чекаючи, поки він збереже об’єкт як ключ, а потім змінивши Mutableоб’єкт. Тепер пошук у цьому HashMapне вдасться, оскільки я змінив ключ, пов’язаний з об’єктом. Чи має це сенс?
templatetypedef

3
@ templatetypedef - Так, я зрозумів це зараз ... треба сказати, це було приголомшливе пояснення ... хоч трохи часу, щоб зрозуміти це
Ананд

1
@ SAM- Це правильно. Однак це насправді не має значення, оскільки замінені функції більше ніколи не посилаються на цю змінну.
templatetypedef

47

Попри те , що багато людей вважають, що робить незмінний клас 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 буде: "Зробіть клас остаточним або, при розробці для підкласу, чітко задокументуйте, що всі підкласи повинні залишатися незмінними".


3
Варто зазначити, що Java "очікує", але не забезпечує, що з урахуванням двох об'єктів Xі Y, значення X.equals(Y)буде незмінним (до тих пір, поки Xі Yпродовжуватиме посилатися на ті самі об'єкти). Він має подібні очікування щодо хеш-кодів. Зрозуміло, що ніхто не повинен очікувати, що компілятор забезпечить незмінність відношень еквівалентності та хеш-кодів (оскільки це просто не може). Я не бачу причини, за якою люди повинні очікувати, що це буде застосовано щодо інших аспектів типу.
supercat

1
Крім того, є багато випадків, коли може бути корисно мати абстрактний тип, контракт якого визначає незмінність. Наприклад, може бути абстрактний тип незмінюваної матриці, який, отримуючи пару координат, повертає a double. Можна отримати a, GeneralImmutableMatrixякий використовує масив як резервне сховище, але можна також мати, наприклад, ImmutableDiagonalMatrixякий просто зберігає масив елементів по діагоналі (читання елемента X, Y дасть Arr [X], якщо X == y, інакше нуль) .
supercat

2
Мені це пояснення найбільше подобається. Завжди створення незмінного класу finalобмежує його корисність, особливо коли ви розробляєте API, призначений для розширення. В інтересах безпеки потоків має сенс зробити ваш клас якомога незміннішим, але тримати його розширюваним. Ви можете зробити поля protected finalзамість private final. Потім чітко встановіть контракт (у документації) про підкласи, що дотримуються гарантій незмінності та безпеки потоків.
curioustechizen

4
+1 - Я з вами до кінця цитати Блоха. Ми не повинні бути такими параноїками. 99% кодування є кооперативними. Нічого страшного, якщо комусь дійсно потрібно щось перевизначити, нехай. 99% з нас не пишуть деяких основних API, таких як String або Collection. Багато порад Блоха, на жаль, базуються на таких випадках використання. Вони не дуже корисні для більшості програмістів, особливо нових.
ZhongYu

Я вважаю за краще зробити незмінний клас final. Те, що equalsі hashcodeне може бути остаточним, не означає, що я можу терпіти те саме для незмінного класу, якщо у мене немає вагомих причин для цього, і в цьому випадку я можу використовувати документацію або тестування як захисну лінію.
О. Бадр,

21

Не відзначайте весь фінал класу.

Існують вагомі причини, що дозволяють продовжувати незмінний клас, як зазначено в деяких інших відповідях, тому позначати клас як остаточний не завжди є гарною ідеєю.

Краще позначити свою власність приватною та остаточною, і якщо ви хочете захистити "контракт", позначте свої майстри як остаточні.

Таким чином ви можете дозволити розширення класу (так, можливо, навіть змінним класом), проте незмінні аспекти вашого класу захищені. Властивості є приватними та не можуть бути доступними, геттери для цих властивостей остаточні і не можуть бути замінені.

Будь-який інший код, який використовує екземпляр вашого незмінного класу, зможе покладатися на незмінні аспекти вашого класу, навіть якщо переданий ним підклас є змінним в інших аспектах. Звичайно, оскільки він бере екземпляр вашого класу, він навіть не знав би про ці інші аспекти.


1
Я волів би інкапсулювати всі незмінні аспекти в окремий клас і використовувати його за допомогою композиції. Було б простіше міркувати, ніж змішувати мінливий і незмінний стан в одній одиниці коду.
toniedzwiedz

1
Повністю згоден. Композиція була б дуже хорошим способом досягнення цього, за умови, що ваш мінливий клас містить ваш незмінний клас. Якщо у вашій структурі навпаки, це було б непоганим частковим рішенням.
Джастін Омс,

5

Якщо він не остаточний, тоді кожен може розширити клас і робити все, що йому подобається, наприклад, надавати сетери, затінювати ваші приватні змінні і, в основному, робити його змінним.


Хоча ви маєте рацію, незрозуміло, чому той факт, що ви можете додати змінну поведінку, обов’язково порушить ситуацію, якщо поля базового класу є приватними та остаточними. Реальна небезпека полягає в тому, що підклас може перетворити раніше незмінний об'єкт на змінний об'єкт, замінивши поведінку.
templatetypedef

4

Це обмежує інші класи, що розширюють ваш клас.

фінальний клас не можна продовжувати іншими класами.

Якщо клас розширить клас, який ви хочете зробити незмінним, він може змінити стан класу через принципи успадкування.

Просто поясніть "це може змінитися". Підклас може замінити поведінку суперкласу, як використання методу перевизначення (як відповідь templatetypedef / Ted Hop)


2
Це правда, але навіщо тут це потрібно?
templatetypedef

@templatetypedef: Ви занадто швидкі. Я редагую свою відповідь з підтримкою.
kosa

4
Зараз ваша відповідь настільки розмита, що я не думаю, що вона взагалі відповідає на питання. Що ви маєте на увазі під "це може змінити стан класу через принципи успадкування?" Якщо ви вже не знаєте, чому вам слід це позначити final, я не бачу, як ця відповідь допомагає.
templatetypedef

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, і він не буде поводитися як очікуваний контракт.


2
Я думаю, це майже ідентично моїй відповіді.
templatetypedef

Так, перевірено на половині публікації, нових відповідей немає. А потім, коли їх розмістили, там були ваші, за 1 хвилину до мене. Принаймні ми не використовували однакові імена для всього.
Roger Lindsjö

Одне виправлення: у класі FakeImmutable ім'я конструктора має бути FakeImmutable NOT Immutable
Sunny Gupta

2

Для створення незмінного класу не обов'язково позначати клас як остаточний.

Дозвольте взяти один із таких прикладів із класів Java, сам клас "BigInteger" є незмінним, але не остаточним.

Насправді незмінність - це концепція, згідно з якою об'єкт, створений тоді, не може бути змінений.

Давайте подумаємо з точки зору JVM, з точки зору JVM всі потоки повинні мати однакову копію об'єкта, і він повністю побудований до того, як будь-який потік отримає до нього доступ, і стан об'єкта не змінюється після його побудови.

Незмінність означає, що жодним чином не можна змінити стан об'єкта після його створення, і це досягається трьома правилами великого пальця, що змушує компілятор визнати, що клас незмінний, і вони такі:

  • всі не приватні поля повинні бути остаточними
  • переконайтеся, що в класі немає методу, який може змінювати поля об’єкта прямо чи опосередковано
  • будь-яке посилання на об'єкт, визначене в класі, не може бути змінено поза класом

Для отримання додаткової інформації зверніться до URL-адреси нижче

http://javaunturnedtopics.blogspot.in/2016/07/can-we-create-immutable-class-without.html


1

Скажімо, у вас є такий клас:

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]

Як бачите, поки оригінальний клас зберігає свою незмінність, дочірні класи можуть змінюватися. Отже, у своєму дизайні ви не можете бути впевнені, що об'єкт, який ви використовуєте, є незмінним, якщо ви не зробите свій клас остаточним.


0

Припустимо, наступний клас не був 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можна присвоїти змінній типу , вже не гарантовано змінюється. Це може спричинити проблеми з такими речами, як хешування, рівність, паралельність тощо.


0

Дизайн сам по собі не має ніякої цінності. Для досягнення мети завжди використовується дизайн. Яка мета тут? Ми хочемо зменшити кількість сюрпризів у коді? Ми хочемо запобігти помилкам? Ми сліпо дотримуємось правил?

Крім того, дизайн завжди має свої витрати. Кожен дизайн, який заслуговує на назву, означає, що у вас є конфлікт цілей .

Маючи це на увазі, вам потрібно знайти відповіді на ці запитання:

  1. Скільки очевидних помилок це запобіжить?
  2. Скільки тонких помилок це запобіжить?
  3. Як часто це робить інший код більш складним (= більше схильний до помилок)?
  4. Це робить тестування простішим чи складнішим?
  5. Наскільки хороші розробники у вашому проекті? Скільки їм потрібне керівництво кувалдою?

Скажімо, у вашій команді багато молодших розробників. Вони відчайдушно спробують будь-яку дурість лише тому, що поки не знають хороших рішень для своїх проблем. Виконання класу остаточним може запобігти помилкам (хорошим), але також може змусити їх придумати "розумні" рішення, такі як копіювання всіх цих класів у змінні, скрізь у коді.

З іншого боку, скласти клас буде дуже важко final після його використання скрізь, але легко зробити finalклас не finalпізніше, якщо з’ясуєте, що вам потрібно його продовжити.

Якщо ви правильно використовуєте інтерфейси, ви можете уникнути проблеми "Мені потрібно зробити це змінним", завжди використовуючи інтерфейс, а потім додаючи змінну реалізацію, коли виникає потреба.

Висновок: "Найкращого" рішення для цієї відповіді не існує. Це залежить від того, яку ціну ви готові і яку вам доведеться заплатити.


0

Значення за замовчуванням equals () таке саме, як і посилальна рівність. Для незмінних типів даних це майже завжди неправильно. Отже, вам доведеться перевизначити метод equals (), замінивши його власною реалізацією. посилання

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.