Por que String.chars () é um fluxo de entradas no Java 8?

195

No Java 8, existe um novo método String.chars()que retorna um fluxo de ints ( IntStream) que representa os códigos de caracteres. Eu acho que muitas pessoas esperariam um fluxo de chars aqui. Qual foi a motivação para projetar a API dessa maneira?

Adam Dyga
fonte
4
@RohitJain Eu não quis dizer nenhum fluxo específico. Se CharStreamnão existe, qual seria o problema para adicioná-lo?
Adam Dyga
5
@AdamDyga: Os designers escolheram explicitamente evitar a explosão de classes e métodos limitando os fluxos primitivos a 3 tipos, uma vez que os outros tipos (char, short, float) podem ser representados pelo seu equivalente maior (int, double) sem nenhum significado significativo. penalidade de desempenho.
JB Nizet
3
@JBNizet eu entendi. Mas ainda parece uma solução suja apenas para salvar algumas novas classes.
Adam Dyga
9
@JB Nizet: Para mim parece que já tem uma explosão de interfaces de dados todos os fluxos de sobrecarga, bem como todas as interfaces de função ...
Holger
5
Sim, já existe uma explosão, mesmo com apenas três especializações de fluxo primitivas. O que seria se todas as oito primitivas tivessem especializações em stream? Um cataclismo? :-)
Stuart Marks

Respostas:

215

Como outros já mencionaram, a decisão de design por trás disso foi impedir a explosão de métodos e classes.

Ainda assim, pessoalmente, acho que foi uma péssima decisão, e deveria, dado que eles não querem tomar CharStream, o que é razoável, métodos diferentes em vez de chars(), eu pensaria em:

  • Stream<Character> chars(), que fornece um fluxo de caracteres de caixas, que terão alguma penalidade no desempenho de luz.
  • IntStream unboxedChars(), que seria usado para o código de desempenho.

No entanto , em vez de focar no motivo de isso ser feito atualmente, acho que essa resposta deve se concentrar em mostrar uma maneira de fazer isso com a API que obtivemos com o Java 8.

No Java 7, eu teria feito assim:

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

E acho que um método razoável para fazer isso no Java 8 é o seguinte:

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

Aqui eu obtenho um IntStreame mapeio-o para um objeto via lambda i -> (char)i, isso o colocará automaticamente em um Stream<Character>, e então podemos fazer o que queremos, e ainda usar as referências de método como um plus.

Esteja ciente de que você deve fazer mapToObj, se você esquecer e usar map, nada reclamará, mas você ainda terá um IntStreame poderá ficar se perguntando por que ele imprime os valores inteiros em vez das cadeias que representam os caracteres.

Outras alternativas feias para Java 8:

Ao permanecer em um IntStreame desejando imprimi-los, você não poderá mais usar referências de métodos para imprimir:

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

Além disso, o uso de referências de métodos ao seu próprio método não funciona mais! Considere o seguinte:

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

e depois

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

Isso gerará um erro de compilação, pois possivelmente há uma conversão com perdas.

Conclusão:

A API foi projetada dessa maneira, por não querer adicionar CharStream, eu pessoalmente acho que o método deve retornar a Stream<Character>, e a solução atualmente é usar mapToObj(i -> (char)i)uma IntStreampara poder trabalhar corretamente com eles.

skiwi
fonte
7
Minha conclusão: esta parte da API está quebrada por design. Mas obrigado pela resposta extensa
Adam Dyga
27
+1, mas minha proposta é usar em codePoints()vez de chars()e você encontrará várias funções da biblioteca que já aceitam um intponto de código for adicionalmente a char, por exemplo, todos os métodos java.lang.Charactere também StringBuilder.appendCodePoint, etc. Esse suporte existe desde então jdk1.5.
Holger
6
Bom ponto sobre pontos de código. Usá-los manipulará caracteres suplementares, representados como pares substitutos em um Stringou char[]. Aposto que a maioria dos charcódigos de processamento manipula mal os pares substitutos.
Stuart Marks
2
@skiwi, defina void print(int ch) { System.out.println((char)ch); }e então você pode usar referências de método.
Stuart Marcas
2
Veja minha resposta para saber por que Stream<Character>foi rejeitado.
Stuart Marks
90

A resposta do skiwi já cobriu muitos dos principais pontos. Vou preencher um pouco mais de fundo.

O design de qualquer API é uma série de compensações. Em Java, uma das questões difíceis é lidar com decisões de design que foram tomadas há muito tempo.

As primitivas estão em Java desde a 1.0. Eles tornam o Java uma linguagem orientada a objetos "impura", uma vez que as primitivas não são objetos. A adição de primitivos foi, acredito, uma decisão pragmática para melhorar o desempenho à custa da pureza orientada a objetos.

Essa é uma troca que ainda estamos vivendo hoje, quase 20 anos depois. O recurso de caixa automática adicionado no Java 5 eliminou principalmente a necessidade de desorganizar o código-fonte com chamadas de métodos de boxe e unboxing, mas a sobrecarga ainda está lá. Em muitos casos, isso não é perceptível. No entanto, se você realizasse boxe ou unboxing dentro de um loop interno, veria que isso pode impor uma sobrecarga significativa na CPU e na coleta de lixo.

Ao projetar a API do Streams, ficou claro que precisávamos oferecer suporte a primitivos. A sobrecarga de boxe / unboxing mataria qualquer benefício de desempenho do paralelismo. No entanto, não queríamos oferecer suporte a todas as primitivas, pois isso acrescentaria uma enorme quantidade de confusão à API. (Você realmente pode ver o uso de um ShortStream?) "Todos" ou "nenhum" são lugares confortáveis ​​para um design, mas nenhum deles era aceitável. Então tivemos que encontrar um valor razoável de "alguns". Nós acabamos com especializações primitivos para int, long, e double. (Pessoalmente eu teria deixado de foraint mas sou apenas eu.)

Por CharSequence.chars()considerarmos o retorno Stream<Character>(um protótipo inicial pode ter implementado isso), mas foi rejeitado por causa da sobrecarga do boxe. Considerando que uma String tem charvalores como primitivos, parece um erro impor boxe incondicionalmente quando o chamador provavelmente apenas processa um pouco o valor e o desmarca de volta em uma string.

Também consideramos uma CharStreamespecialização primitiva, mas seu uso pareceria bastante restrito em comparação com a quantidade de volume que acrescentaria à API. Não parecia valer a pena adicioná-lo.

A penalidade que isso impõe aos chamadores é que eles precisam saber que os valores IntStreamcontidos charsão representados como intse que a transmissão deve ser feita no local apropriado. Isso é duplamente confuso, pois há chamadas de API sobrecarregadas como PrintStream.print(char)e PrintStream.print(int)que diferem acentuadamente em seu comportamento. Um ponto adicional de confusão possivelmente surge porque a codePoints()chamada também retorna um, IntStreammas os valores que ela contém são bem diferentes.

Portanto, isso se resume a escolher pragmaticamente entre várias alternativas:

  1. Não poderíamos fornecer especializações primitivas, resultando em uma API simples, elegante e consistente, mas que impõe um alto desempenho e sobrecarga de GC;

  2. poderíamos fornecer um conjunto completo de especializações primitivas, com o custo de sobrecarregar a API e impor uma carga de manutenção aos desenvolvedores do JDK; ou

  3. poderíamos fornecer um subconjunto de especializações primitivas, fornecendo uma API de tamanho médio e alto desempenho que impõe uma carga relativamente pequena aos chamadores em uma variedade bastante estreita de casos de uso (processamento de caracteres).

Nós escolhemos o último.

Stuart Marks
fonte
1
Boa resposta! No entanto, ele não responde por que não pode haver dois métodos diferentes para chars(), um que retorna um Stream<Character>(com pequena penalidade de desempenho) e outro IntStream, isso também foi considerado? É bem provável que as pessoas acabem mapeando-o de Stream<Character>qualquer maneira, se acharem que a conveniência vale a pena sobre a penalidade de desempenho.
skiwi
3
O minimalismo entra aqui. Se já existe um chars()método que retorna os valores de char em um IntStream, não é necessário adicionar outra chamada à API que obtenha os mesmos valores, mas em forma de caixa. O chamador pode colocar os valores em caixa sem muitos problemas. Certamente, seria mais conveniente não fazer isso nesse caso (provavelmente raro), mas com o custo de adicionar desorganização à API.
Stuart Marks
5
Graças à pergunta duplicada, notei esta. Concordo que o chars()retorno IntStreamnão é um grande problema, especialmente devido ao fato de esse método raramente ser usado. No entanto, seria bom ter uma maneira integrada de converter de volta IntStreampara o arquivo String. Isso pode ser feito .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), mas é muito longo.
Tagir Valeev
7
@TagirValeev Sim, é um pouco complicado. Com um fluxo de pontos de código (uma IntStream) não é muito ruim: collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Eu acho que não é realmente mais curto, mas o uso de pontos de código evita as (char)conversões e permite o uso de referências de método. Além disso, ele lida com substitutos corretamente.
Stuart Marcas
2
@IlyaBystrov Infelizmente, os fluxos primitivos, como IntStreamnão têm um collect()método que leva a Collector. Eles têm apenas um collect()método de três argumentos , como mencionado nos comentários anteriores.
Stuart Marcas