Зачем объявлять неизменяемый класс final в Java?

83

Я читал, что для того, чтобы сделать класс неизменяемым в Java, мы должны сделать следующее:

  1. Не предоставлять никаких сеттеров
  2. Отметить все поля как частные
  3. Сделать класс финальным

Почему требуется шаг 3? Почему я должен отмечать класс final?

Ананд
источник
java.math.BigIntegerclass является примером, его значения неизменны, но не окончательны.
Нандкумар Текале
@Nandkumar Если у вас есть BigInteger, вы не знаете, является ли он неизменным или нет. Это испорченный дизайн. / java.io.File- более интересный пример.
Том Хотин - tackline
1
Не позволяйте подклассам переопределять методы. Самый простой способ сделать это - объявить класс окончательным. Более сложный подход - сделать конструктор закрытым и создавать экземпляры в фабричных методах - from - docs.oracle.com/javase/tutorial/essential/concurrency/…
Raúl
1
@mkobit Fileинтересен тем, что его можно доверять как неизменяемый, что приводит к атакам TOCTOU (время проверки / время использования). Надежный код проверяет Fileналичие допустимого пути, а затем использует его. Подкласс Fileможет изменять значение между проверкой и использованием.
Том Хотин - tackline
1
Make the class finalили Убедитесь, что все подклассы неизменяемы
O.Badr

Ответы:

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предотвращает это.

Надеюсь это поможет!

templatetypedef
источник
13
Если я сделаю Mutable m = new Mutable (4); m.setValue (5); Здесь я играю с объектом класса Mutable, а не с объектом класса Immutable, поэтому я до сих пор не понимаю, почему класс 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 (и вообще ООП) склонны думать о контрактах как о связанных с что может делать класс , а не то, что он не может .

Неизменность также влияет больше , чем просто один метод - это влияет на весь экземпляр - но это на самом деле не сильно отличается от пути , equalsи hashCodeв Java работы. У этих двух методов есть конкретный контракт Object. В этом контракте очень тщательно прописаны вещи, которые эти методы сделать не могут . Этот контракт конкретизируется в подклассах. Это очень легко отменить equalsили hashCodeнарушить договор. Фактически, если вы переопределите только один из этих двух методов без другого, скорее всего, вы нарушите контракт. Так должно equalsи hashCodeбыли объявлены finalв , Objectчтобы избежать этого? Я думаю, что большинство будет утверждать, что не следует. Точно так же нет необходимости делать неизменяемые классыfinal.

Тем не менее, большинство ваших классов, неизменяемых или нет, вероятно, должны быть final. См. Пункт 17 действующего второго издания Java : «Разработайте и задокументируйте наследование, иначе запретите его».

Итак, правильная версия вашего шага 3 будет выглядеть так: «Сделайте класс окончательным или, при проектировании для создания подклассов, четко задокументируйте, что все подклассы должны оставаться неизменными».

Лоуренс Гонсалвес
источник
3
Стоит отметить, что Java «ожидает», но не требует, чтобы для двух объектов Xи Yзначение X.equals(Y)было неизменным (до тех пор, пока Xи Yпродолжают ссылаться на одни и те же объекты). Он имеет аналогичные ожидания относительно хэш-кодов. Понятно, что никто не должен ожидать, что компилятор обеспечит неизменность отношений эквивалентности и хэш-кодов (поскольку он просто не может). Я не вижу причин, по которым люди должны ожидать, что это будет применяться для других аспектов типа.
supercat
1
Кроме того, во многих случаях может быть полезно иметь абстрактный тип, контракт которого определяет неизменяемость. Например, у одного может быть абстрактный тип ImmutableMatrix, который, учитывая пару координат, возвращает 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 04
Я бы предпочел сделать неизменяемый класс final. Просто потому, что equalsи hashcodeне может быть окончательным, не означает, что я могу терпеть то же самое для неизменяемого класса, если у меня нет веской причины для этого, и в этом случае я могу использовать документацию или тестирование в качестве линии защиты.
O.Badr
21

Не отмечайте окончание всего класса.

Существуют веские причины для разрешения расширения неизменяемого класса, как указано в некоторых других ответах, поэтому маркировка класса как окончательного не всегда является хорошей идеей.

Лучше пометить ваши свойства как частные и окончательные, и если вы хотите защитить «контракт», пометьте получателей как окончательные.

Таким образом вы можете разрешить расширение класса (да, возможно, даже изменяемым классом), однако неизменные аспекты вашего класса защищены. Свойства являются частными и недоступны, методы получения этих свойств являются окончательными и не могут быть переопределены.

Любой другой код, который использует экземпляр вашего неизменяемого класса, сможет полагаться на неизменяемые аспекты вашего класса, даже если переданный подкласс является изменяемым в других аспектах. Конечно, поскольку он принимает экземпляр вашего класса, он даже не узнает об этих других аспектах.

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

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

digitaljoel
источник
Хотя вы правы, непонятно, почему тот факт, что вы можете добавить изменяемое поведение, обязательно сломает ситуацию, если все поля базового класса являются закрытыми и окончательными. Реальная опасность заключается в том, что подкласс может превратить ранее неизменный объект в изменяемый объект, переопределив поведение.
templatetypedef
4

Это ограничивает другие классы, расширяющие ваш класс.

final класс не может быть расширен другими классами.

Если класс расширяет класс, который вы хотите сделать неизменным, он может изменить состояние класса из-за принципов наследования.

Сразу уточните «может измениться». Подкласс может переопределить поведение суперкласса, например использование переопределения метода (например, templatetypedef / ответ Теда Хопа)

коса
источник
2
Это правда, но зачем это здесь нужно?
templatetypedef
@templatetypedef: вы слишком быстро. Я редактирую свой ответ с опорой.
kosa
4
Прямо сейчас ваш ответ настолько расплывчатый, что я не думаю, что он вообще отвечает на вопрос. Что вы имеете в виду под «он может изменить состояние класса из-за принципов наследования»? Если вы еще не знаете, почему вы должны отмечать это final, я не понимаю, как этот ответ помогает.
templatetypedef
4

Если вы не сделаете его окончательным, я могу расширить его и сделать неизменяемым.

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 минуту раньше меня. По крайней мере, мы не использовали одинаковые названия для всего.
Роджер Линдсьо,
Одно исправление: в классе FakeImmutable имя конструктора должно быть FakeImmutable NOT Immutable
Санни Гупта,
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]

Как видите, в то время как исходный класс сохраняет неизменность, дочерние классы могут быть изменяемыми. Следовательно, в вашем дизайне вы не можете быть уверены, что объект, который вы используете, неизменяемый, если вы не сделаете свой класс final.

валихон
источник
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 (), заменив его собственной реализацией. ссылка

Анита Б.С.
источник