Perché String.chars () è un flusso di ints in Java 8?


198

In Java 8, esiste un nuovo metodo String.chars()che restituisce un flusso di ints ( IntStream) che rappresenta i codici carattere. Immagino che molte persone si aspetterebbero un flusso di chars qui invece. Qual è stata la motivazione per progettare l'API in questo modo?


4
@RohitJain Non intendevo alcun flusso particolare. Se CharStreamnon esiste quale sarebbe il problema aggiungerlo?
Adam Dyga,

5
@AdamDyga: i progettisti hanno scelto esplicitamente di evitare l'esplosione di classi e metodi limitando i flussi primitivi a 3 tipi, poiché gli altri tipi (char, short, float) possono essere rappresentati dal loro equivalente più grande (int, double) senza alcun significativo penalità di prestazione.
JB Nizet,

3
@JBNizet Ho capito. Ma sembra ancora una soluzione sporca solo per salvare un paio di nuove classi.
Adam Dyga,

9
@JB Nizet: A me sembra che abbiamo già abbiamo un'esplosione di interfacce dato tutto flusso sovraccarico così come tutte le interfacce di funzione ...
Holger

5
Sì, c'è già un'esplosione, anche con solo tre specializzazioni di flusso primitivo. Cosa sarebbe se tutte e otto le primitive avessero specializzazioni in streaming? Un cataclisma? :-)
Stuart segna il

Risposte:


218

Come altri hanno già detto, la decisione progettuale alla base di ciò è stata quella di prevenire l'esplosione di metodi e classi.

Tuttavia, personalmente penso che questa sia stata una pessima decisione, e dovrei, dato che non vogliono prendere CharStream, che è ragionevole, metodi diversi invece di chars(), vorrei pensare a:

  • Stream<Character> chars(), che fornisce un flusso di scatole di personaggi, che avranno una leggera penalità prestazionale.
  • IntStream unboxedChars(), che verrebbe utilizzato per il codice delle prestazioni.

Tuttavia , invece di concentrarmi sul perché attualmente viene fatto in questo modo, penso che questa risposta dovrebbe focalizzarsi sul mostrare un modo per farlo con l'API che abbiamo ottenuto con Java 8.

In Java 7 l'avrei fatto così:

for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

E penso che un metodo ragionevole per farlo in Java 8 sia il seguente:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

Qui ottengo un IntStreame lo mappa su un oggetto tramite lambda i -> (char)i, questo lo inscatolerà automaticamente in un Stream<Character>, e quindi possiamo fare quello che vogliamo, e ancora utilizzare i riferimenti di metodo come un plus.

Siate consapevoli del fatto che dovete farlo mapToObj, se dimenticate e usate map, allora niente vi lamenterà, ma finirete comunque con un IntStream, e potreste essere lasciati fuori chiedendovi perché stampa i valori interi anziché le stringhe che rappresentano i caratteri.

Altre brutte alternative per Java 8:

Rimanendo in un IntStreame volendo stamparli alla fine, non è più possibile utilizzare i riferimenti di metodo per la stampa:

hello.chars()
        .forEach(i -> System.out.println((char)i));

Inoltre, l'uso dei riferimenti di metodo al tuo metodo non funziona più! Considera quanto segue:

private void print(char c) {
    System.out.println(c);
}

e poi

hello.chars()
        .forEach(this::print);

Ciò genererà un errore di compilazione, poiché potrebbe esserci una conversione con perdita.

Conclusione:

L'API è stata progettata in questo modo per non voler aggiungere CharStream, penso personalmente che il metodo debba restituire un Stream<Character>, e la soluzione alternativa attualmente è quella di utilizzare mapToObj(i -> (char)i)un IntStreamper essere in grado di funzionare correttamente con loro.


7
La mia conclusione: questa parte dell'API è rotta dal design. Ma grazie per la risposta estesa
Adam Dyga,

27
+1, ma la mia proposta è di utilizzare al codePoints()posto di chars()e troverete molte funzioni di libreria che accettano già un intpunto di codice in aggiunta a char, ad esempio tutti i metodi di java.lang.Charactercosì come StringBuilder.appendCodePoint, ecc. Questo supporto esiste da allora jdk1.5.
Holger,

6
Buon punto sui punti di codice. Il loro utilizzo gestirà caratteri supplementari, che sono rappresentati come coppie surrogate in un Stringo char[]. Scommetto che la maggior parte del charcodice di elaborazione gestisce male le coppie surrogate.
Stuart segna il

2
@skiwi, definire void print(int ch) { System.out.println((char)ch); }e quindi è possibile utilizzare i riferimenti di metodo.
Stuart segna il

2
Vedi la mia risposta per il motivo per cui è Stream<Character>stato respinto.
Stuart segna il

90

La risposta di Skiwi copriva già molti dei punti principali. Riempirò un po 'di più lo sfondo.

Il design di qualsiasi API è una serie di compromessi. In Java, uno dei problemi difficili riguarda le decisioni di progettazione prese molto tempo fa.

I primitivi sono in Java dal 1.0. Fanno di Java un linguaggio "impuro" orientato agli oggetti, poiché le primitive non sono oggetti. L'aggiunta di primitivi è stata, a mio avviso, una decisione pragmatica per migliorare le prestazioni a scapito della purezza orientata agli oggetti.

Questo è un compromesso con cui viviamo ancora oggi, circa 20 anni dopo. La funzione di autoboxing aggiunta in Java 5 ha eliminato per lo più la necessità di ingombrare il codice sorgente con chiamate al metodo boxing e unboxing, ma l'overhead è ancora lì. In molti casi non è evidente. Tuttavia, se dovessi eseguire il boxing o unboxing all'interno di un ciclo interno, vedresti che può imporre un significativo sovraccarico di CPU e garbage collection.

Durante la progettazione dell'API Streams, era chiaro che dovevamo supportare le primitive. Il sovraccarico di boxe / unboxing eliminerebbe qualsiasi vantaggio prestazionale dal parallelismo. Non volevamo supportare tutti i primitivi, poiché ciò avrebbe aggiunto un'enorme quantità di disordine all'API. (Riesci davvero a vedere un uso per un ShortStream?) "Tutti" o "nessuno" sono luoghi confortevoli per un design, ma nessuno dei due era accettabile. Quindi abbiamo dovuto trovare un valore ragionevole di "alcuni". Abbiamo finito con specializzazioni primitivi per int, longe double. (Personalmente avrei lasciato fuori intma sono solo io.)

Perché CharSequence.chars()abbiamo considerato di tornare Stream<Character>(un primo prototipo potrebbe averlo implementato) ma è stato rifiutato a causa del sovraccarico di boxe. Considerando che una stringa ha charvalori come primitivi, sembrerebbe un errore imporre la boxe incondizionatamente quando il chiamante probabilmente farebbe solo un po 'di elaborazione sul valore e lo annullerebbe di nuovo in una stringa.

Abbiamo anche considerato una CharStreamspecializzazione primitiva, ma il suo utilizzo sembrerebbe piuttosto limitato rispetto alla quantità di massa che aggiungerebbe all'API. Non mi è sembrato utile aggiungerlo.

La penalità che questo impone ai chiamanti è che devono sapere che i valori IntStreamcontenuti charsono rappresentati come intse che il casting deve essere eseguito nel posto giusto. Questo è doppiamente confuso perché ci sono chiamate API sovraccaricate come PrintStream.print(char)e PrintStream.print(int)che differiscono notevolmente nel loro comportamento. Potrebbe sorgere un ulteriore punto di confusione perché anche la codePoints()chiamata restituisce un IntStreamma i valori in essa contenuti sono piuttosto diversi.

Quindi, questo si riduce a scegliere pragmaticamente tra diverse alternative:

  1. Non siamo in grado di fornire specializzazioni primitive, risultando in un'API semplice, elegante e coerente, ma che impone prestazioni elevate e costi generali GC;

  2. potremmo fornire un set completo di specializzazioni primitive, al costo di ingombrare l'API e imporre un onere di manutenzione agli sviluppatori JDK; o

  3. potremmo fornire un sottoinsieme di specializzazioni primitive, fornendo un'API di dimensioni moderate e ad alte prestazioni che impone un onere relativamente piccolo per i chiamanti in una gamma piuttosto ristretta di casi d'uso (elaborazione dei caratteri).

Abbiamo scelto l'ultimo.


1
Bella risposta! Tuttavia non risponde perché non ci possano essere due metodi diversi chars(), uno che restituisce un Stream<Character>(con una piccola penalità prestazionale) e un altro essere IntStream, è stato considerato anche questo? È molto probabile che le persone finiscano per mapparlo su Stream<Character>comunque se pensano che valga la pena la convenienza rispetto alla penalità prestazionale.
Skiwi

3
Il minimalismo entra qui. Se esiste già un chars()metodo che restituisce i valori char in an IntStream, non aggiunge molto per avere un'altra chiamata API che ottiene gli stessi valori ma in formato box. Il chiamante può inscatolare i valori senza troppi problemi. Certo sarebbe più conveniente non doverlo fare in questo caso (probabilmente raro), ma a costo di aggiungere disordine all'API.
Stuart segna il

5
Grazie a una domanda duplicata ho notato questo. Concordo sul fatto che il chars()ritorno IntStreamnon è un grosso problema, soprattutto se si considera che questo metodo è stato usato raramente. Tuttavia sarebbe bene avere un modo integrato per riconvertire IntStreamin String. Si può fare con .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), ma è davvero lungo.
Tagir Valeev,

7
@TagirValeev Sì, è piuttosto ingombrante. Con un flusso di punti di codice (un IntStream) non è troppo male: collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Immagino che non sia molto più breve, ma l'uso di punti di codice evita i (char)cast e consente l'uso di riferimenti a metodi. Inoltre gestisce correttamente i surrogati.
Stuart Marks

2
@IlyaBystrov Purtroppo i flussi primitivi come IntStreamnon hanno un collect()metodo che richiede un Collector. Hanno solo un collect()metodo a tre argomenti , come menzionato nei commenti precedenti.
Stuart segna il
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.