Po co deklarować niezmienną klasę jako ostateczną w Javie?

83

Czytałem, że aby uczynić klasę niezmienną w Javie, powinniśmy wykonać następujące czynności:

  1. Nie dostarczaj żadnych ustawiaczy
  2. Oznacz wszystkie pola jako prywatne
  3. Doprowadź zajęcia do finału

Dlaczego wymagany jest krok 3? Dlaczego mam oznaczyć zajęcia final?

Anand
źródło
java.math.BigIntegerclass jest przykładem, jego wartości są niezmienne, ale nie są ostateczne.
Nandkumar Tekale,
@Nandkumar Jeśli masz BigInteger, nie wiesz, czy jest niezmienny, czy nie. To pomieszany projekt. / java.io.Filejest bardziej interesującym przykładem.
Tom Hawtin - tackline,
1
Nie zezwalaj podklasom na przesłanianie metod - Najprostszym sposobem na to jest zadeklarowanie klasy jako ostatecznej. Bardziej wyrafinowane podejście do konstruktora prywatnych i skonstruować instancje w metodach fabrycznych - od - docs.oracle.com/javase/tutorial/essential/concurrency/...
Raúl
1
@mkobit Filejest interesujący, ponieważ można mu ufać, że jest niezmienny, a zatem prowadzi do ataków TOCTOU (Time Of Check / Time Of Use). Zaufany kod sprawdza, czy Fileścieżka ma akceptowalną ścieżkę, a następnie używa ścieżki. Podklasa Filemoże zmieniać wartość między sprawdzeniem a użyciem.
Tom Hawtin - tackline
1
Make the class finallub Upewnij się, że wszystkie podklasy są niezmienne
O.Badr

Odpowiedzi:

138

Jeśli nie zaznaczysz klasy final, może być możliwe, że nagle zmienię twoją pozornie niezmienną klasę na zmienną. Na przykład rozważ ten kod:

public class Immutable {
     private final int value;

     public Immutable(int value) {
         this.value = value;
     }

     public int getValue() {
         return value;
     }
}

Teraz załóżmy, że wykonam następujące czynności:

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

Zauważ, że w mojej Mutablepodklasie nadpisałem zachowanie, getValueaby odczytać nowe, zmienne pole zadeklarowane w mojej podklasie. W rezultacie Twoja klasa, która początkowo wygląda na niezmienną, w rzeczywistości nie jest niezmienna. Mogę przekazać ten Mutableobiekt wszędzie tam, gdzie Immutableoczekiwany jest obiekt, co może spowodować bardzo złe rzeczy w kodzie, zakładając, że obiekt jest naprawdę niezmienny. Oznaczenie klasy bazowej finalzapobiega temu.

Mam nadzieję że to pomoże!

templatetypedef
źródło
13
Jeśli zrobię Mutable m = new Mutable (4); m.setValue (5); Tutaj bawię się z obiektem klasy Mutable, a nie z obiektem klasy Immutable, więc nadal nie wiem, dlaczego klasa Immutable nie jest niezmienna
Anand
18
@ anand- Wyobraź sobie, że masz funkcję, która przyjmuje Immutableargument. Mogę przekazać Mutableobiekt do tej funkcji, ponieważ Mutable extends Immutable. Wewnątrz tej funkcji, podczas gdy myślisz, że twój obiekt jest niezmienny, mógłbym mieć dodatkowy wątek, który zmienia wartość w trakcie działania funkcji. Mógłbym również podać Mutableobiekt, który funkcja przechowuje, a następnie zmienić zewnętrznie jego wartość. Innymi słowy, jeśli twoja funkcja zakłada, że ​​wartość jest niezmienna, może łatwo się zepsuć, ponieważ mógłbym dać ci zmienny obiekt i zmienić go później. Czy to ma sens?
templatetypedef
6
@ anand - metoda, do której przekazujesz Mutableobiekt, nie zmieni obiektu. Problem polega na tym, że ta metoda może zakładać, że obiekt jest niezmienny, podczas gdy tak naprawdę nie jest. Na przykład metoda może zakładać, że ponieważ uważa, że ​​obiekt jest niezmienny, może być używany jako klucz w pliku HashMap. Mógłbym wtedy przerwać tę funkcję, przekazując a Mutable, czekając, aż zapisze obiekt jako klucz, a następnie zmieniając Mutableobiekt. Teraz wyszukiwanie w tym HashMapnie powiedzie się, ponieważ zmieniłem klucz powiązany z obiektem. Czy to ma sens?
templatetypedef
3
@ templatetypedef- Tak, rozumiem to teraz… muszę powiedzieć, że to było niesamowite wyjaśnienie… chociaż zajęło trochę czasu, aby to zrozumieć
Anand,
1
@ SAM- Zgadza się. Jednak tak naprawdę nie ma to znaczenia, ponieważ nadpisane funkcje nigdy więcej nie odwołują się do tej zmiennej.
templatetypedef,
47

Wbrew temu, w co wielu ludzi sądzi, tworzenie niezmiennej klasy niefinal jest wymagane.

Standardowym argumentem przemawiającym za tworzeniem niezmiennych klas finaljest to, że jeśli tego nie zrobisz, podklasy mogą dodać zmienność, naruszając w ten sposób kontrakt nadklasy. Klienci klasy przyjmą niezmienność, ale będą zaskoczeni, gdy coś spod nich wyskoczy.

Jeśli doprowadzisz ten argument do jego logicznego ekstremum, należy zastosować wszystkie metody final, ponieważ w przeciwnym razie podklasa mogłaby przesłonić metodę w sposób, który nie jest zgodny z kontraktem jej nadklasy. To interesujące, że większość programistów Javy uważa to za śmieszne, ale w jakiś sposób zgadza się z ideą, że niezmienne klasy powinny być final. Podejrzewam, że ma to coś wspólnego z tym, że generalnie programiści Java nie są do końca zadowoleni z pojęcia niezmienności i być może z jakimś rozmytym myśleniem związanym z wieloma znaczeniami finalsłowa kluczowego w Javie.

Zgodność z umową twojej superklasy nie jest czymś, co może lub powinno być zawsze wymuszane przez kompilator. Kompilator może wymusić pewne aspekty kontraktu (np .: minimalny zestaw metod i ich sygnatury typów), ale istnieje wiele części typowych kontraktów, których kompilator nie może wymusić.

Niezmienność jest częścią kontraktu klasy. To trochę różni się od niektórych rzeczy, do których ludzie są bardziej przyzwyczajeni, ponieważ mówi, czego klasa (i wszystkie podklasy) nie mogą zrobić, podczas gdy myślę, że większość programistów Java (i ogólnie OOP) ma tendencję do myślenia o kontraktach jako odnoszących się do co klasa może zrobić, a nie czego nie może zrobić.

Niezmienność ma również wpływ nie tylko na pojedynczą metodę - ma wpływ na całą instancję - ale nie różni się to zbytnio od sposobu equalsi hashCodedziałania języka Java. Te dwie metody mają określoną umowę zawartą w Object. Ta umowa bardzo dokładnie określa rzeczy, których te metody nie mogą zrobić. Ten kontrakt jest bardziej szczegółowy w podklasach. Bardzo łatwo jest to uchylić equalslub hashCodew sposób naruszający umowę. W rzeczywistości, jeśli zastąpisz tylko jedną z tych dwóch metod bez drugiej, istnieje prawdopodobieństwo, że naruszysz umowę. Więc należy equalsi hashCodezostały uznane finalw Objecttego uniknąć? Myślę, że większość argumentowałaby, że nie powinni. Podobnie nie jest konieczne tworzenie niezmiennych klasfinal.

To powiedziawszy, większość twoich zajęć, niezmienna lub nie, prawdopodobnie powinna być final. Zobacz Effective Java Second Edition, pozycja 17: „Projektowanie i dokumentowanie do dziedziczenia albo zabronienie”.

Tak więc poprawna wersja kroku 3 brzmiałaby: „Uczyń klasę ostateczną lub, projektując podklasę, jasno udokumentuj, że wszystkie podklasy muszą być niezmienne”.

Laurence Gonsalves
źródło
3
Warto zauważyć, że Java „oczekuje”, ale nie narzuca, że ​​przy danych dwóch obiektach Xi Ywartość X.equals(Y)będzie niezmienna (o ile Xi Ynadal będzie się odnosić do tych samych obiektów). Ma podobne oczekiwania co do kodów skrótów. Jest jasne, że nikt nie powinien oczekiwać, że kompilator wymusi niezmienność relacji równoważności i kodów skrótu (ponieważ po prostu nie może). Nie widzę powodu, dla którego ludzie mieliby oczekiwać, że będzie to egzekwowane w innych aspektach tego typu.
supercat
1
Ponadto istnieje wiele przypadków, w których może być przydatne posiadanie typu abstrakcyjnego, którego umowa określa niezmienność. Na przykład można mieć abstrakcyjny typ ImmutableMatrix, który, biorąc pod uwagę parę współrzędnych, zwraca double. Można wyprowadzić a, GeneralImmutableMatrixktóre wykorzystuje tablicę jako magazyn zapasowy, ale można również mieć np. ImmutableDiagonalMatrixKtóry po prostu przechowuje tablicę elementów wzdłuż przekątnej (odczytanie elementu X, Y dałoby Arr [X], jeśli X == y i zero w przeciwnym razie) .
supercat
2
Najbardziej podoba mi się to wyjaśnienie. Tworzenie niezmiennej klasy zawsze finalogranicza jej użyteczność, zwłaszcza podczas projektowania interfejsu API, który ma zostać rozszerzony. W interesie bezpieczeństwa wątków sensowne jest uczynienie klasy tak niezmiennej, jak to tylko możliwe, ale utrzymywanie jej rozszerzalności. Możesz utworzyć pola protected finalzamiast private final. Następnie wyraźnie ustal kontrakt (w dokumentacji) o podklasach zgodnych z gwarancjami niezmienności i bezpieczeństwa wątków.
curioustechizen
4
+1 - Jestem z tobą aż do cytatu Blocha. Nie musimy być tak paranoikami. 99% kodowania to współpraca. Nie ma sprawy, jeśli ktoś naprawdę musi coś zmienić, pozwól mu. 99% z nas nie pisze niektórych podstawowych interfejsów API, takich jak String czy Collection. Niestety, wiele rad Blocha opiera się na tego rodzaju przypadkach użycia. Nie są zbyt pomocne dla większości programistów, zwłaszcza nowych.
ZhongYu,
Wolałbym uczynić niezmienną klasę final. To, że equalsi hashcodenie może być ostateczne, nie oznacza, że ​​mogę tolerować to samo dla niezmiennej klasy, chyba że mam do tego ważny powód, aw tym przypadku mogę użyć dokumentacji lub testów jako linii obrony.
O.Badr
21

Nie oceniaj całej klasy jako finału.

Istnieją uzasadnione powody, dla których zezwala się na przedłużenie niezmiennej klasy, jak stwierdzono w niektórych innych odpowiedziach, więc oznaczanie klasy jako ostatecznej nie zawsze jest dobrym pomysłem.

Lepiej jest oznaczyć swoje nieruchomości jako prywatne i ostateczne, a jeśli chcesz chronić „kontrakt”, oznacz swoje gettery jako ostateczne.

W ten sposób możesz pozwolić na rozszerzenie klasy (tak, być może nawet o zmienną klasę), jednak niezmienne aspekty twojej klasy są chronione. Właściwości są prywatne i nie można uzyskać do nich dostępu, metody pobierające te właściwości są ostateczne i nie można ich zastąpić.

Każdy inny kod, który używa instancji Twojej niezmiennej klasy, będzie mógł polegać na niezmiennych aspektach Twojej klasy, nawet jeśli przekazywana podklasa jest zmienna w innych aspektach. Oczywiście, ponieważ wymaga instancji Twojej klasy, nie wiedziałby nawet o innych aspektach.

Justin Ohms
źródło
1
Wolałbym zawrzeć wszystkie niezmienne aspekty w osobnej klasie i użyć ich za pomocą kompozycji. Byłoby łatwiejsze do rozważenia niż mieszanie stanu mutowalnego i niezmiennego w jednej jednostce kodu.
toniedzwiedz
1
Kompletnie się zgadzam. Kompozycja byłaby bardzo dobrym sposobem osiągnięcia tego celu, pod warunkiem, że twoja zmienna klasa zawiera niezmienną klasę. Jeśli twoja struktura jest odwrotna, byłoby to dobre rozwiązanie częściowe.
Justin Ohms,
5

Jeśli nie jest to ostateczne, każdy może rozszerzyć klasę i robić, co mu się podoba, na przykład dostarczać setery, cieniać prywatne zmienne i zasadniczo zmieniać je.

digitaljoel
źródło
Chociaż masz rację, nie jest jasne, dlaczego fakt, że możesz dodać zmienne zachowanie, musiałby zepsuć sytuację, jeśli wszystkie pola klasy bazowej są prywatne i ostateczne. Prawdziwym zagrożeniem jest to, że podklasa może zmienić poprzednio niezmienny obiekt w obiekt zmienny przez zastąpienie zachowania.
templatetypedef
4

To ogranicza inne klasy rozszerzające twoją klasę.

klasa końcowa nie może być przedłużona o inne zajęcia.

Jeśli klasa rozszerza klasę, którą chcesz uczynić jako niezmienną, może zmienić stan klasy ze względu na zasady dziedziczenia.

Po prostu wyjaśnij, że „może się zmienić”. Podklasa może nadpisywać zachowanie nadklasy, takie jak używanie zastępowania metod (np. Odpowiedź templatetypedef / Ted Hop)

kosa
źródło
2
To prawda, ale dlaczego jest to konieczne?
templatetypedef
@templatetypedef: Jesteś za szybki. Edytuję swoją odpowiedź z dodatkowymi punktami.
kosa
4
W tej chwili twoja odpowiedź jest tak niejasna, że ​​nie sądzę, by w ogóle odpowiadała na pytanie. Co masz na myśli, mówiąc „może to zmienić stan klasy ze względu na zasady dziedziczenia”? Jeśli nie wiesz już, dlaczego powinieneś to zaznaczyć final, nie widzę, jak ta odpowiedź pomaga.
templatetypedef
4

Jeśli nie sprawisz, że będzie ostateczna, mogę ją przedłużyć i uczynić niezmienną.

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

Teraz mogę przekazać FakeImmutable do dowolnej klasy, która oczekuje Immutable i nie będzie zachowywać się zgodnie z oczekiwanym kontraktem.

Roger Lindsjö
źródło
2
Myślę, że jest to prawie identyczne z moją odpowiedzią.
templatetypedef
Tak, sprawdzone w połowie wysyłania, brak nowych odpowiedzi. A potem, kiedy opublikowano, twoje były, 1 minutę przede mną. Przynajmniej nie używaliśmy tych samych nazw dla wszystkiego.
Roger Lindsjö,
Jedna korekta: w klasie FakeImmutable nazwa Konstruktora powinna brzmieć FakeImmutable NOT Immutable
Sunny Gupta
2

Aby utworzyć niezmienną klasę, oznaczanie jej jako ostatecznej nie jest obowiązkowe.

Weźmy jeden z takich przykładów z samych klas java. Klasa „BigInteger” jest niezmienna, ale nie jest ostateczna.

Właściwie niezmienność to pojęcie, zgodnie z którym obiekt utworzył, a następnie nie można go modyfikować.

Pomyślmy z punktu widzenia JVM, z punktu widzenia JVM wszystkie wątki muszą dzielić tę samą kopię obiektu i jest ona w pełni skonstruowana, zanim jakikolwiek wątek uzyska do niego dostęp, a stan obiektu nie zmienia się po jego skonstruowaniu.

Niezmienność oznacza, że ​​nie ma możliwości zmiany stanu obiektu po jego utworzeniu i jest to osiągane za pomocą trzech reguł kciuka, które sprawiają, że kompilator rozpoznaje, że klasa jest niezmienna i są one następujące: -

  • wszystkie pola nieprywatne powinny być ostateczne
  • upewnij się, że w klasie nie ma metody, która może bezpośrednio lub pośrednio zmienić pola obiektu
  • żadne odwołanie do obiektu zdefiniowane w klasie nie może być modyfikowane poza klasą

Więcej informacji można znaleźć pod poniższym adresem URL

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

Tarun Bedi
źródło
1

Powiedzmy, że masz następującą klasę:

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

}

Następnie przedłużasz go i łamiesz jego niezmienność.

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

Tutaj to testujemy:

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

Wynik wyjściowy będzie:

[a, b]
[a, b, some value]

Jak widać, podczas gdy oryginalna klasa zachowuje swoją niezmienność, klasy potomne mogą być modyfikowalne. W związku z tym w swoim projekcie nie możesz być pewien, że obiekt, którego używasz, jest niezmienny, chyba że sprawisz, że klasa będzie ostateczna.

valijon
źródło
0

Załóżmy, że następujące klasy nie były final:

public class Foo {
    private int mThing;
    public Foo(int thing) {
        mThing = thing;
    }
    public int doSomething() { /* doesn't change mThing */ }
}

Najwyraźniej jest niezmienny, ponieważ nawet podklasy nie mogą modyfikować mThing. Jednak podklasa może być zmienna:

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

Teraz obiekt, który można przypisać do zmiennej typu, Foonie jest już możliwy do zmodyfikowania. Może to powodować problemy z takimi rzeczami, jak haszowanie, równość, współbieżność itp.

Ted Hopp
źródło
0

Sam projekt nie ma wartości. Projekt jest zawsze używany do osiągnięcia celu. Jaki jest tutaj cel? Czy chcemy zmniejszyć ilość niespodzianek w kodzie? Czy chcemy zapobiegać błędom? Czy ślepo przestrzegamy zasad?

Ponadto projekt zawsze wiąże się z kosztami. Każdy projekt, który zasługuje na miano, oznacza konflikt celów .

Mając to na uwadze, musisz znaleźć odpowiedzi na następujące pytania:

  1. Ile oczywistych błędów to zapobiegnie?
  2. Ile subtelnych błędów to zapobiegnie?
  3. Jak często spowoduje to, że inny kod będzie bardziej złożony (= bardziej podatny na błędy)?
  4. Czy to sprawia, że ​​testowanie jest łatwiejsze czy trudniejsze?
  5. Jak dobrzy są programiści w Twoim projekcie? Ile wskazówek z młotkiem kowalskim potrzebują?

Załóżmy, że masz w swoim zespole wielu młodszych programistów. Będą desperacko próbować każdej głupoty tylko dlatego, że nie znają jeszcze dobrych rozwiązań swoich problemów. Ostateczna wersja klasy może zapobiec błędom (dobrze), ale może również spowodować, że znajdą "sprytne" rozwiązania, takie jak kopiowanie wszystkich tych klas do zmiennych, które można zmieniać w każdym miejscu kodu.

Z drugiej strony bardzo trudno będzie utworzyć klasę, finalgdy będzie używana wszędzie, ale łatwo jest uczynić finalklasę inną niżfinal później, jeśli okaże się, że musisz ją przedłużyć.

Jeśli właściwie używasz interfejsów, możesz uniknąć problemu „muszę uczynić ten mutowalny”, zawsze używając interfejsu, a następnie dodając modyfikowalną implementację, gdy zajdzie taka potrzeba.

Wniosek: nie ma „najlepszego” rozwiązania dla tej odpowiedzi. To zależy od tego, jaką cenę chcesz, a którą musisz zapłacić.

Aaron Digulla
źródło
0

Domyślne znaczenie equals () jest takie samo jak równość referencyjna. W przypadku niezmiennych typów danych jest to prawie zawsze błędne. Musisz więc nadpisać metodę equals (), zastępując ją własną implementacją. połączyć

Anitha BS
źródło