Co by się naprawdę stało, gdyby java.lang.String nie były ostateczne?

16

Jestem długoletnim programistą Java i wreszcie, po ukończeniu studiów, mam czas na przyzwoite przestudiowanie go, aby przystąpić do egzaminu certyfikacyjnego ... Jedną z rzeczy, które zawsze mnie niepokoiły, jest to, że String jest „ostateczny”. Rozumiem to, kiedy poczytam o kwestiach bezpieczeństwa i pokrewnych rzeczach ... Ale, na serio, czy ktoś ma na to prawdziwy przykład?

Na przykład, co by się stało, gdyby String nie był ostateczny? Jakby tego nie było w Ruby. Nie słyszałem żadnych skarg od społeczności Ruby ... I jestem świadomy StringUtils i pokrewnych klas, które musisz albo zaimplementować sam, albo przeszukać Internet, aby zaimplementować to zachowanie (4 linie kodu) chętnie.

wleao
źródło
9
To pytanie może Cię zainteresować na stosie przepełnienia: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens
Podobnie, co by się stało, gdyby java.io.Filenie było final. Och, nie jest. Facet.
Tom Hawtin - tackline
1
Koniec cywilizacji zachodniej, jaką znamy.
Tulains Córdova

Odpowiedzi:

22

Główną przyczyną jest szybkość: finalnie można rozszerzyć klas, co pozwoliło JIT na wszelkiego rodzaju optymalizacje podczas obsługi ciągów - nigdy nie trzeba sprawdzać przesłoniętych metod.

Innym powodem jest bezpieczeństwo wątków: niezmienne są zawsze bezpieczne dla wątków, ponieważ wątek musi je całkowicie zbudować, zanim zostaną przekazane komuś innemu - a po zbudowaniu nie można ich już zmienić.

Ponadto twórcy środowiska wykonawczego Java zawsze chcieli się mylić po stronie bezpieczeństwa. Możliwość rozszerzenia String (coś, co często robię w Groovy, ponieważ jest to bardzo wygodne), może otworzyć całą puszkę robaków, jeśli nie wiesz, co robisz.

Aaron Digulla
źródło
2
To nieprawidłowa odpowiedź. Poza weryfikacją wymaganą przez specyfikację JVM, HotSpot używa jedynie finaldo szybkiego sprawdzenia, czy metoda nie jest przesłonięta podczas kompilacji kodu.
Tom Hawtin - tackline
1
@ TomHawtin-tackline: Referencje?
Aaron Digulla
1
Wydaje się, że sugerujesz, że niezmienność i bycie ostatecznym są w jakiś sposób powiązane. „Innym powodem jest bezpieczeństwo wątków: niezmienne są zawsze bezpieczne dla wątków b ...”. Powodem, dla którego obiekt jest niezmienny lub nie, nie jest to, że są lub nie są ostateczne.
Tulains Córdova
1
Ponadto klasa String jest niezmienna bez względu na finalsłowo kluczowe w klasie. Tablica znaków, którą zawiera, jest ostateczna, dzięki czemu jest niezmienna. Ostateczne zamknięcie pętli przez klasę, jak wspomina @abl, ponieważ nie można wprowadzić zmiennej podklasy.
1
@Snowman Mówiąc ściślej, tablica znaków będąca końcową powoduje, że odwołanie do tablicy jest niezmienne, a nie jej zawartość.
Michał Kosmulski,
10

Jest jeszcze jeden powód, dla którego Stringklasa Java musi być ostateczna: jest ważna dla bezpieczeństwa w niektórych scenariuszach. Gdyby Stringklasa nie była ostateczna, następujący kod byłby podatny na subtelne ataki złośliwego rozmówcy:

 public String makeSafeLink(String url, String text) {
     if (!url.startsWith("http:") && !url.startsWith("https:"))
         throw SecurityException("only http/https URLs are allowed");
     return "<a href=\"" + escape(url) + "\">" + escape(text) + "</a>";
 }

Atak: złośliwy rozmówca może utworzyć podklasę String, EvilStringgdzie EvilString.startsWith()zawsze zwraca true, ale gdzie wartość EvilStringjest czymś złym (np javascript:alert('xss').). Ze względu na podklasę uniknęłoby to kontroli bezpieczeństwa. Atak ten znany jest jako podatność na przekroczenie limitu czasu użycia (TOCTTOU): między momentem sprawdzenia (od początku adresu URL http / https) a momentem użycia wartości (aby skonstruować fragment kodu HTML), efektywna wartość może się zmienić. Gdyby Stringnie było to ostateczne, ryzyko TOCTTOU byłoby wszechobecne.

Jeśli więc Stringnie jest to ostateczne, trudno jest napisać bezpieczny kod, jeśli nie możesz zaufać swojemu rozmówcy. Oczywiście w takiej właśnie sytuacji znajdują się biblioteki Java: mogą być one wywoływane przez niezaufane aplety, więc nie mają odwagi zaufać swojemu programowi wywołującemu. Oznacza to, że napisanie bezpiecznego kodu biblioteki byłoby nierozsądnie trudne, gdyby Stringnie było ostateczne.

DW
źródło
Oczywiście nadal można zdjąć ten wulgarny TOCTTOU za pomocą odbicia w celu zmodyfikowania char[]łańcucha, ale wymaga to trochę czasu.
Peter Taylor,
2
@Peter Taylor, to bardziej skomplikowane. Można użyć refleksji, aby uzyskać dostęp do zmiennych prywatnych, tylko jeśli SecurityManager zezwoli na to. Aplety i inny niezaufany kod nie mają tego uprawnienia, a zatem nie mogą używać refleksji do modyfikowania wartości prywatnej char[]w ciągu; dlatego nie mogą wykorzystać luki w zabezpieczeniach TOCTTOU.
DW
Wiem, ale wciąż pozostawia to aplikacje i podpisane aplety - a ilu ludzi tak naprawdę przeraża okno dialogowe ostrzegające o aplecie z podpisem własnym?
Peter Taylor,
2
@Peter, nie o to chodzi. Chodzi o to, że pisanie zaufanych bibliotek Java byłoby o wiele trudniejsze, a o wiele więcej zgłoszonych luk w bibliotekach Java, jeśli Stringnie byłoby to ostateczne. Tak długo, jak ten model zagrożenia jest istotny, staje się ważne, aby się przed nim bronić. O ile jest to ważne w przypadku apletów i innego niezaufanego kodu, jest to prawdopodobnie wystarczające uzasadnienie Stringostatecznego opracowania, nawet jeśli nie jest potrzebne w przypadku zaufanych aplikacji. (W każdym razie zaufane aplikacje mogą już ominąć wszystkie środki bezpieczeństwa, niezależnie od tego, czy są one ostateczne.)
DW
„Atak: złośliwy program wywołujący może utworzyć podklasę String, EvilString, gdzie EvilString.startsWith () zawsze zwraca true ...” - nie, jeśli startWith () jest ostateczny.
Erik
4

Jeśli nie, wielowątkowe aplikacje Java byłyby bałaganem (nawet gorzej niż w rzeczywistości).

IMHO Główną zaletą końcowych ciągów (niezmiennych) jest to, że są one z natury bezpieczne dla wątków: nie wymagają synchronizacji (pisanie tego kodu jest czasami dość trywialne, ale bardziej niż mniej niż to). Gdyby były zmienne, zagwarantowanie takich rzeczy, jak wielowątkowe zestawy narzędzi do systemu Windows byłoby bardzo trudne, a te rzeczy są absolutnie konieczne.

Randolf Rincón Fadul
źródło
3
finalklasy nie mają nic wspólnego ze zmiennością klasy.
Ta odpowiedź nie dotyczy pytania. -1
FUZxxl
3
Jarrod Roberson: jeśli klasa nie była ostateczna, można tworzyć zmienne struny za pomocą dziedziczenia.
ysdx
@JarrodRoberson w końcu ktoś zauważa błąd. Przyjęta odpowiedź oznacza również, że ostateczna wartość jest niezmienna.
Tulains Córdova
1

Kolejną zaletą Stringbycia ostatecznym jest to, że zapewnia przewidywalne wyniki, podobnie jak niezmienność. String, chociaż klasa ma być traktowana w kilku scenariuszach, takich jak typ wartości (kompilator nawet obsługuje ją przez String blah = "test"). Dlatego jeśli utworzysz ciąg o określonej wartości, oczekujesz, że będzie on miał pewne zachowanie, które jest dziedziczone do dowolnego ciągu, podobnie do oczekiwań, jakie miałbyś w przypadku liczb całkowitych.

Pomyśl o tym: co zrobić, jeśli podklasujesz java.lang.Stringi przesadzasz metody equalslub hashCode? Nagle dwa „testy” struny nie mogły się już równać.

Wyobraź sobie chaos, gdybym mógł to zrobić:

public class Password extends String {
    public Password(String text) {
        super(text);
    }

    public boolean equals(Object o) {
        return true;
    }
}

inne klasy:

public boolean isRightPassword(String suppliedString) {
    return suppliedString != null && suppliedString.equals("secret");
}

public void login(String username, String password) {
    return isRightUsername(username) && isRightPassword(password);
}

public void loginTest() {
    boolean success = login("testuser", new Password("wrongpassword"));
    // success is true, despite the password being wrong
}
Brandon
źródło
1

tło

Są trzy miejsca, w których finał może pojawić się w Javie:

Ukończenie klasy uniemożliwia wszystkie podklasy klasy. Ukończenie metody zapobiega nadpisaniu jej przez podklasy metody. Ostateczne ustawienie pola zapobiega późniejszej zmianie.

Nieporozumienia

Istnieją optymalizacje, które mają miejsce wokół ostatecznych metod i pól.

Ostatnia metoda ułatwia HotSpotowi optymalizację poprzez wstawianie. Jednak HotSpot robi to, nawet jeśli metoda nie jest ostateczna, ponieważ działa przy założeniu, że nie została zastąpiona, dopóki nie udowodniono inaczej. Więcej o tym na SO

Ostateczną zmienną można agresywnie zoptymalizować, a więcej na ten temat można przeczytać w rozdziale 17.5.3 JLS .

Jednak przy takim zrozumieniu należy pamiętać, że żadna z tych optymalizacji nie ma na celu doprowadzenia do końca klasy . Ukończenie klasy nie przyniesie żadnego wzrostu wydajności.

Ostatni aspekt klasy nie ma też nic wspólnego z niezmiennością. Można mieć niezmienną klasę (taką jak BigInteger ), która nie jest ostateczna, lub klasę, która jest zmienna i końcowa (taka jak StringBuilder ). Decyzja o tym, czy klasa powinna być ostateczna, jest kwestią projektu.

Ostateczny projekt

Ciągi są jednym z najczęściej używanych typów danych. Znajdują się jako klucze do map, przechowują nazwy użytkowników i hasła, są tym, co czytasz z klawiatury lub pola na stronie internetowej. Ciągi są wszędzie .

Mapy

Pierwszą rzeczą do rozważenia, co by się stało, gdybyś mógł podklasować String, jest uświadomienie sobie, że ktoś mógłby zbudować zmienną klasę String, która w innym przypadku wyglądałaby na String. Spowodowałoby to zamieszanie w Mapach wszędzie.

Rozważ ten hipotetyczny kod:

Map t = new TreeMap<String, Integer>();
Map h = new HashMap<String, Integer>();
MyString one = new MyString("one");
MyString two = new MyString("two");

t.put(one, 1); h.put(one, 1);
t.put(two, 2); h.put(two, 2);

one.prepend("z");

Jest to problem z używaniem generalnie modyfikowalnego klucza z Mapą, ale staram się tam znaleźć to, że nagle kilka rzeczy związanych z łamaniem Mapy. Wejście nie znajduje się już we właściwym miejscu na mapie. W HashMap wartość skrótu zmieniła się (powinna była) i dlatego nie jest już na właściwym wejściu. W TreeMap drzewo jest teraz zepsute, ponieważ jeden z węzłów znajduje się po niewłaściwej stronie.

Ponieważ używanie ciągu dla tych kluczy jest tak powszechne, należy temu zapobiec, czyniąc ciąg końcowym.

Być może zainteresuje Cię lektura Dlaczego String jest niezmienny w Javie? po więcej informacji o niezmiennej naturze Strun.

Niecne struny

Istnieje wiele różnych niecnych opcji dla ciągów. Zastanów się, czy utworzyłem ciąg, który zawsze zwracał wartość true, gdy wywołano równość ... i przekazałem go do sprawdzenia hasła? A może sprawiło, że przypisania do MyString wysyłałyby kopię ciągu na jakiś adres e-mail?

Są to bardzo realne możliwości, gdy masz możliwość podklasy String.

Java.lang Optymalizacje ciągów

Chociaż wcześniej wspominałem, że finał nie przyspiesza Sznurka. Jednak klasa String (i inne klasy w java.lang) często korzystają z ochrony pól i metod na poziomie pakietu, aby umożliwić innym java.langklasom majstrowanie przy elementach wewnętrznych zamiast ciągłego korzystania z publicznego interfejsu API dla String. Funkcje takie jak getChars bez sprawdzania zakresu lub lastIndexOf, który jest używany przez StringBuffer, lub konstruktor, który dzieli podstawową tablicę (zwróć uwagę, że jest to rzecz Java 6, która została zmieniona z powodu problemów z pamięcią).

Gdyby ktoś stworzył podklasę String, nie byłby w stanie udostępnić tych optymalizacji (chyba że byłby to również część java.lang, ale jest to zapieczętowany pakiet ).

Trudniej jest zaprojektować coś do rozszerzenia

Projektowanie czegoś, co ma być rozszerzalne, jest trudne . Oznacza to, że musisz ujawnić części swoich wewnętrznych elementów, aby coś innego można było zmodyfikować.

Rozszerzalny ciąg nie mógł naprawić przecieku pamięci . Te części musiałyby być narażone na podklasy, a zmiana tego kodu oznaczałaby, że podklasy uległyby awarii.

Java szczyci się kompatybilnością wsteczną i otwierając podstawowe klasy na rozszerzenia, traci się część tej zdolności do naprawiania rzeczy przy jednoczesnym zachowaniu obliczalności w podklasach stron trzecich.

Checkstyle ma regułę, która wymusza (co naprawdę frustruje mnie podczas pisania kodu wewnętrznego) o nazwie „DesignForExtension”, która wymusza, że ​​każda klasa to:

  • Abstrakcyjny
  • Finał
  • Puste wdrożenie

Uzasadnieniem dla którego jest:

Ten styl projektowania API chroni nadklasy przed podziałem na podklasy. Wadą jest to, że podklasy mają ograniczoną elastyczność, w szczególności nie mogą zapobiec wykonywaniu kodu w nadklasie, ale oznacza to również, że podklasy nie mogą uszkodzić stanu nadklasy poprzez zapomnienie o wywołaniu metody super.

Zezwolenie na rozszerzenie klas implementacji oznacza, że ​​podklasy mogą zepsuć stan klasy, na której są oparte, i sprawić, że różne gwarancje, które daje nadklasa, są nieważne. Czegoś tak skomplikowane jak String, to jest prawie pewne, że zmiana jego część będzie złamać coś.

Deweloperów

Jest to część bycia programistą. Rozważyć prawdopodobieństwo, że każdy programista stworzą własne podklasy String z własnej kolekcji utils w nim. Ale teraz tych podklas nie można swobodnie przypisywać sobie wzajemnie.

WleaoString foo = new WleaoString("foo");
MichaelTString bar = foo; // This doesn't work.

Ta droga prowadzi do szaleństwa. Casting do String wszędzie i sprawdzając, czy ciąg jest instancją swojej klasy String lub jeśli nie, co czyni nowy ciąg na podstawie tego jednego i ... po prostu nie. Nie rób

Jestem pewien, że można napisać dobrą klasę String ... ale zostawić pisanie wielu implementacji ciąg do tych szalonych ludzi, którzy piszą w C ++ i mają do czynienia z std::stringa char*i coś z doładowania i SString i całej reszty .

Java String Magic

Istnieje kilka magicznych rzeczy, które Java robi ze Strings. Ułatwiają one programistom radzenie sobie, ale wprowadzają pewne niespójności w języku. Zezwalanie na podklasy w Stringach wymagałoby bardzo ważnej przemyślenia, jak radzić sobie z tymi magicznymi rzeczami:

  • Literały łańcuchowe (JLS 3.10.5 )

    Posiadanie kodu, który pozwala na:

    String foo = "foo";

    Nie należy tego mylić z boksem typów liczbowych, takich jak liczba całkowita. Nie możesz 1.toString(), ale możesz "foo".concat(bar).

  • +Operatora (na trasę 15.18.1 )

    Żaden inny typ odwołania w Javie nie pozwala na użycie operatora. Ciąg jest wyjątkowy. Operator konkatenacji łańcuchów działa również na poziomie kompilatora, dzięki czemu "foo" + "bar"staje się "foobar"on bardziej kompilowany niż w czasie wykonywania.

  • Konwersja ciągów (JLS 5.1.11 )

    Wszystkie obiekty można konwertować na ciągi, używając ich w kontekście ciągów.

  • String Interning ( JavaDoc )

    Klasa String ma dostęp do puli ciągów, która pozwala na kanoniczne reprezentacje obiektu wypełnionego w typie kompilacji literałami ciągu.

Zezwalanie na podklasę String oznaczałoby, że te bity z String, które ułatwiają programowanie, stałyby się bardzo trudne lub niemożliwe, gdy możliwe są inne typy String.

Społeczność
źródło
0

Gdyby String nie był ostateczny, każda skrzynka narzędziowa programisty zawierałaby swój własny „String z kilkoma przyjemnymi metodami pomocniczymi”. Każda z nich byłaby niezgodna ze wszystkimi innymi skrzyniami narzędzi.

Uznałem to za głupie ograniczenie. Dziś jestem przekonany, że była to bardzo rozsądna decyzja. Utrzymuje ciągi jako ciągi znaków.


źródło
finalw Javie to nie to samo, co finalw C #. W tym kontekście jest to to samo, co C # readonly. Zobacz stackoverflow.com/questions/1327544/…
Arseni Mourzenko
9
@MainMa: Właściwie w tym kontekście wydaje się, że jest taki sam jak zapieczętowany C #, jak w public final class String.
konfigurator