Должен ли я генерировать исключение при попытке мутации неизменяемого класса? Если да, то какое исключение?

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

Изменяемый базовый класс: Vector3

public class Vector3 {
    public static final Vector3 Zero = new ImmutableVector3(0, 0, 0);

    private float x;
    private float y;
    private float z;

    public Vector3(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public void set(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

Неизменяемая версия: ImmutableVector3

public final class ImmutableVector3 extends Vector3 {
    public ImmutableVector3(float x, float y, float z) {
        super(x, y, z);
    }

    //Made immutable by overriding any set functionality.
    @Override
    public void set(float x, float y, float z) {
        throw new Exception("Immutable object."); //Should I even throw an exception?
    }
}

Мой вариант использования выглядит следующим образом:

public class MyObject {
    //position is set to a flyweight instance of a zero filled vector.
    //Since this is shared and static, I don't want it mutable.
    private Vector3 position = Vector3.Zero;
}

Допустим, разработчик в моей команде решил, что ему нужно манипулировать положением объекта, но в настоящее время он установлен на статический неизменяемый экземпляр общего вектора Vector3.Zero.

Было бы лучше, если бы он знал заранее, что ему нужно создать новый экземпляр изменяемого вектора, если вектор положения установлен на общий экземпляр Vector3.Zero:

if (position == Vector3.Zero)
    position = new Vector3(x, y, z);

Но, скажем, он не проверяет это первым. Я думаю, в этом случае было бы хорошо выбросить Exception, как показано выше, в методе ImmutableVector3.set(x,y,z). Я хотел использовать стандартное исключение Java, но не был уверен, какое из них будет наиболее подходящим, и я не на 100% уверен, что это лучший способ справиться с этим.

Предложения, которые я видел (этот вопрос) [Подходит ли IllegalStateException для неизменяемого объекта? кажется, предлагает следующее:

  • IllegalStateException - но это неизменяемый объект, поэтому имеет только одно состояние
  • UnsupportedOperationException — метод set() не поддерживается, поскольку он переопределяет базовый класс, так что это может быть хорошим выбором.
  • NoSuchElementException - я не уверен, что это был бы хороший выбор, если бы метод не считался element.

Кроме того, я даже не уверен, что должен создавать здесь исключение. Я мог бы просто не менять объект и не предупреждать разработчика, что он пытается изменить неизменяемый объект, но это тоже кажется хлопотным. Проблема с созданием исключения в ImmutableVector3.set(x,y,z) заключается в том, что set должно вызывать исключение как для ImmutableVector3, так и для Vector3. Так что теперь, хотя Vector3.set(x,y,z) никогда не вызовет исключение, его нужно поместить в блок try/catch.

Questions
  • Я упускаю из виду другой вариант, кроме создания исключений?
  • Если исключения — лучший способ, какое исключение выбрать?

person crush    schedule 29.08.2013    source источник
comment
Я бы пошел с UnsuppoertedOperationException. unmodifiable* декораторы из Collection — хороший прецедент.   -  person kiheru    schedule 29.08.2013
comment
@crush Вы решаете известную проблему с производительностью, делая Vector3D изменяемым? Если нет, просто сделайте его неизменным. Пусть люди создают новый экземпляр, когда им нужны новые значения. Известная проблема с производительностью = вы провели тестирование/анализ производительности, и это узкое место.   -  person Eric Stein    schedule 29.08.2013
comment
@EricStein значения Vector3 будут меняться каждую секунду или чаще для сотен тысяч объектов в моей области. Я придерживаюсь, возможно, ложного предположения, что создание нового объекта Vector3 каждый раз, когда мне нужно изменить положение объекта, в конечном итоге вызовет проблемы с паузами GC и/или будет медленнее, чем просто установка новой позиции на существующем объекте.   -  person crush    schedule 29.08.2013
comment
@crush Я не могу обещать, что это не будет проблемой, но Java довольно хорошо справляется с недолговечными объектами (я виню Dimension). Вы можете попробовать его и посмотреть, не взорвет ли он ваш код первым.   -  person Eric Stein    schedule 29.08.2013
comment
@EricStein На самом деле, я только что вспомнил этот факт, что еще одно ограничение здесь, связанное с неизменяемостью Vector3, заключается в том, что JPA не позволяет сериализовать неизменяемые объекты, и мы используем JPA для нашего уровня абстракции db/serialization.   -  person crush    schedule 29.08.2013
comment
@crush Это конец вечеринки. Вся эта шумиха ради одного случая, да? Нулевой вектор? Бросьте UnsupportedOperationException, как все остальные (правильно) сказали.   -  person Eric Stein    schedule 29.08.2013
comment
У меня есть целая горстка подобных статических, совместно используемых, легковесных экземпляров, но по сути да.   -  person crush    schedule 29.08.2013


Ответы (4)


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

Сам Java API рекомендует это в структуре коллекций:

«Деструктивные» методы, содержащиеся в этом интерфейсе, т. е. методы, которые изменяют коллекцию, над которой они работают, указываются для создания исключения UnsupportedOperationException, если эта коллекция не поддерживает операцию.

Альтернативой при создании полной иерархии классов является создание суперкласса Vector, который не поддается изменению и не имеет изменяющих методов, таких как set(), и двух подклассов ImmutableVector (что гарантирует неизменность) и MutableVector (что является модифицируемым). Это было бы лучше, потому что вы получаете безопасность во время компиляции (вы даже не можете пытаться изменить ImmutableVector), но в зависимости от ситуации может быть перепроектировано (и вы можете получить много классов).

Этот подход был выбран, например, для коллекций Scala, все они существуют в три вариации.

Во всех случаях, когда вы хотите изменить вектор, вы используете тип MutableVector. Во всех случаях, когда вы не заботитесь об изменении и просто хотите прочитать его, вы просто используете Vector. Во всех случаях, когда вы хотите гарантировать неизменность (например, экземпляр Vector передается в конструктор, и вы хотите гарантировать, что никто не изменит его позже), вы используете ImmutableVector (который обычно предоставляет метод копирования, поэтому вы просто вызываете ImmutableVector.copyOf(vector)) . Приведение вниз от Vector никогда не должно быть необходимым, вместо этого укажите правильный подтип, который вам нужен в качестве типа. Весь смысл этого решения (которое требует большего количества кода) заключается в том, чтобы быть типобезопасным и с проверками во время компиляции, поэтому приведение вниз уничтожает это преимущество. Однако есть исключения, например, метод ImmutableVector.copyOf() в качестве оптимизации обычно выглядит так:

public static ImmutableVector copyOf(Vector v) {
  if (v instanceof ImmutableVector) {
    return (ImmutableVector)v;
  } else {
    return new ImmutableVector(v);
  }
}

Это по-прежнему безопасно и дает вам еще одно преимущество неизменяемых типов, а именно предотвращение ненужного копирования.

библиотека Guava содержит большую коллекцию неизменяемая коллекция, которые следуют этому шаблону (хотя они по-прежнему расширяют изменяемые стандартные интерфейсы коллекций Java и, таким образом, не обеспечивают времени компиляции). безопасность). Вы должны прочитать их описание, чтобы узнать больше об неизменяемых типах.

Третья альтернатива состоит в том, чтобы просто иметь неизменяемый векторный класс и вообще не использовать изменяемые классы. Очень часто изменяемые классы, используемые для повышения производительности, являются преждевременной оптимизацией, поскольку Java очень хорошо справляется с обработкой большого количества небольших недолговечных временных объектов (из-за сборщика мусора на основе поколений размещение объектов чрезвычайно дешево, а очистка объектов в лучшем случае может быть даже бесплатной). Конечно, это не решение для очень больших объектов, таких как списки, но для трехмерного вектора это, безусловно, вариант (и, вероятно, лучшая комбинация, потому что она проста, безопасна и требует меньше кода, чем другие). Я не знаю примеров этой альтернативы в Java API, потому что в те времена, когда создавалось большинство частей API, виртуальная машина не была оптимизирована, и поэтому слишком много объектов могло быть проблемой производительности. Но это не должно помешать вам сделать это сегодня.

person Philipp Wendler    schedule 29.08.2013
comment
Будет ли суперкласс по-прежнему иметь метод set()? Без метода set() в базе или интерфейсе мне придется выполнить приведение к MutableVector, чтобы получить доступ к сеттеру, что также означает проверку, является ли position экземпляром MutableVector в первую очередь. - person crush; 29.08.2013
comment
Однако в этом случае я хочу изменить экземпляр только в том случае, если он сначала не установлен в неизменяемый экземпляр. Если это экземпляр Immutable, я хочу создать новый экземпляр Mutable и назначить его position. Таким образом, с тех пор мне всегда приходилось приводить к MutableVector, чтобы set вектор. - person crush; 29.08.2013
comment
Обнажая проблемы с производительностью, вы должны стремиться просто иметь неизменяемый класс. Это будет иметь дополнительное преимущество, позволяющее избежать базовой/неизменной/изменяемой иерархии каждый раз, когда вы попадаете в эту ситуацию. - person Daniel Kaplan; 29.08.2013
comment
В этом случае можно (и нужно) залить, хотя мне очень не нравится такой дизайн. - person Philipp Wendler; 29.08.2013
comment
Проблема с тем, чтобы сделать их все неизменяемыми, заключается в том, что мои руки связаны с JPA, не позволяющим сериализовать окончательные поля (что ошеломляет меня, но что угодно). - person crush; 29.08.2013
comment
Я не уверен, поможет ли это вам, но есть вопрос SO о JPA и неизменяемых типах: stackoverflow.com/questions/7222366/ - person Philipp Wendler; 29.08.2013
comment
Я хотел бы поблагодарить вас за всю информацию и исследования по этому вопросу. Я решил использовать то, что вы предложили вместе с предложением tieTYT выше. Я буду использовать интерфейс для Vector3, но поместить на него set(). Он вернет экземпляр Vector3. Этот экземпляр будет this в MutableVector3 и new MutableVector3(x,y,z) в ImmutableVector3. Это поможет облегчить модель наилегчайшего веса, которую я хочу получить. Однако я не могу решить, какой ответ принять, поскольку вы оба в равной степени внесли свой вклад в мое решение, по моей оценке. - person crush; 29.08.2013
comment
Я дал вам согласие, потому что ваш ответ самый подробный и охватывает множество подходов. - person crush; 29.08.2013

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

Неизменяемые классы не должны расширять изменяемые.

person SLaks    schedule 29.08.2013
comment
Вы говорите: извлеките интерфейс из изменяемого класса и вместо этого реализуйте этот интерфейс в неизменяемом классе? - person Sotirios Delimanolis; 29.08.2013
comment
Я не был слишком уверен в расширении изменяемого класса и попытке сделать его неизменным. Есть ли у вас какие-либо дополнительные предложения о том, как я могу справиться с вышеуказанными ситуациями? - person crush; 29.08.2013
comment
Я бы предпочел, чтобы Java поддерживала методы const, что помогло бы решить эту ерунду, но для этого уже слишком поздно. :-) - person Chris Jester-Young; 29.08.2013

Учитывая вашу ситуацию, я думаю, вам следует бросить UnsupportedOperationException. Вот почему:

package com.sandbox;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Sandbox {

    public static void main(String[] args) throws IOException {
        List<Object> objects = Collections.unmodifiableList(new ArrayList<Object>());
        objects.add(new Object());
    }

}

Это вызовет UnsupportedOperationException и очень похожа на вашу ситуацию.

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

Ваш метод set должен возвращать новый ImmutableVector, который ничего не мутирует.

Возможно, вы захотите воспользоваться идеей @SotiriosDelimanolis и заставить оба класса реализовать интерфейс без установщика, а затем передать ваш код через интерфейс. Опять же, это может быть невозможно, учитывая ваш устаревший код.

person Daniel Kaplan    schedule 29.08.2013
comment
Я обдумывал предложение Сотириоса Делиманолиса. Разве я не был бы вынужден разыграть до MutableVector3, чтобы мутировать? Это также потребует дополнительной проверки: if (position instanceof MutableVector3) Я думаю. - person crush; 29.08.2013
comment
@crush Ты может. Это зависит от вашего устаревшего кода. Если у вас есть код, который зависит от реализации изменяемых методов вектора, вам придется это сделать. Возможно, вы сможете реорганизовать метод set изменяемого вектора, чтобы он возвращал свой собственный тип. Затем весь устаревший код использует результат установки. Тогда ваш устаревший код будет зависеть от вашего нового интерфейса, а не от старой реализации. - person Daniel Kaplan; 29.08.2013
comment
Что-то вроде MutableVector3, public Vector3 set(x,y,z) { /*...*/ return this; } и на ImmutableVector3,public Vector3 set(x,y,z) { /*...*/ return new Vector3(x,y,z);? - person crush; 29.08.2013
comment
@давить да. Если вы делаете все в устаревшем коде, который вызывает MutableVector3.set, всегда делайте это: x.myMutableVector = x.myMutableVector.set(x, y, z);, тогда вы сможете заменить все реализации своим ImmutableVector3 и полностью удалить свой MutableVector3. К сожалению, компилятор не поможет вам, если вы забудете сказать x.myMutableVector = - person Daniel Kaplan; 29.08.2013
comment
Мне очень нравится ваше предложение вернуть экземпляр из метода set. Это даже упрощает мой код и упрощает модель легковеса! Когда кто-то попытается установить неизменяемый особый случай, он получит изменяемый Vector3 с установленными значениями, которые затем можно будет использовать с этого момента. Это, наряду с объектами Interface/Mutable/Immutable, является маршрутом, который я решил выбрать! Однако мне трудно решить, чей ответ принять, поскольку вы оба в равной степени помогли моему решению. - person crush; 29.08.2013
comment
@crush Это не тот маршрут, который я предлагал, но если это то, что лучше всего подходит для вас, это может быть жизнеспособным. Я думаю, что set на ImmutableVector должен вернуть еще один ImmutableVector. - person Daniel Kaplan; 29.08.2013
comment
В большинстве случаев, возможно, но так, как я его использую, лучше всего, чтобы он возвращал изменяемый вектор. Я обязательно задокументирую это надлежащим образом. Спасибо. - person crush; 29.08.2013

Неизменяемые классы не должны быть производными от конкретных изменяемых классов и наоборот. Вместо этого у вас должен быть ReadableFoo абстрактный класс с конкретными MutableFoo и ImmutableFoo производными. Таким образом, метод, которому нужен объект, который он может видоизменять, может потребовать MutableFoo, метод, которому нужен объект, чье текущее состояние можно сохранить, просто сохранив ссылку на объект, может потребовать ImmutableFoo, а метод, который не захочет изменять состояние объекта и не будет заботиться о том, что делает состояние после его возврата, может свободно принимать ReadableFoo, не беспокоясь о том, являются ли они изменчивыми или неизменяемыми.

Самый сложный аспект заключается в том, чтобы решить, лучше ли включить в базовый класс метод для проверки того, является ли он изменчивым, вместе с методом «set», который будет вызывать исключение, если это не так, или просто опустить метод «set». Я бы предпочел опустить метод «set», если только тип не включает функции, облегчающие шаблон копирования при записи.

Такой шаблон можно упростить, если базовый класс включает абстрактные методы AsImmutable, AsMutable вместе с конкретными AsNewMutable. Метод, который получает ReadableFoo с именем George и хочет зафиксировать его текущее состояние, может сохранить в поле GeorgeField либо George.AsImmutable(), либо George.AsNewMutable() в зависимости от того, думает ли он, что захочет изменить его. Если ему нужно изменить его, он может установить GeorgeField = GeorgeField.AsMutable(). Если ему нужно поделиться им, он может вернуть GeorgeField.AsImmutable(); если он планирует поделиться им много раз, прежде чем изменить его снова, он может установить GeorgeField = GeorgeField.AsImmutable(). Если многим объектам необходимо совместно использовать состояния ...Foos, и большинству из них вообще не нужно их изменять, но некоторым требуется много их модифицировать, такой подход может работать более эффективно, чем использование только изменяемых или неизменяемых объектов.

person supercat    schedule 29.08.2013
comment
Рассмотрим мой пример выше. У меня есть MyObject с членом position на нем. position это Vector3. Предположим, что Vector3 — это базовый класс/интерфейс, и у меня есть MutableVector3 и ImmutableVector3, производные от него. position изначально установлен как экземпляр ImmutableVector3. Он остается ImmutableVector3 до тех пор, пока не потребуется изменить position. В этот момент в измененной позиции будет создан новый MutableVector3, который будет использоваться с этого момента. Однако, если set() не указан в базе/интерфейсе, то в любое время, когда я захочу set(), мне нужно будет проверить неизменность. - person crush; 29.08.2013
comment
@crush: если бы методы set были исключены из базового интерфейса, для здравомыслия, вероятно, пришлось бы включить один AsSameMutable() [либо вернуть self как изменяемый, либо выдать исключение]; если бы кто-то хотел вызвать SetXX и SetYY на GeorgeField, потребовался бы GeorgeField=GeorgeField.AsMutable(); GeorgeField.AsSameMutable.SetXX(); GeorgeField.AsSameMutable.SetYY();. Если использовать AsMutable вместо AsSameMutable и пропустить первый AsMutable, ошибочно полагая, что это уже произошло, попытки установить XX и YY просто провалятся. - person supercat; 29.08.2013