Cosa succederebbe davvero se java.lang.String non fosse definitivo?


16

Sono uno sviluppatore Java da molto tempo e finalmente, dopo essermi laureato, ho tempo di studiarlo decentemente per sostenere l'esame di certificazione ... Una cosa che mi ha sempre infastidito è che String è "finale". Lo capisco quando leggo dei problemi di sicurezza e cose correlate ... Ma, sul serio, qualcuno ne ha un vero esempio?

Ad esempio, cosa accadrebbe se String non fosse definitiva? Come se non fosse in Ruby. Non ho sentito lamentele dalla comunità di Ruby ... E sono consapevole di StringUtils e delle classi correlate che devi implementare te stesso o cercare sul web per implementare quel comportamento (4 righe di codice) che sei disposto a farlo.


9
Potresti essere interessato a questa domanda su Stack Overflow: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens

Allo stesso modo, cosa accadrebbe se java.io.Filenon lo fosse final. Oh non lo è. Bugger.
Tom Hawtin - affronta il

1
La fine della civiltà occidentale come la conosciamo.
Tulains Córdova,

Risposte:


22

Il motivo principale è la velocità: le finalclassi non possono essere estese, il che ha consentito a JIT di eseguire tutti i tipi di ottimizzazioni durante la gestione delle stringhe: non è mai necessario verificare la presenza di metodi ignorati.

Un altro motivo è la sicurezza dei thread: gli immutabili sono sempre sicuri per i thread perché un thread deve costruirli completamente prima che possano essere passati a qualcun altro - e dopo la creazione, non possono più essere modificati.

Inoltre, gli inventori del runtime Java hanno sempre voluto sbagliare dal punto di vista della sicurezza. Essere in grado di estendere String (cosa che faccio spesso in Groovy perché è così conveniente) può aprire un'intera lattina di worm se non sai cosa stai facendo.


2
È una risposta errata. Oltre alla verifica richiesta dalle specifiche JVM, HotSpot utilizza solo finalcome controllo rapido che un metodo non venga sovrascritto durante la compilazione del codice.
Tom Hawtin - affronta il

1
@ TomHawtin-tackline: riferimenti?
Aaron Digulla,

1
Sembrerebbe implicare che l'immutabilità e l'essere definitivi siano in qualche modo correlati. "Un altro motivo è la sicurezza del thread: gli immutabili sono sempre thread thread b ...". La ragione per cui un oggetto è immutabile o no non è perché sono o non sono definitive.
Tulains Córdova,

1
Inoltre, la classe String è immutabile indipendentemente dalla finalparola chiave sulla classe. La matrice di caratteri che contiene è definitiva, rendendola immutabile. Rendere la classe stessa definitiva chiude il ciclo come menzionato @abl poiché non è possibile introdurre una sottoclasse mutabile.

1
@Snowman Per essere precisi, l'array di caratteri essendo finalizzato rende immutabile solo il riferimento dell'array, non il contenuto dell'array.
Michał Kosmulski,

10

C'è un altro motivo per cui la Stringclasse Java deve essere definitiva: è importante per la sicurezza in alcuni scenari. Se la Stringclasse non fosse definitiva, il seguente codice sarebbe vulnerabile agli attacchi sottili di un chiamante malintenzionato:

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

L'attacco: un chiamante malintenzionato potrebbe creare una sottoclasse di String, EvilStringdove EvilString.startsWith()ritorna sempre vero, ma dove il valore di EvilStringè qualcosa di malvagio (ad es javascript:alert('xss').). A causa della sottoclasse, questo eviterebbe il controllo di sicurezza. Questo attacco è noto come vulnerabilità TOCTTOU (time-of-check-to-time-of-use): tra il momento in cui viene eseguito il controllo (che l'URL inizia con http / https) e l'ora in cui viene utilizzato il valore (per costruire lo snippet html), il valore effettivo può cambiare. Se Stringnon fosse definitivo, i rischi di TOCTTOU sarebbero pervasivi.

Quindi, se Stringnon è definitivo, diventa difficile scrivere un codice sicuro se non puoi fidarti del tuo chiamante. Naturalmente, questa è esattamente la posizione in cui si trovano le librerie Java: potrebbero essere invocate da applet non attendibili, quindi non osano fidarsi del loro chiamante. Ciò significa che sarebbe irragionevolmente complicato scrivere un codice di libreria sicuro, se Stringnon fosse definitivo.


Certo, è ancora possibile estrarre quel vulcano TOCTTOU usando la riflessione per modificare l' char[]interno della stringa, ma richiede un po 'di fortuna nei tempi.
Peter Taylor,

2
@Peter Taylor, è più complicato di così. È possibile utilizzare la riflessione solo per accedere alle variabili private se SecurityManager ti dà il permesso di farlo. Alle applet e ad altro codice non attendibile non viene concessa questa autorizzazione, pertanto non è possibile utilizzare la reflection per modificare il privato char[]all'interno della stringa; pertanto, non possono sfruttare la vulnerabilità TOCTTOU.
DW

Lo so, ma questo lascia ancora applicazioni e applet firmate - e quante persone sono effettivamente spaventate dalla finestra di dialogo di avviso per un'applet autofirmata?
Peter Taylor,

2
@Peter, non è questo il punto. Il punto è che scrivere librerie Java attendibili sarebbe molto più difficile e ci sarebbero molte più vulnerabilità segnalate nelle librerie Java, se Stringnon fosse definitivo. Finché qualche volta questo modello di minaccia è rilevante, diventa importante difendersi. Fintanto che è importante per applet e altro codice non attendibile, ciò è probabilmente abbastanza per giustificare la realizzazione Stringfinale, anche se non è necessario per applicazioni affidabili. (In ogni caso, le applicazioni affidabili possono già ignorare tutte le misure di sicurezza, indipendentemente dal fatto che siano definitive.)
DW

"L'attacco: un chiamante malintenzionato potrebbe creare una sottoclasse di String, EvilString, in cui EvilString.startsWith () restituisce sempre true ..." - non se startWith () è definitivo.
Erik,

4

Altrimenti, le app Java multithread sarebbero un disastro (anche peggio di ciò che realmente è).

IMHO Il vantaggio principale delle stringhe finali (immutabili) è che sono intrinsecamente sicure per i thread: non richiedono alcuna sincronizzazione (scrivere quel codice a volte è abbastanza banale ma più che meno lontano da quello). Se fossero mutabili, garantire che cose come i toolkit di Windows con multithreading sarebbero molto difficili, e quelle cose sono strettamente necessarie.


3
finalle classi non hanno nulla a che fare con la mutabilità della classe.

Questa risposta non affronta la domanda. -1
FUZxxl

3
Jarrod Roberson: se la classe non era definitiva, puoi creare stringhe mutabili usando l'ereditarietà.
ysdx,

@JarrodRoberson finalmente qualcuno nota l'errore. La risposta accettata implica anche che i finali finali sono immutabili.
Tulains Córdova

1

Un altro vantaggio Stringdell'essere definitivi è che garantisce risultati prevedibili, proprio come l'immutabilità. String, sebbene una classe, è destinata a essere trattata in alcuni scenari, come un tipo di valore (il compilatore lo supporta anche tramite String blah = "test"). Pertanto, se si crea una stringa con un determinato valore, ci si aspetta che abbia un comportamento ereditato da qualsiasi stringa, simile alle aspettative che si avrebbero con gli interi.

Pensa a questo: cosa succede se subclassi e hai java.lang.Stringsuperato i metodi equalso hashCode? Improvvisamente il "test" di due stringhe non poteva più eguagliarsi.

Immagina il caos se potessi farlo:

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

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

qualche altra classe:

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
}

1

sfondo

Esistono tre posizioni in cui è possibile visualizzare Final in Java:

Rendere finale una classe impedisce qualsiasi sottoclasse della classe. La finalizzazione di un metodo impedisce alle sottoclassi del metodo di sovrascriverlo. La realizzazione di un campo finale ne impedisce la successiva modifica.

fraintendimenti

Ci sono ottimizzazioni che si verificano intorno ai metodi e ai campi finali.

Un metodo finale semplifica l'ottimizzazione di HotSpot tramite inline. Tuttavia, HotSpot lo fa anche se il metodo non è definitivo in quanto funziona supponendo che non sia stato ignorato fino a prova contraria. Maggiori informazioni al riguardo su SO

Un'ultima variabile può essere ottimizzata in modo aggressivo e si può leggere di più nella sezione JLS 17.5.3 .

Tuttavia, con questa comprensione si dovrebbe essere consapevoli del fatto che nessuna di queste ottimizzazioni riguarda la conclusione di una classe . Non si ottiene alcun miglioramento delle prestazioni rendendo finale una classe.

L'aspetto finale di una classe non ha nulla a che fare con l'immutabilità. Si può avere una classe immutabile (come BigInteger ) che non è final o una classe che è mutabile e final (come StringBuilder ). La decisione se una classe debba essere definitiva è una questione di design.

Progetto finale

Le stringhe sono uno dei tipi di dati più utilizzati. Si trovano come chiavi per le mappe, memorizzano nomi utente e password, sono ciò che leggi da una tastiera o un campo in una pagina web. Le stringhe sono ovunque .

Mappe

La prima cosa da considerare di ciò che accadrebbe se si potesse sottoclassare String è rendersi conto che qualcuno potrebbe costruire una classe String mutabile che sembrerebbe altrimenti essere una String. Ciò rovinerebbe Maps ovunque.

Considera questo ipotetico codice:

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");

Questo è un problema con l'utilizzo di una chiave modificabile in generale con una mappa, ma la cosa che sto cercando di ottenere è che all'improvviso un certo numero di cose sull'interruzione della mappa. La voce non è più nel posto giusto nella mappa. In una HashMap, il valore di hash è (avrebbe dovuto) essere modificato e quindi non è più nella voce corretta. Nella TreeMap, l'albero ora è rotto perché uno dei nodi è dalla parte sbagliata.

Poiché l'uso di una stringa per queste chiavi è così comune, questo comportamento dovrebbe essere prevenuto rendendo la stringa definitiva.

Potresti essere interessato a leggere Perché String è immutabile in Java? per ulteriori informazioni sulla natura immutabile delle stringhe.

Stringhe nefaste

Esistono diverse opzioni nefaste per le stringhe. Considera se ho creato una stringa che ha sempre restituito true quando è stato chiamato uguale ... e l'ha passato in un controllo password? O hai fatto in modo che i compiti a MyString inviassero una copia della stringa a qualche indirizzo email?

Queste sono possibilità reali quando hai la possibilità di sottoclassare String.

Java.lang Ottimizzazioni delle stringhe

Mentre prima ho detto che final non rende String più veloce. Tuttavia, la classe String (e altre classi in java.lang) fanno un uso frequente della protezione a livello di pacchetto di campi e metodi per consentire ad altre java.langclassi di essere in grado di armeggiare con gli interni anziché passare continuamente attraverso l'API pubblica per String. Funzioni come getChars senza controllo dell'intervallo o lastIndexOf utilizzato da StringBuffer o il costruttore che condivide l'array sottostante (si noti che è una cosa Java 6 che è stata modificata a causa di problemi di memoria).

Se qualcuno creasse una sottoclasse di String, non sarebbe in grado di condividere tali ottimizzazioni (a meno che non ne facesse parte java.lang, ma si tratta di un pacchetto sigillato ).

È più difficile progettare qualcosa per l'estensione

Progettare qualcosa da estendere è difficile . Significa che devi esporre parti dei tuoi interni affinché qualcos'altro possa essere modificato.

Una stringa estendibile non avrebbe potuto avere una perdita di memoria riparare la . Quelle parti di esso avrebbero dovuto essere esposte a sottoclassi e la modifica di quel codice avrebbe comportato la rottura delle sottoclassi.

Java è orgoglioso della retrocompatibilità e aprendo le classi core all'estensione, si perde parte di quella capacità di sistemare le cose pur mantenendo la calcolabilità con sottoclassi di terze parti.

Checkstyle ha una regola che impone (che mi frustra davvero quando scrivo codice interno) chiamato "DesignForExtension" che impone che ogni classe sia:

  • Astratto
  • Finale
  • Implementazione vuota

Il razionale per cui è:

Questo stile di progettazione dell'API protegge le superclassi dall'interruzione delle sottoclassi. Il rovescio della medaglia è che le sottoclassi sono limitate nella loro flessibilità, in particolare non possono impedire l'esecuzione di codice nella superclasse, ma ciò significa anche che le sottoclassi non possono corrompere lo stato della superclasse dimenticando di chiamare il metodo super.

Consentire l'estensione delle classi di implementazione significa che le sottoclassi possono eventualmente corrompere lo stato della classe su cui si basa e far sì che varie garanzie fornite dalla superclasse non siano valide. Per qualcosa di così complesso come String, è quasi una certezza che cambiando parte di esso si romperà qualcosa.

Hurbis sviluppatore

Fa parte dell'essere uno sviluppatore. Considera la probabilità che ogni sviluppatore crei la propria sottoclasse String con la propria raccolta di programmi di utilità all'interno. Ma ora queste sottoclassi non possono essere assegnate liberamente l'una all'altra.

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

In questo modo porta alla follia. Casting per String tutto il luogo e il controllo per vedere se la stringa è un'istanza della vostra classe String o, se non, facendo una nuova stringa in base a quello e ... basta, no. Non farlo.

Sono sicuro che è possibile scrivere una buona classe String ... ma lascio la scrittura più implementazioni di stringa a quei matti che scrivono C ++ e avere a che fare con std::stringe char*e qualcosa da boost e sString , e tutto il resto .

Magia della stringa Java

Ci sono molte cose magiche che Java fa con le stringhe. Ciò semplifica la gestione di un programmatore ma introduce alcune incongruenze nella lingua. Consentire le sottoclassi su String richiederebbe un pensiero molto significativo su come gestire queste cose magiche:

  • Letterali per archi (JLS 3.10.5 )

    Avere codice che consente di fare:

    String foo = "foo";

    Questo non deve essere confuso con la boxe dei tipi numerici come Integer. Non puoi farlo 1.toString(), ma puoi farlo "foo".concat(bar).

  • L' +operatore (JLS 15.18.1 )

    Nessun altro tipo di riferimento in Java consente di utilizzare un operatore su di esso. La stringa è speciale. L'operatore di concatenazione di stringhe funziona anche a livello di compilatore in modo che "foo" + "bar"diventi "foobar"quando viene compilato anziché in fase di esecuzione.

  • Conversione di stringhe (JLS 5.1.11 )

    Tutti gli oggetti possono essere convertiti in stringhe semplicemente utilizzandoli in un contesto String.

  • String Interning ( JavaDoc )

    La classe String ha accesso al pool di stringhe che gli consentono di avere rappresentazioni canoniche dell'oggetto che viene popolato al tipo di compilazione con i letterali String.

Consentire una sottoclasse di String significherebbe che questi bit con String che semplificano la programmazione diventerebbero molto difficili o impossibili da eseguire quando sono possibili altri tipi di String.


0

Se String non fosse definitivo, ogni cassetta degli strumenti del programmatore conterrebbe il proprio "String con solo alcuni simpatici metodi di supporto". Ognuno di questi sarebbe incompatibile con tutti gli altri forzieri.

Ho considerato una restrizione sciocca. Oggi sono convinto che sia stata una decisione molto valida. Mantiene le stringhe come stringhe.


finalin Java non è la stessa cosa finaldi C #. In questo contesto, è la stessa cosa di C # readonly. Vedere stackoverflow.com/questions/1327544/...
Arseni Mourzenko

9
@MainMa: In realtà, in questo contesto sembra che sia lo stesso del sigillo di C #, come in public final class String.
configuratore
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.