2013-08-29 9 views
5

Chcę powiadomić programistę, gdy próbuje zmutować niezmienny obiekt. Obiekt niezmienny jest w rzeczywistości rozszerzeniem zmiennego obiektu i przesłonił seterów na wymienionym obiekcie, aby stał się niezmienny.Czy powinienem rzucić wyjątek w próbie mutacji niezmiennej klasy? Jeśli tak, jaki wyjątek?

Zmienna klasa bazowa: 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; 
    } 
} 

wersja Niezmienne: 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? 
    } 
} 

Moje przypadek użycia jest następujący:

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; 
} 

Powiedzmy, że develo na mój zespół decyduje, że potrzebuje manipulować pozycją obiektu, ale obecnie jest ustawiony na statyczną, niezmienną instancję wektora współdzielonego Vector3.Zero.

Byłoby najlepiej, gdyby wiedział z wyprzedzeniem, że musi utworzyć nową instancję zmienny wektor jeżeli wektor pozycja jest ustawiona na udostępnionym instancji Vector3.Zero:

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

Ale załóżmy, że najpierw tego nie sprawdza. Myślę, że w tym przypadku dobrze byłoby rzucić wyjątek, jak pokazano powyżej w metodzie ImmutableVector3.set(x,y,z). Chciałem użyć standardowego wyjątku Java, ale nie byłem pewien, który byłby najbardziej odpowiedni i nie jestem w 100% przekonany, że jest to najlepszy sposób, aby sobie z tym poradzić.

Sugestie, które widziałem (to pytanie) [Czy nielegalny stan wyjątku jest właściwy dla obiektu niezmiennego? zdają się sugerować następujące:

  • IllegalStateException - ale to jest niezmienne obiekt, co ma tylko jednego państwa
  • UnsupportedOperationException - metoda set() jest obsługiwana, ponieważ zastępuje klasę bazową, więc może to być dobry wybór .
  • Wyjątek NoSuchElementException - nie jestem pewien, w jaki sposób byłby to dobry wybór, chyba że metoda jest uważana za element.

Co więcej, nie jestem nawet pewien, czy powinienem tu podać wyjątek. Nie mogłem po prostu zmienić obiektu i nie powiadomić programisty, że próbują zmienić niezmienny obiekt, ale wydaje się to również kłopotliwe. Problem z wyrzuceniem wyjątku w ImmutableVector3.set(x,y,z) polega na tym, że set musi wyrzucić wyjątek zarówno dla ImmutableVector3 jak i Vector3. Więc teraz, nawet jeśli Vector3.set(x,y,z) nigdy nie wyrzuci wyjątku, musi zostać umieszczony w bloku try/catch.

Pytania
  • jestem widokiem innej opcji oprócz rzucania wyjątków?
  • Jeśli wyjątki są najlepszym rozwiązaniem, który wyjątek powinienem wybrać?
+2

Pozwoliłbym użyć 'UnsuppoertedOperationException'. Dekoratory 'niemodyfikowalne *' z 'Collection' są dobrym precedensem. – kiheru

+0

@crush Czy rozwiązujesz * znany * problem z wydajnością, modyfikując Vector3D? Jeśli nie, po prostu unieważnij. Niech ludzie konstruują nową instancję, gdy chcą nowych wartości. Znany problem z wydajnością = wykonałeś testowanie/analizę wydajności i to było wąskie gardło. –

+0

@EricStein wartości Vector3 będą się zmieniać co sekundę lub częściej dla setek tysięcy obiektów w moim zasięgu. Jestem pod, prawdopodobnie fałszywym założeniem, że tworzenie nowego obiektu Vector3 za każdym razem, gdy potrzebuję zmienić położenie obiektu, może ostatecznie spowodować problemy z GC pauzami i/lub być wolniejsze niż po prostu ustawienie nowej pozycji na istniejącym obiekcie. – crush

Odpowiedz

10

Wyrzucanie wyjątków jest zwykle drogą do zrobienia i powinieneś użyć UnsupportedOperationException.

API sama Java nie polecam w collections framework:

The „destrukcyjne” metod zawartych w tym interfejsie, czyli metody, które modyfikują kolekcję na którym działają, są określone rzucać UnsupportedOperationException jeśli ta kolekcja nie obsługuje operacji.

Alternatywą jeśli tworzysz pełne klasy hierarchia jest stworzenie super klasy Vector że nie można go zmieniać i nie ma żadnego metod modyfikujących jak set() oraz dwie podklasy ImmutableVector (który gwarantuje niezmienność) i MutableVector (to jest modyfikowalne). Byłoby to lepsze, ponieważ uzyskasz bezpieczeństwo podczas kompilacji (nie możesz nawet próbować modyfikować numeru ImmutableVector), ale w zależności od sytuacji może być on zbyt zaawansowany (i możesz skończyć z wieloma klasami).

To podejście zostało wybrane na przykład dla modelu Scala collections, wszystkie one istnieją w trzech odmianach.

We wszystkich przypadkach, w których chcesz zmodyfikować wektor, należy użyć typu MutableVector. We wszystkich przypadkach, gdy nie interesuje cię modyfikowanie i po prostu chcesz z niego czytać, po prostu użyj Vector. We wszystkich przypadkach, w których chcesz zagwarantować niezmienność (np. Instancja Vector jest przekazywana do konstruktora, a chcesz się upewnić, że nikt jej później nie zmodyfikuje) użyjesz ImmutableVector (która zwykle zapewnia metodę kopiowania, więc po prostu zadzwoń pod ImmutableVector.copyOf(vector)) . Rzucanie w dół z Vector nigdy nie powinno być konieczne, należy podać poprawny podtyp, który jest potrzebny jako typ. Istotą tego rozwiązania (wymagającego większej ilości kodu) jest to, że jest się bezpiecznym typem i podczas kontroli kompilacji, więc odlewanie niszczące niszczy tę korzyść. Istnieją wyjątki, ale, na przykład metoda ImmutableVector.copyOf() będzie, jak optymalizacji, zwykle wygląda tak:

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

To wciąż bezpieczny i daje jedną dodatkową zaletę niezmiennych rodzajów, a mianowicie unikanie niepotrzebnego kopiowania.

Guava library ma duży zbiór implementacji immutable collection, które są zgodne z tym wzorcem (mimo że wciąż rozszerzają one standardowe interfejsy kolekcji Java, które nie dają się kompilować). Powinieneś przeczytać ich opis, aby dowiedzieć się więcej o niezmiennych typach.

Trzecia alternatywa: byłaby po prostu na niezmiennej klasie wektorowej, a nie w ogóle żadnych zmiennych. Bardzo często zmiennymi klasami używanymi do celów wydajności są przedwczesne optymalizacje, jak Java is very good in handling lots of small short-lived temporary objects (ze względu na pokoleniowy garbage collector, przydział obiektów jest bardzo tani, a oczyszczanie obiektów może być bez żadnych kosztów w najlepszym przypadku). Oczywiście nie jest to rozwiązanie dla bardzo dużych obiektów, takich jak listy, ale dla trójwymiarowego wektora jest to z pewnością opcja (i prawdopodobnie najlepsza kombinacja, ponieważ jest łatwa, bezpieczna i wymaga mniej kodu niż inne). Nie znam żadnych przykładów w języku Java API tej alternatywy, ponieważ w czasach, gdy powstawała większość części API, maszyna wirtualna nie była zoptymalizowana, a zbyt wiele obiektów mogło być problemem z wydajnością. Ale to nie powinno przeszkadzać ci dzisiaj.

+0

Czy super klasa nadal ma na sobie metodę 'set()'? Bez metody 'set()' na podstawie lub interfejsie, będę musiał rzucić do 'MutableVector', aby uzyskać dostęp do settera, co oznacza również sprawdzenie, czy' position' jest instancją 'MutableVector'. – crush

+0

W tym przypadku chcę jednak zmutować instancję tylko wtedy, gdy nie jest ustawiona jako instancja Immutable. Jeśli jest to instancja Immutable, to chcę utworzyć nową instancję Mutable i przypisać ją do 'position'. W związku z tym zawsze musiałbym rzutować na 'MutableVector', aby" ustawić "wektor. – crush

+0

Występują problemy z wydajnością, powinieneś dążyć do posiadania niezmiennej klasy. Będzie to miało dodatkową zaletę, polegającą na unikaniu podstawowej/niezmiennej/zmieniającej się hierarchii za każdym razem, gdy wejdziesz w tę sytuację. –

3

Powinieneś rzucić błąd kompilatora, jeśli spróbujesz zmutować niezmienną klasę.

Klasy trwałe nie powinny przedłużyć wartości zmiennych.

+1

Mówisz: Wyodrębnij interfejs z klasy zmiennych i zamiast tego niezmienną klasę zaimplementuj w tym interfejsie? –

+0

Nie byłam zbyt pewna, czy rozszerzyć zmienną klasę i czynić ją niezmienną. Czy masz jakieś sugestie dotyczące tego, jak poradzić sobie z powyższymi sytuacjami? – crush

+0

Wolałbym zamiast tego obsługiwane przez Javę metody "const", co znacznie przyczyniłoby się do rozwiązania tego nonsensu, ale jest już za późno. :-) –

3

Biorąc pod uwagę Twoją sytuację, myślę, że powinieneś rzucić UnsupportedOperationException. Oto dlaczego:

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()); 
    } 

} 

Ten rzuci UnsupportedOperationException i jest bardzo podobna sytuacja do Ciebie.

Ale jeszcze lepszą rzeczą do zrobienia (co może nie być możliwe) jest zaprojektowanie niezmiennych klas, więc nie ma metody mutowania, aby zadzwonić w pierwszej kolejności. Jako programista nie chcę się uczyć w środowisku wykonawczym, że nie mogę użyć metody, chcę się uczyć w czasie kompilacji lub przed nim.

Twoja metoda set powinna zwrócić nową ImmutableVector, która niczego nie mutuje.

Być może zechcesz wziąć pomysł @ SotiriosDelimanolis i sprawić, aby obie klasy zaimplementowały interfejs bez ustawiacza, a następnie poprowadzi twój kod dookoła interfejsu. Ponownie, może to nie być możliwe, biorąc pod uwagę twój dotychczasowy kod.

+0

Zastanawiam się nad sugestią SotiriosDelimanolis. Czy nie byłbym zmuszony rzucić na 'MutableVector3' w celu zmutowania? To też wymagałoby dodatkowego sprawdzenia: 'if (pozycja instanceof MutableVector3)' wierzę. – crush

+0

@crush You * może *. To zależy od Twojego starszego kodu. Jeśli masz kod, który zależy od implementacji zmiennych metod wektora, musisz. Możliwe, że będziesz w stanie refaktoryzować metodę 'set' wektora zmiennych, aby zwracała swój własny typ. Następnie cały dotychczasowy kod używaj wyniku settera. Twój starszy kod będzie zależał od nowego interfejsu, a nie od starej implementacji. –

+0

Coś jak 'MutableVector3',' publiczny zestaw Vector3 (x, y, z) {/*...*/ zwróć to; } 'i na' ImmutableVector3', 'publiczny zestaw Vector3 (x, y, z) {/*...*/ zwraca nowy wektor3 (x, y, z);'? – crush

1

Klasy trwałe nie powinny pochodzić z konkretnych zmiennych klas ani odwrotnie. Zamiast tego powinieneś mieć klasę abstrakcji ReadableFoo z konkretnymi pochodnymi betonu MutableFoo i ImmutableFoo. W ten sposób metoda, która chce obiektu, który może zmutować, może wymagać, aby obiekt, którego obecny stan można było utrzymać tylko przez utrzymywanie odniesienia do obiektu, może wymagać ImmutableFoo i metody, która nie będzie chciała zmieniać stan obiektu i nie będzie dbał o to, co stanie po zwróceniu, może swobodnie akceptować ReadableFoo, nie martwiąc się o to, czy są zmienne czy niezmienne.

Najtrudniejszym aspektem jest podjęcie decyzji, czy lepiej jest, aby klasa podstawowa zawierała metodę sprawdzania, czy jest zmienna, oraz metodę "ustawioną", która zostanie rzucona, jeśli nie jest, lub po prostu pominie "zestaw" metoda. Byłbym zwolennikiem pominięcia metody "zestawu", chyba że typ zawiera funkcje ułatwiające wzór kopiowania przy zapisie.

Taki wzór może być ułatwiony dzięki temu, że klasa podstawowa zawiera abstrakcyjne metody AsImmutable, AsMutable, wraz z konkretnym AsNewMutable. Metoda, która ma ReadableFoo o nazwie i chce przechwycić jej obecny stan, może przechowywać do pola GeorgeField albo w zależności od tego, czy uważa, że ​​będzie chciał ją zmodyfikować. Jeśli trzeba go zmodyfikować, może ustawić GeorgeField = GeorgeField.AsMutable(). Jeśli trzeba go udostępnić, może zwrócić GeorgeField.AsImmutable(); jeśli spodziewa się go udostępnić wiele razy przed ponowną zmianą, może ustawić GeorgeField = GeorgeField.AsImmutable(). Jeśli wiele obiektów wymaga współdzielenia stanów, a większość z nich nie musi ich wcale modyfikować, ale niektóre z nich wymagają dużej modyfikacji, takie podejście może pracować wydajniej niż przy użyciu samych obiektów zmiennych lub niezmiennych.

+0

Rozważ mój przykład powyżej. Mam 'MyObject' z elementem' position' na nim. 'position' to' Vector3'. Powiedzmy, że 'Vector3' jest klasą bazową/interfejsem i mam od niej' MutableVector3' i 'ImmutableVector3'. "position" jest początkowo ustawione na instancję 'ImmutableVector3'. Pozostaje 'ImmutableVector3', dopóki' pozycja' nie zmieni się. W tym momencie na zmienionej pozycji zostanie utworzone nowe 'MutableVector3' i użyte od tego momentu. Jednakże, jeśli 'set()' zostanie pominięte w bazie/interfejsie, to w każdej chwili, gdy chcę ustawić '()', powinienem sprawdzić niezmienność. – crush

+0

@crush: Jeśli 'set' metody zostały pominięte w interfejsie bazowym, to dla zdrowia prawdopodobnie musiałoby zawierać jedną' AsSameMutable() '[albo * zwracaj * self * jako zmienną lub wyrzuć wyjątek]; gdyby ktoś chciał wywołać 'SetXX' i' SetYY' na 'GeorgeField', potrzebował by' GeorgeField = GeorgeField.AsMutable(); GeorgeField.AsSameMutable.SetXX(); GeorgeField.AsSameMutable.SetYY(); '. Jeśli używałeś 'AsMutable' zamiast' AsSameMutable' i pominąłeś pierwsze 'AsMutable' błędnie myślące, że już się stało, próby ustawienia XX i YY po cichu zawiedzie. – supercat

Powiązane problemy