Por que o Iterable <T> não fornece os métodos stream () e parallelStream ()?

241

Eu estou querendo saber por que a Iterableinterface não fornece os métodos stream()e parallelStream(). Considere a seguinte classe:

public class Hand implements Iterable<Card> {
    private final List<Card> list = new ArrayList<>();
    private final int capacity;

    //...

    @Override
    public Iterator<Card> iterator() {
        return list.iterator();
    }
}

É uma implementação de uma Mão, pois você pode ter cartas na sua mão enquanto joga um Jogo de Cartas Colecionáveis.

Essencialmente, ele envolve a List<Card>, garante uma capacidade máxima e oferece alguns outros recursos úteis. É melhor implementá-lo diretamente como um List<Card>.

Agora, por conveniência, pensei que seria bom implementar Iterable<Card>, de modo que você possa usar loops for-for aprimorados se desejar fazer um loop sobre ele. (Minha Handclasse também fornece um get(int index)método, portanto, isso Iterable<Card>é justificado na minha opinião.)

A Iterableinterface fornece o seguinte (excluído o javadoc):

public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Agora você pode obter um fluxo com:

Stream<Hand> stream = StreamSupport.stream(hand.spliterator(), false);

Então, para a verdadeira questão:

  • Por que Iterable<T>não fornece métodos padrão que implementam stream()e parallelStream()não vejo nada que torne isso impossível ou indesejado?

Uma pergunta relacionada que encontrei é a seguinte: Por que o Stream <T> não implementa o Iterable <T>?
O que é curiosamente sugerido para fazê-lo de outra maneira.

skiwi
fonte
1
Acho que essa é uma boa pergunta para a Lista de Correspondência Lambda .
Edwin Dalorzo
Por que é estranho querer iterar em um fluxo? De que outra forma você poderia fazer break;uma iteração? (Ok, Stream.findFirst()pode ser uma solução, mas que pode não satisfazer todas as necessidades ...)
glglgl
Consulte também Converter Iterável em Stream usando o Java 8 JDK para soluções práticas.
Vadzim

Respostas:

298

Isso não foi uma omissão; houve discussão detalhada na lista de EG em junho de 2013.

A discussão definitiva do grupo de especialistas está enraizada neste tópico .

Embora parecesse "óbvio" (até mesmo para o grupo de especialistas) que stream()parecia fazer sentido Iterable, o fato de Iterableser tão geral se tornou um problema, devido à assinatura óbvia:

Stream<T> stream()

nem sempre era o que você ia querer. Algumas coisas que Iterable<Integer>prefeririam que seu método stream retornasse um IntStream, por exemplo. Mas colocar o stream()método tão alto na hierarquia tornaria isso impossível. Então, em vez disso, facilitamos muito a criação Streamde a Iterable, fornecendo um spliterator()método. A implementação de stream()in Collectioné apenas:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

Qualquer cliente pode obter o fluxo que deseja de um Iterablecom:

Stream s = StreamSupport.stream(iter.spliterator(), false);

No final, concluímos que adicionar stream()a Iterableseria um erro.

Brian Goetz
fonte
8
Vejo, antes de tudo, muito obrigado por responder à pergunta. Ainda estou curioso, porém, por que um Iterable<Integer>(acho que você está falando?) Gostaria de retornar um IntStream. O iterável então não seria um PrimitiveIterator.OfInt? Ou você quer dizer outro caso de uso?
skiwi
139
Acho estranho que a lógica acima tenha sido aplicada ao Iterable (não posso ter stream () porque alguém pode querer retornar o IntStream), enquanto uma quantidade igual de pensamento não foi dada para adicionar exatamente o mesmo método à Collection ( Talvez eu queira que o stream () do meu Collection <Integer> retorne o IntStream também.) Se estivesse presente em ambos ou ausente em ambos, as pessoas provavelmente teriam acabado de seguir em frente com suas vidas, mas porque estão presentes em um e ausentes em o outro, torna-se bastante uma omissão gritante ...
Trejkaz
6
Brian McCutchon: Isso faz mais sentido para mim. Parece que as pessoas se cansaram de discutir e decidiram jogar pelo seguro.
22816 Jonathan Locke
44
Embora isso faça sentido, há uma razão pela qual não há uma estática alternativa Stream.of(Iterable), que pelo menos tornaria o método razoavelmente detectável ao ler a documentação da API - como alguém que nunca trabalhou realmente com os internos do Streams que eu nunca mesmo olhou em StreamSupport, que é descrito na documentação como o fornecimento de "operações de baixo nível", que são "principalmente para escritores de biblioteca".
Jules
9
Eu concordo totalmente com Jules. um método estático Stream.of (iterador iterável) ou Stream.of (iterador iterador) deve ser adicionado, em vez de StreamSupport.stream (iter.spliterator (), false);
user_3380739
23

Eu fiz uma investigação em várias listas de discussão lambda do projeto e acho que encontrei algumas discussões interessantes.

Até agora não encontrei uma explicação satisfatória. Depois de ler tudo isso, concluí que era apenas uma omissão. Mas você pode ver aqui que foi discutido várias vezes ao longo dos anos durante o design da API.

Especialistas em especificações da Lambda Libs

Encontrei uma discussão sobre isso na lista de correspondência Lambda Libs Spec Experts :

Em Iterable / Iterator.stream (), Sam Pullara disse:

Eu estava trabalhando com Brian para ver como a funcionalidade limite / substream [1] poderia ser implementada e ele sugeriu que a conversão para o Iterator era o caminho certo. Eu tinha pensado nessa solução, mas não encontrei nenhuma maneira óbvia de pegar um iterador e transformá-lo em um fluxo. Acontece que ele está lá, basta converter o iterador em um spliterator e depois converter o spliterator em um fluxo. Portanto, isso me leva a revisitar se devemos pendurar esses itens diretamente no Iterable / Iterator ou em ambos.

Minha sugestão é pelo menos tê-lo no Iterator para que você possa se mover de maneira limpa entre os dois mundos e também seria fácil de descobrir, em vez de precisar fazer:

Streams.stream (Spliterators.spliteratorUnknownSize (iterador, Spliterator.ORDERED))

E então Brian Goetz respondeu :

Acho que o argumento de Sam foi que existem muitas classes de bibliotecas que oferecem um iterador, mas não permitem que você escreva seu próprio spliterator. Então, tudo o que você pode fazer é chamar o fluxo (spliteratorUnknownSize (iterador)). Sam está sugerindo que definamos Iterator.stream () para fazer isso por você.

Eu gostaria de manter os métodos stream () e spliterator () como sendo para escritores de bibliotecas / usuários avançados.

E depois

"Dado que escrever um Spliterator é mais fácil do que escrever um Iterator, eu preferiria apenas escrever um Spliterator em vez de um Iterator (o Iterator tem mais de 90 anos :)"

Você está perdendo o ponto, no entanto. Existem milhões de classes por aí que lhe entregam um iterador. E muitos deles não estão prontos para spliterator.

Discussões anteriores na lista de distribuição do Lambda

Essa pode não ser a resposta que você está procurando, mas na lista de discussão do Project Lambda isso foi discutido brevemente. Talvez isso ajude a promover uma discussão mais ampla sobre o assunto.

Nas palavras de Brian Goetz, sob Streams from Iterable :

Recuando ...

Existem várias maneiras de criar um fluxo. Quanto mais informações você tiver sobre como descrever os elementos, mais funcionalidade e desempenho a biblioteca de fluxos poderá fornecer. Em ordem do mínimo para a maioria das informações, elas são:

Iterador

Iterador + tamanho

Spliterator

Spliterator que sabe seu tamanho

Spliterator que sabe seu tamanho e ainda sabe que todas as subdivisões sabem seu tamanho.

(Alguns podem se surpreender ao descobrir que podemos extrair paralelismo mesmo de um iterador burro nos casos em que Q (trabalho por elemento) não é trivial.)

Se o Iterable tivesse um método stream (), ele apenas envolveria um Iterator com um Spliterator, sem informações de tamanho. Mas, a maioria das coisas que são Iterable fazer tem informações de tamanho. O que significa que estamos fornecendo fluxos deficientes. Isso não é tão bom.

Uma desvantagem da prática de API descrita por Stephen aqui, de aceitar o Iterable em vez de Collection, é que você está forçando as coisas através de um "pequeno canal" e, portanto, descartando informações de tamanho quando isso pode ser útil. Tudo bem se tudo o que você estiver fazendo for forCada vez, mas se você quiser fazer mais, será melhor preservar todas as informações que desejar.

O padrão fornecido pelo Iterable seria realmente ruim - descartaria o tamanho mesmo que a grande maioria dos Iterables conheça essas informações.

Contradição?

Embora, parece que a discussão se baseia nas mudanças que o Grupo de Especialistas fez no design inicial do Streams, que foi inicialmente baseado em iteradores.

Mesmo assim, é interessante notar que em uma interface como Collection, o método stream é definido como:

default Stream<E> stream() {
   return StreamSupport.stream(spliterator(), false);
}

Qual poderia ser exatamente o mesmo código que está sendo usado na interface Iterable.

Então, foi por isso que eu disse que essa resposta provavelmente não é satisfatória, mas ainda é interessante para a discussão.

Evidência de refatoração

Continuando com a análise na lista de correspondência, parece que o método splitIterator estava originalmente na interface Collection e, em algum momento de 2013, eles o moveram para Iterable.

Puxe splitIterator para cima de Collection para Iterable .

Conclusão / Teorias?

Então, é provável que a falta do método no Iterable seja apenas uma omissão, pois parece que eles deveriam ter movido o método stream também quando moveram o splitIterator de Collection para Iterable.

Se houver outras razões, essas não são evidentes. Alguém mais tem outras teorias?

Edwin Dalorzo
fonte
Agradeço sua resposta, mas não concordo com o raciocínio. No momento em que você substituir o spliterator()do Iterable, então todas as questões não são fixos, e você pode trivialmente implementar stream()e parallelStream()..
skiwi
@skiwi Foi por isso que eu disse que essa provavelmente não é a resposta. Estou apenas tentando acrescentar à discussão, porque é difícil saber por que o grupo de especialistas tomou as decisões que tomou. Acho que tudo o que podemos fazer é tentar fazer alguma análise forense na lista de e-mails e ver se conseguimos encontrar algum motivo.
Edwin Dalorzo
1
@skiwi Examinei outras listas de discussão e encontrei mais evidências para a discussão e talvez algumas idéias que ajudem a teorizar algum diagnóstico.
Edwin Dalorzo
Obrigado por seus esforços, eu realmente devo aprender a dividir com eficiência essas listas de discussão. Ajudaria se eles pudessem ser visualizados de alguma maneira ... moderna, como um fórum ou algo assim, porque ler e-mails em texto sem formatação com aspas não é exatamente eficiente.
skiwi
6

Se você souber o tamanho que pode usar, java.util.Collectionque fornece o stream()método:

public class Hand extends AbstractCollection<Card> {
   private final List<Card> list = new ArrayList<>();
   private final int capacity;

   //...

   @Override
   public Iterator<Card> iterator() {
       return list.iterator();
   }

   @Override
   public int size() {
      return list.size();
   }
}

E depois:

new Hand().stream().map(...)

Eu enfrentei o mesmo problema e fiquei surpreso que minha Iterableimplementação pudesse ser facilmente estendida a uma AbstractCollectionimplementação simplesmente adicionando o size()método (felizmente eu tinha o tamanho da coleção :-)

Você também deve considerar substituir Spliterator<E> spliterator().

Udo
fonte