Dlaczego klasa String została uznana za ostateczną w Javie?

141

Odkąd dowiedziałem się, że zajęcia java.lang.Stringsą deklarowane jako końcowe w Javie, zastanawiałem się, dlaczego tak jest. Nie znalazłem wtedy żadnej odpowiedzi, ale ten post: Jak stworzyć replikę klasy String w Javie? przypomniał mi o moim pytaniu.

Jasne, String zapewnia wszystkie funkcje, których kiedykolwiek potrzebowałem i nigdy nie pomyślałem o żadnej operacji, która wymagałaby rozszerzenia klasy String, ale nigdy nie będziesz wiedział, czego ktoś może potrzebować!

Czy zatem ktoś wie, jaki był zamiar projektantów, gdy zdecydowali się na ostateczną wersję?

Alex Ntousias
źródło
Dziękuję wszystkim za odpowiedzi, zwłaszcza TrueWill, Bruno Reis i Thilo! Chciałbym móc wybrać więcej niż jedną odpowiedź jako najlepszą, ale niestety ...!
Alex Ntousias
1
Weź również pod uwagę rozprzestrzenianie się projektów typu „Och, potrzebuję tylko kilku użytecznych metod w projektach String”, które się pojawią - z których wszystkie nie mogą używać nawzajem łańcuchów, ponieważ były inną klasą.
Thorbjørn Ravn Andersen
Dzięki za tę odpowiedź jest bardzo przydatna. mamy teraz dwa fakty. String jest klasą Final i jest niezmienny, ponieważ nie można go zmienić, ale można go odnieść do innego obiektu. ale co z: - String a = new String ("test1"); wtedy s = "test2"; Jeśli String jest obiektem klasy Final, to jak można go zmodyfikować? Jak mogę użyć zmodyfikowanego obiektu końcowego. Proszę, pozwól mi, jeśli źle o coś zapytam.
Suresh Sharma
Możesz obejrzeć ten dobry artykuł .
Aniket Thakur
4
Jedną rzeczą, której na szczęście uniknęliśmy w Javie, jest to, że „każdy ma swoją własną podklasę String z wieloma dodatkowymi metodami i żadna z nich nie jest ze sobą kompatybilna”.
Thorbjørn Ravn Andersen

Odpowiedzi:

88

Implementacja łańcuchów znaków jako niezmiennych obiektów jest bardzo przydatna . Przeczytaj o niezmienności, aby dowiedzieć się więcej na ten temat.

Jedną z zalet niezmiennych obiektów jest to

Możesz udostępniać duplikaty, wskazując je na pojedyncze wystąpienie.

( stąd ).

Gdyby łańcuch znaków nie był ostateczny, można by utworzyć podklasę i mieć dwa ciągi, które wyglądają podobnie, gdy są „widziane jako ciągi znaków”, ale w rzeczywistości są różne.

Bruno Reis
źródło
70
O ile nie ma związku między końcowymi klasami a niezmiennymi obiektami, których nie widzę, nie widzę, jak twoja odpowiedź odnosi się do pytania.
wrzesień
9
Ponieważ jeśli nie jest to ostateczne, możesz przekazać StringChild do jakiejś metody jako parametr String i może być modyfikowalny (ponieważ zmienia się stan klasy potomnej).
helios
4
Łał! Głosy przeciw? Czy nie rozumiesz, jak podklasy odnoszą się do niezmienności? Byłbym wdzięczny za wyjaśnienie, na czym polega problem.
Bruno Reis
7
@Bruno, re: downvotes: Nie przegłosowałem Cię, ale możesz dodać zdanie o tym, jak zapobieganie podklasom wymusza niezmienność. W tej chwili jest to połowiczna odpowiedź.
Thilo
12
@BrunoReis - znalazłem fajny artykuł, do którego możesz linkować, zawiera wywiad z Jamesem Goslingiem (twórcą Java), w którym krótko omawia ten temat tutaj . Oto interesujący fragment: „Jedną z rzeczy, która zmusiła ciągi do niezmienności, było bezpieczeństwo. Masz metodę otwierania pliku. Przekazujesz do niej String. Następnie wykonuje wszelkiego rodzaju testy uwierzytelniania, zanim przejdzie do wykonania systemu operacyjnego Zadzwoń. Jeśli uda ci się zrobić coś, co skutecznie zmutowało String, po kontroli bezpieczeństwa i przed wywołaniem systemu operacyjnego, to bum, jesteś w ... "
Anurag
60

To fajny artykuł, który przedstawia dwa powody, o których już wspomniano w powyższych odpowiedziach:

  1. Bezpieczeństwo : system może przekazywać wrażliwe fragmenty informacji tylko do odczytu bez obawy, że zostaną zmienione
  2. Wydajność : niezmienne dane są bardzo przydatne w zapewnianiu bezpieczeństwa wątków.

I to jest prawdopodobnie najbardziej szczegółowy komentarz w tym artykule. Ma to związek z pulą ciągów w Javie i kwestiami bezpieczeństwa. Chodzi o to, jak zdecydować, co trafia do puli strun. Zakładając, że oba ciągi znaków są równe, jeśli ich sekwencja znaków jest taka sama, wówczas mamy warunek wyścigu określający, kto dotrze tam pierwszy, a wraz z nim kwestie bezpieczeństwa. Jeśli nie, to pula ciągów będzie zawierała zbędne ciągi, co spowoduje utratę korzyści z posiadania jej w pierwszej kolejności. Po prostu przeczytaj to sobie, dobrze?


Rozszerzanie String mogłoby siać spustoszenie wśród równych sobie i stażystów. JavaDoc mówi, że równa się:

Porównuje ten ciąg z określonym obiektem. Wynik jest prawdziwy wtedy i tylko wtedy, gdy argument nie ma wartości null i jest obiektem String, który reprezentuje tę samą sekwencję znaków, co ten obiekt.

Zakładając, że java.lang.Stringnie było ostateczne, a SafeStringmoże równać się a Stringi na odwrót; ponieważ reprezentowałyby tę samą sekwencję znaków.

Co by się stało, gdybyś zgłosił się interndo SafeString- czy SafeStringtrafiłby do puli stringów JVM? ClassLoaderI wszystkich obiektow SafeStringreferencje uznane byłyby następnie zablokowane na miejscu przez cały okres JVM. Otrzymasz stan wyścigu o tym, kto może być pierwszym, który internuje sekwencję postaci - może SafeStringwygrałbyś, może String, a może SafeStringzaładowany przez inny program ładujący (a więc inną klasę).

Jeśli wygrałeś wyścig w puli, byłby to prawdziwy singleton, a ludzie mogliby uzyskać dostęp do całego twojego środowiska (piaskownicy) poprzez refleksję i secretKey.intern().getClass().getClassLoader().

JVM może też zablokować tę dziurę, upewniając się, że do puli zostały dodane tylko konkretne obiekty typu String (bez podklas).

Jeśli equals zostało zaimplementowane w taki sposób, że SafeString! = StringThen SafeString.intern! = String.internI SafeStringmusiałoby zostać dodane do puli. Pula stałaby się wtedy pulą <Class, String>zamiast, <String>a wszystko, czego potrzebujesz, aby wejść do puli, to nowy program ładujący klasy.

Anurag
źródło
2
Oczywiście przyczyna wydajności jest błędna: gdyby String był interfejsem, byłbym w stanie zapewnić implementację, która działa lepiej w mojej aplikacji.
Stephan Eggermont
28

Absolutnie najważniejszym powodem, dla którego String jest niezmienny lub ostateczny, jest to, że jest używany przez mechanizm ładowania klas, a zatem ma głębokie i podstawowe aspekty bezpieczeństwa.

Gdyby parametr String był zmienny lub nie był ostateczny, żądanie załadowania „java.io.Writer” mogłoby zostać zmienione na załadowanie „mil.vogoon.DiskErasingWriter”

odniesienie: Dlaczego ciąg znaków jest niezmienny w Javie

Jackob
źródło
15

String jest bardzo podstawową klasą w Javie, wiele rzeczy polega na tym, że działa w określony sposób, na przykład jest niezmienna.

Utworzenie klasy finalzapobiega podklasom, które mogłyby złamać te założenia.

Zauważ, że nawet teraz, jeśli używasz odbicia, możesz przerwać ciągi (zmienić ich wartość lub kod skrótu). Refleksję można zatrzymać za pomocą menedżera ds. Bezpieczeństwa. Gdyby Stringnie było final, każdy mógłby to zrobić.

Inne klasy, które nie są zadeklarowane, finalpozwalają na zdefiniowanie nieco uszkodzonych podklas (na przykład możesz mieć Listdodaną do niewłaściwej pozycji), ale przynajmniej JVM nie jest zależny od tych klas w swoich podstawowych operacjach.

Thilo
źródło
6
finalna klasie nie gwarantuje niezmienności. Gwarantuje tylko, że niezmienniki klasy (z których jeden może być niezmiennością) nie mogą zostać zmienione przez podklasę.
Kevin Brock
1
@Kevin: tak. Finał na klasie gwarantuje, że nie ma podklas. Nie ma nic wspólnego z niezmiennością.
Thilo
4
Finał klasy sam w sobie nie czyni go odpornym. Jednak ostateczne utworzenie niezmiennej klasy gwarantuje, że nikt nie utworzy podklasy, która łamie niezmienność. Być może osoby, które zwracały uwagę na niezmienność, nie wiedziały dokładnie, co mają na myśli, ale ich wypowiedzi są poprawne, gdy są rozumiane w kontekście.
Jay
Kilka razy przeczytałem tę odpowiedź i pomyślałem, że to dobra odpowiedź, a potem przeczytałem hashcode & equals z „The Effective Java” i zdałem sobie sprawę, że to bardzo dobra odpowiedź. Każdy potrzebuje wyjaśnień, polecam ieam 8 i iteam 9 tej samej książki.
Abhishek Singh
6

Jak powiedział Bruno, chodzi o niezmienność. Nie chodzi tylko o ciągi, ale także o wszelkie opakowania, np. Double, Integer, Character, itp. Jest wiele powodów takiego stanu rzeczy:

  • Bezpieczeństwo nici
  • Bezpieczeństwo
  • Sterta zarządzana przez samą Javę (inaczej niż zwykła sterta, która jest zbierana w inny sposób)
  • Zarządzanie pamięcią

Zasadniczo to, abyś jako programista mógł być pewien, że Twój łańcuch nigdy nie zostanie zmieniony. Jeśli wiesz, jak to działa, to również może poprawić zarządzanie pamięcią. Spróbuj utworzyć dwa identyczne ciągi jeden po drugim, na przykład „witaj”. Jeśli debugujesz, zauważysz, że mają identyczne identyfikatory, co oznacza, że ​​są to dokładnie TEGO SAME obiekty. Wynika to z faktu, że Java pozwala ci to zrobić. Nie byłoby to możliwe, gdyby struny były zmienne. Mogą mieć to samo „ja” itp., Ponieważ nigdy się nie zmienią. Więc jeśli kiedykolwiek zdecydujesz się utworzyć 1 000 000 ciągów znaków „cześć”, tak naprawdę utwórz 1 000 000 wskaźników na „cześć”. Podobnie, połączenie dowolnej funkcji w łańcuchu lub jakichkolwiek opakowań z tego powodu spowodowałoby utworzenie innego obiektu (ponownie spójrz na identyfikator obiektu - zmieni się).

Dodatkowo finał w Javie niekoniecznie oznacza, że ​​obiekt nie może się zmienić (różni się np. Od C ++). Oznacza to, że adres, na który wskazuje, nie może się zmienić, ale nadal możesz zmienić jego właściwości i / lub atrybuty. Zatem zrozumienie różnicy między niezmiennością a ostatecznością w niektórych przypadkach może być naprawdę ważne.

HTH

Bibliografia:

Artur
źródło
1
Nie wierzę, że ciągi znaków trafiają na inną stertę lub używają innego zarządzania pamięcią. Z pewnością nadają się do zbierania śmieci.
Thilo
2
Ponadto ostatnie słowo kluczowe w klasie jest zupełnie inne od końcowego słowa kluczowego dla pola.
Thilo
1
Okay, w JVM Sun'a, ciągi, które są intern () ed, mogą przejść do perm-gen, który nie jest częścią sterty. Ale to zdecydowanie nie dotyczy wszystkich Strings lub wszystkich JVM.
Thilo
2
Nie wszystkie struny trafiają do tego obszaru, tylko te, które zostały internowane. Internowanie jest automatyczne w przypadku literalnych ciągów znaków. (@Thilo, wpisując swój komentarz).
Kevin Brock
Dzięki za tę odpowiedź jest bardzo przydatna. mamy teraz dwa fakty. String jest klasą Final i jest niezmienny, ponieważ nie można go zmienić, ale można go odnieść do innego obiektu. ale co z: - String a = new String ("test1"); wtedy s = "test2"; Jeśli String jest obiektem klasy Final, to jak można go zmodyfikować? Jak mogę użyć zmodyfikowanego obiektu końcowego. Proszę, pozwól mi, jeśli źle o coś zapytam.
Suresh Sharma
2

Być może chodziło o uproszczenie wdrażania. Jeśli projektujesz klasę, która będzie dziedziczona przez użytkowników tej klasy, masz do rozważenia cały nowy zestaw przypadków użycia. Co się stanie, jeśli zrobią to lub tamto z polem chronionym X? Robiąc to ostatecznie, mogą skupić się na poprawnym działaniu interfejsu publicznego i upewnić się, że jest solidny.

AaronLS
źródło
3
+1 za „projektowanie do dziedziczenia jest trudne”. Przy okazji, to bardzo ładnie wyjaśniono w „Efektywnej Javie” Blocha.
sleske
2

Mając wiele dobrych punktów, które zostały już wspomniane, chciałbym dodać kolejny jeden z powodów, dla których String jest niezmienny w Javie, polega na umożliwieniu Stringowi buforowania swojego kodu skrótu , będąc niezmiennym String w Javie buforuje swój kod skrótu i nie oblicza wszystkich raz wywołujemy metodę hashcode typu String , co sprawia, że ​​jest ona bardzo szybka jak klucz hashmap do użycia w hashmap w Javie.

Krótko mówiąc, ponieważ String jest niezmienny, nikt nie może zmienić jego zawartości po utworzeniu, co gwarantuje, że hashCode String będzie taki sam przy wielu wywołaniach.

Jeśli widzisz, Stringklasa ma jest zadeklarowana jako

/** Cache the hash code for the string */
private int hash; // Default to 0

a hashcode()funkcja jest następująca -

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

Jeśli jest to już komputer, po prostu zwróć wartość.

Aniket Thakur
źródło
2

Aby mieć pewność, że nie otrzymamy lepszej implementacji. Powinien to być oczywiście interfejs.

[edytuj] Ach, coraz więcej głosów w dół. Odpowiedź jest całkowicie poważna. Musiałem kilkakrotnie programować obejście głupiej implementacji String, co doprowadziło do poważnej utraty wydajności i produktywności

Stephan Eggermont
źródło
2

Oprócz oczywistych powodów sugerowanych w innych odpowiedziach, jedna myśl o uczynieniu klasy String ostateczną może być również związana z narzutem wydajności metod wirtualnych. Pamiętaj, że String jest klasą ciężką, co sprawia, że ​​ta ostateczna implementacja oznacza na pewno brak pod-implementacji, co oznacza brak narzutu wywołań pośrednich. Oczywiście teraz mamy takie rzeczy jak wirtualne wywołanie i inne, które zawsze wykonują tego rodzaju optymalizację za Ciebie.

Abhishek Singh
źródło
2

Oprócz powodów wymienionych w innych odpowiedziach (bezpieczeństwo, niezmienność, wydajność) należy zauważyć, że Stringma specjalne wsparcie językowe. Możesz pisać Stringliterały i jest wsparcie dla +operatora. Zezwolenie programistom na tworzenie podklas Stringzachęcałoby do włamań, takich jak:

class MyComplex extends String { ... }

MyComplex a = new MyComplex("5+3i");
MyComplex b = new MyComplex("7+4i");
MyComplex c = new MyComplex(a + b);   // would work since a and b are strings,
                                      // and a string + a string is a string.
aioobe
źródło
1

Cóż, mam trochę inny myśli, że nie jestem pewien, czy mam rację, czy nie, ale w Java String jest jedynym obiektem, który może być traktowany jako prymitywne typu danych, a mam na myśli możemy stworzyć obiekt String jako String name = "java ” . Teraz, podobnie jak inne prymitywne typy danych, które są kopiowane przez wartość, a nie przez odniesienie, oczekuje się, że String będzie miał takie samo zachowanie, dlatego String jest ostateczny. Tak właśnie myślałem. Proszę zignorować, jeśli jest to całkowicie nielogiczne.

webdev
źródło
1
Moim zdaniem String nigdy nie zachowuje się jak typ prymitywny. Literał łańcuchowy, taki jak „java”, jest w rzeczywistości obiektem klasy String (można na nim użyć operatora kropki, bezpośrednio po cudzysłowie zamykającym). Zatem przypisanie literału do zmiennej łańcuchowej jest zwykłym przypisywaniem odwołań do obiektów. Różnica polega na tym, że klasa String ma wbudowaną obsługę na poziomie języka w kompilatorze ... zamieniając rzeczy w podwójnych cudzysłowach na obiekty String i operator +, jak wspomniano powyżej.
Georgie,
1

Ostateczność strun również broni ich jako standard. W C ++ możesz tworzyć podklasy stringów, więc każdy sklep programistyczny może mieć własną wersję stringów. Prowadziłoby to do braku mocnych standardów.

ncmathsadist
źródło
1

Powiedzmy, że masz Employeeklasę, która ma metodę greet. Kiedy greetmetoda jest wywoływana, po prostu drukuje Hello everyone!. Więc to jest oczekiwane zachowanie od greetmetody

public class Employee {

    void greet() {
        System.out.println("Hello everyone!");
    }
}

Teraz niech GrumpyEmployeepodklasa Employeei przesłonięcie greetmetody, jak pokazano poniżej.

public class GrumpyEmployee extends Employee {

    @Override
    void greet() {
        System.out.println("Get lost!");
    }
}

Teraz w poniższym kodzie spójrz na sayHellometodę. Przyjmuje Employeeinstancję jako parametr i wywołuje metodę greet z nadzieją, że powie Hello everyone!Ale to, co otrzymujemy, to Get lost!. Ta zmiana w zachowaniu jest spowodowanaEmployee grumpyEmployee = new GrumpyEmployee();

public class TestFinal {
    static Employee grumpyEmployee = new GrumpyEmployee();

    public static void main(String[] args) {
        TestFinal testFinal = new TestFinal();
        testFinal.sayHello(grumpyEmployee);
    }

    private void sayHello(Employee employee) {
        employee.greet(); //Here you would expect a warm greeting, but what you get is "Get lost!"
    }
}

Sytuacji tej można uniknąć, jeśli Employeezajęcia zostały wykonane final. Teraz to zależy od twojej wyobraźni, jaki chaos mógłby spowodować bezczelny programista, gdyby StringClass nie został zadeklarowany jako final.

Andy
źródło
0

Czy JVM wie, co jest niezmienne? Odpowiedź brzmi: Nie, stała pula zawiera wszystkie niezmienne pola, ale wszystkie niezmienne pola / obiekty nie są przechowywane tylko w stałej puli. Tylko my wdrażamy go w taki sposób, aby osiągnął niezmienność i swoje cechy. CustomString mógłby zostać zaimplementowany bez finalizacji przy użyciu MarkerInterface, który zapewniłby specjalne zachowanie java dla jego puli, ta funkcja jest nadal oczekiwana!

hi.nitish
źródło
0

Większość odpowiedzi dotyczy niezmienności - dlaczego obiekt typu String nie może być aktualizowany w miejscu. Jest tu wiele dobrych dyskusji, a społeczność Javy zrobiłaby dobrze, gdyby przyjęła niezmienność jako zasadę. (Nie wstrzymuję oddechu.)

Jednak pytanie PO dotyczy tego, dlaczego jest ostateczny - dlaczego nie można go przedłużyć. Niektórzy tutaj podjęli się tego, ale zgodziłbym się z PO, że jest tu prawdziwa luka. Inny język pozwala programistom tworzyć nowe typy nominalne dla typu. Na przykład w Haskell mogę utworzyć następujące nowe typy, które są identyczne w czasie wykonywania jak tekst, ale zapewniają bezpieczeństwo wiązania w czasie kompilacji.

newtype AccountCode = AccountCode Text
newtype FundCode = FundCode Text

Dlatego przedstawiłbym następującą sugestię jako ulepszenie języka Java:

newtype AccountCode of String;
newtype FundCode of String;

AccountCode acctCode = "099876";
FundCode fundCode = "099876";

acctCode.equals(fundCode);  // evaluates to false;
acctCode.toString().equals(fundCode.toString());  // evaluates to true;

acctCode=fundCode;  // compile error
getAccount(fundCode);  // compile error

(A może moglibyśmy zacząć oddzielać się od Javy)

dsmith
źródło
-1

Jeśli raz utworzysz łańcuch. Rozważy to, że jest to obiekt, jeśli chcesz to zmodyfikować, nie jest to możliwe, utworzy nowy obiekt.

Jasne
źródło
Proszę wyjaśnić swoją odpowiedź.
Marko Popovic,