Трябва ли да хвърля изключение при опит за мутация на неизменен клас? Ако е така, кое изключение?

Искам да предупредя разработчика, когато се опитва да промени неизменен обект. Неизменният обект всъщност е разширение на променлив обект и отменя сетерите на споменатия обект, за да го направи неизменен.

Променлив базов клас: 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);

Но да кажем, че той не проверява първо това. Мисля, че в този случай би било добре да хвърлите изключение, както е показано по-горе в метода 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 е много добър при обработката на много малки краткотрайни временни обекти (поради събирач на боклук от поколение, разпределянето на обекти е изключително евтино и почистването на обекти може дори да бъде без никакви разходи в най-добрия случай). Разбира се, това не е решение за много големи обекти като списъци, но за 3-измерен вектор това със сигурност е опция (и вероятно най-добрата комбинация, защото е лесна, безопасна и изисква по-малко код от другите). Не знам никакви примери в Java API за тази алтернатива, защото в дните, когато повечето части от API бяха създадени, виртуалната машина не беше толкова оптимизирана и твърде много обекти може би бяха проблем с производителността. Но това не трябва да ви пречи да го направите днес.

person Philipp Wendler    schedule 29.08.2013
comment
Щеше ли суперкласът все още да има метод set() върху него? Без метода set() на базата или интерфейс, ще трябва да прехвърлям до MutableVector, за да имам достъп до сетера, което също означава първо да проверя дали position е екземпляр на MutableVector. - person crush; 29.08.2013
comment
В този случай обаче искам да променя екземпляра само ако първо не е зададен на неизменен екземпляр. Ако това е неизменен екземпляр, тогава искам да създам нов променлив екземпляр и да го присвоя на 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
Не съм сигурен дали това ви помага, но има такъв въпрос относно 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
@crush да. Ако накарате всичко в наследения код, който извиква 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