Devo devolver uma coleção ou um fluxo?

163

Suponha que eu tenha um método que retorne uma exibição somente leitura em uma lista de membros:

class Team {
    private List < Player > players = new ArrayList < > ();

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}

Suponha ainda que tudo o que o cliente faz é iterar sobre a lista uma vez, imediatamente. Talvez colocar os jogadores em uma JList ou algo assim. O cliente não armazena uma referência à lista para inspeção posterior!

Dado esse cenário comum, devo retornar um fluxo?

public Stream < Player > getPlayers() {
    return players.stream();
}

Ou o retorno de um fluxo não é idiomático em Java? Os fluxos foram projetados para sempre serem "finalizados" dentro da mesma expressão em que foram criados?

fredoverflow
fonte
12
Definitivamente, não há nada de errado nisso, como um idioma. Afinal, players.stream()é exatamente esse método que retorna um fluxo para o chamador. A verdadeira questão é: você realmente deseja restringir o chamador a uma travessia única e também negar a ele o acesso à sua coleção pela CollectionAPI? Talvez o interlocutor apenas queira fazer addAllisso para outra coleção?
Marko Topolnik
2
Tudo depende. Você sempre pode fazer collection.stream () e Stream.collect (). Portanto, cabe a você e o chamador que usa essa função.
Raja Anbazhagan

Respostas:

222

A resposta é, como sempre, "depende". Depende do tamanho da coleção retornada. Depende se o resultado muda ao longo do tempo e qual a importância da consistência do resultado retornado. E isso depende muito de como o usuário provavelmente usará a resposta.

Primeiro, observe que você sempre pode obter uma coleção de um fluxo e vice-versa:

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

Portanto, a questão é: o que é mais útil para os chamadores?

Se o resultado for infinito, existe apenas uma opção: Stream.

Se o resultado for muito grande, você provavelmente prefere o Stream, pois pode não haver nenhum valor em materializá-lo de uma só vez, e isso pode criar uma pressão significativa no heap.

Se tudo o que o chamador fizer é iterar através dele (pesquisar, filtrar, agregar), você deve preferir o Stream, já que o Stream já os possui e não há necessidade de materializar uma coleção (especialmente se o usuário não puder processar o resultado completo.) Este é um caso muito comum.

Mesmo que você saiba que o usuário irá iterá-lo várias vezes ou mantê-lo por perto, você ainda pode retornar um Stream, pelo simples fato de que qualquer coleção que você escolher colocar (por exemplo, ArrayList) pode não ser a eles desejam e, em seguida, o chamador deve copiá-lo de qualquer maneira. se você retornar um fluxo, ele pode fazer collect(toCollection(factory))e obtê-lo exatamente da forma que deseja.

Os casos acima "preferem Stream" derivam principalmente do fato de o Stream ser mais flexível; você pode vincular tarde a como usá-lo sem incorrer nos custos e restrições de materializá-lo em uma coleção.

O único caso em que você deve devolver uma coleção é quando existem requisitos de consistência fortes e é necessário produzir uma captura instantânea consistente de um destino em movimento. Então, você desejará colocar os elementos em uma coleção que não será alterada.

Então, eu diria que na maioria das vezes, o Stream é a resposta certa - é mais flexível, não impõe custos de materialização geralmente desnecessários e pode ser facilmente transformado na coleção de sua escolha, se necessário. Mas, às vezes, talvez você precise devolver uma coleção (por exemplo, devido a fortes requisitos de consistência), ou talvez queira devolver a coleção porque sabe como o usuário a usará e sabe que isso é a coisa mais conveniente para eles.

Brian Goetz
fonte
6
Como eu disse, existem alguns casos em que ele não voa, como aqueles em que você deseja retornar um instantâneo no tempo de um alvo em movimento, especialmente quando você tem requisitos de consistência fortes. Mas, na maioria das vezes, o Stream parece ser a escolha mais geral, a menos que você saiba algo específico sobre como será usado.
Brian Goetz
8
@ Marko Mesmo se você limitar sua pergunta de maneira tão restrita, ainda discordo de sua conclusão. Talvez você esteja assumindo que a criação de um fluxo é de alguma forma muito mais cara do que agrupar a coleção com um invólucro imutável? (E, mesmo se você não o fizer, a exibição do fluxo que você obtém no wrapper é pior do que a do original; como UnmodifiableList não substitui o spliterator (), você efetivamente perderá todo o paralelismo.) de viés de familiaridade; você conhece a Collection há anos, e isso pode fazer você desconfiar do recém-chegado.
Brian Goetz
5
@MarkoTopolnik Claro. Meu objetivo era abordar a questão geral de design da API, que está se tornando uma FAQ. Em relação ao custo, observe que, se você ainda não possui uma coleção materializada, pode retornar ou agrupar (o OP possui, mas muitas vezes não existe uma), materializar uma coleção no método getter não é mais barato do que retornar um fluxo e permitir o chamador materializa um (e, é claro, a materialização inicial pode ser muito mais cara, se o chamador não precisar ou se você retornar ArrayList, mas o chamador quiser o TreeSet.) isto é.
Brian Goetz
4
@MarkoTopolnik Embora a memória seja um caso de uso muito importante, também existem outros casos com bom suporte à paralelização, como fluxos gerados não ordenados (por exemplo, Stream.generate). No entanto, em que o Streams não é adequado, é o caso de uso reativo, em que os dados chegam com latência aleatória. Para isso, eu sugeriria o RxJava.
Brian Goetz
4
@MarkoTopolnik Não acho que discordemos, exceto talvez você tenha gostado de concentrar nossos esforços de maneira um pouco diferente. (Estamos acostumados a isso; não podemos deixar todas as pessoas felizes.) O centro de design do Streams focava em estruturas de dados na memória; o centro de design do RxJava concentra-se em eventos gerados externamente. Ambas são boas bibliotecas; os dois também não se saem muito bem quando você tenta aplicá-los a casos bem fora do centro de design. Mas apenas porque um martelo é uma ferramenta terrível para bordar, isso não sugere que haja algo errado com o martelo.
27730 Brian Brian Goetz
63

Tenho alguns pontos a acrescentar à excelente resposta de Brian Goetz .

É bastante comum retornar um fluxo de uma chamada de método no estilo "getter". Consulte a página de uso do Stream no javadoc do Java 8 e procure "métodos ... que retornam o Stream" para outros pacotes que não java.util.Stream. Esses métodos geralmente são em classes que representam ou podem conter vários valores ou agregações de algo. Nesses casos, as APIs normalmente retornam coleções ou matrizes delas. Por todas as razões que Brian observou em sua resposta, é muito flexível adicionar métodos de retorno de fluxo aqui. Muitas dessas classes já têm métodos de retorno de coleções ou matrizes, porque as classes são anteriores à API do Streams. Se você estiver projetando uma nova API e fizer sentido fornecer métodos de retorno de fluxo, talvez não seja necessário adicionar métodos de retorno de coleção também.

Brian mencionou o custo de "materializar" os valores em uma coleção. Para ampliar esse ponto, existem realmente dois custos aqui: o custo de armazenar valores na coleção (alocação e cópia de memória) e também o custo de criar os valores em primeiro lugar. O último custo geralmente pode ser reduzido ou evitado tirando proveito do comportamento de busca de preguiça de um Stream. Um bom exemplo disso são as APIs em java.nio.file.Files:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

Não apenas readAllLinesprecisa armazenar todo o conteúdo do arquivo na memória para armazená-lo na lista de resultados, mas também ler o arquivo até o final antes de retornar a lista. O linesmétodo pode retornar quase imediatamente após a execução de alguma configuração, deixando a leitura do arquivo e a quebra de linha até mais tarde quando for necessário - ou não for o caso. Esse é um grande benefício, se, por exemplo, o chamador estiver interessado apenas nas dez primeiras linhas:

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

É claro que pode ser economizado um espaço considerável na memória se o chamador filtrar o fluxo para retornar apenas linhas correspondentes a um padrão, etc.

Um idioma que parece estar surgindo é nomear métodos de retorno de fluxo após o plural do nome das coisas que ele representa ou contém, sem um getprefixo. Além disso, embora stream()seja um nome razoável para um método de retorno de fluxo quando houver apenas um conjunto possível de valores a serem retornados, às vezes há classes que possuem agregações de vários tipos de valores. Por exemplo, suponha que você tenha algum objeto que contenha atributos e elementos. Você pode fornecer duas APIs de retorno de fluxo:

Stream<Attribute>  attributes();
Stream<Element>    elements();
Stuart Marks
fonte
3
Ótimos pontos. Você pode dizer mais sobre onde está vendo esse idioma de nomenclatura surgindo e quanta tração (vapor?) Está aumentando? Gosto da ideia de uma convenção de nomenclatura que torna óbvio que você está recebendo um fluxo versus uma coleção - embora eu também espere que a conclusão do IDE no "get" me diga o que posso obter.
Joshua Goldberg
1
Eu também estou muito interessado nisso idioma nomeação
eleitos
5
@JoshuaGoldberg O JDK parece ter adotado esse idioma de nomeação, embora não exclusivamente. Considere: CharSequence.chars () e .codePoints (), BufferedReader.lines () e Files.lines () existiam no Java 8. No Java 9, foram adicionados os seguintes: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Outros métodos de retorno de fluxo foram adicionados que não usam esse idioma - Scanner.findAll () vem à mente - mas o idioma substantivo plural parece ter entrado em uso justo no JDK.
Stuart Marks
1

Os fluxos foram projetados para sempre serem "finalizados" dentro da mesma expressão em que foram criados?

É assim que eles são usados ​​na maioria dos exemplos.

Nota: retornar um fluxo não é tão diferente de retornar um iterador (admitido com muito mais poder expressivo)

IMHO, a melhor solução é encapsular por que você está fazendo isso e não devolver a coleção.

por exemplo

public int playerCount();
public Player player(int n);

ou se você pretende contá-los

public int countPlayersWho(Predicate<? super Player> test);
Peter Lawrey
fonte
2
O problema com esta resposta é que exigiria que o autor antecipasse todas as ações que o cliente deseja executar e aumentaria bastante o número de métodos na classe.
22414 dkatzel #
@dkatzel Depende se o usuário final é o autor ou alguém com quem trabalha. Se os usuários finais são desconhecidos, você precisa de uma solução mais geral. Você ainda pode querer limitar o acesso à coleção subjacente.
12114 Peter Lawrey
1

Se o fluxo for finito e houver uma operação normal / esperada nos objetos retornados que gerará uma exceção verificada, sempre retornarei uma coleção. Porque se você estiver fazendo algo em cada um dos objetos que pode lançar uma exceção de verificação, você odiará o fluxo. Uma falta real de fluxos é a incapacidade de lidar com exceções verificadas de maneira elegante.

Agora, talvez isso seja um sinal de que você não precisa das exceções verificadas, o que é justo, mas às vezes elas são inevitáveis.

designbygravity
fonte
1

Ao contrário das coleções, os fluxos têm características adicionais . Um fluxo retornado por qualquer método pode ser:

  • finito ou infinito
  • paralelo ou sequencial (com um conjunto de encadeamentos compartilhado globalmente padrão que pode impactar qualquer outra parte de um aplicativo)
  • encomendado ou não

Essas diferenças também existem nas coleções, mas são parte do contrato óbvio:

  • Todas as coleções têm tamanho, Iterator / Iterable pode ser infinito.
  • As coleções são explicitamente ordenadas ou não
  • Felizmente, paralelismo não é algo que a coleção se preocupa além da segurança de threads.

Como consumidor de um fluxo (a partir de um retorno de método ou como um parâmetro de método), essa é uma situação perigosa e confusa. Para garantir que seu algoritmo se comporte corretamente, os consumidores de fluxos precisam garantir que o algoritmo não faça suposições erradas sobre as características do fluxo. E isso é uma coisa muito difícil de fazer. Nos testes de unidade, isso significa que você deve multiplicar todos os seus testes para serem repetidos com o mesmo conteúdo de fluxo, mas com fluxos que sejam

  • (finito, ordenado, seqüencial)
  • (finito, ordenado, paralelo)
  • (finito, não ordenado, sequencial) ...

Protetores de método de escrita para fluxos que lançam uma IllegalArgumentException se o fluxo de entrada tiver características que quebram o algoritmo, é difícil, porque as propriedades estão ocultas.

Isso deixa o Stream apenas como uma opção válida em uma assinatura de método quando nenhum dos problemas acima importa, o que raramente é o caso.

É muito mais seguro usar outros tipos de dados nas assinaturas de métodos com um contrato explícito (e sem o processamento implícito do conjunto de encadeamentos) que impossibilita o processamento acidental de dados com suposições erradas sobre ordem, tamanho ou paralelismo (e uso do conjunto de encadeamentos).

tkruse
fonte
2
Suas preocupações com fluxos infinitos são infundadas; a pergunta é "devo retornar uma coleção ou um fluxo". Se a Coleção for uma possibilidade, o resultado será, por definição, finito. Portanto, as preocupações de que os chamadores arriscariam uma iteração infinita, já que você poderia ter retornado uma coleção , são infundadas. O restante dos conselhos nesta resposta é apenas ruim. Parece-me que você encontrou alguém que usou demais o Stream e está girando demais na outra direção. Conselhos compreensíveis, mas ruins.
Brian Goetz
0

Eu acho que depende do seu cenário. Pode ser que, se você fizer seu Teamimplemento Iterable<Player>, é suficiente.

for (Player player : team) {
    System.out.println(player);
}

ou no estilo funcional:

team.forEach(System.out::println);

Mas se você quiser uma API mais completa e fluente, um fluxo pode ser uma boa solução.

gontard
fonte
Observe que, no código publicado pelo OP, a contagem de jogadores é quase inútil, exceto como estimativa ('1034 jogadores jogando agora, clique aqui para começar!') Isso ocorre porque você está retornando uma visão imutável de uma coleção mutável , portanto, a contagem que você obtém agora pode não ser igual à contagem de três microssegundos a partir de agora. Portanto, ao devolver uma coleção, é possível encontrar uma maneira "fácil" de chegar à contagem (e também stream.count()é muito fácil), esse número não é realmente muito significativo para outra coisa senão a depuração ou estimativa.
27530 Brian Iretz
0

Enquanto alguns dos entrevistados mais destacados deram ótimos conselhos gerais, estou surpreso que ninguém tenha afirmado:

Se você já possui um "materializado" Collectionem mãos (ou seja, ele já foi criado antes da chamada - como é o caso no exemplo dado, onde é um campo de membro), não faz sentido convertê-lo para a Stream. O chamador pode facilmente fazer isso sozinho. Visto que, se o chamador quiser consumir os dados em sua forma original, você os converterá em um Streamque os obriga a fazer um trabalho redundante para rematerializar uma cópia da estrutura original.

Daniel Avery
fonte
-1

Talvez uma fábrica de Stream fosse uma escolha melhor. A grande vantagem de apenas expor coleções por meio do Stream é que ele encapsula melhor a estrutura de dados do seu modelo de domínio. É impossível para qualquer uso de suas classes de domínio afetar o funcionamento interno de sua Lista ou Conjunto simplesmente expondo um Stream.

Também incentiva os usuários da sua classe de domínio a escrever código em um estilo Java 8 mais moderno. É possível refatorar gradualmente esse estilo mantendo seus getters existentes e adicionando novos getters com retorno de fluxo. Com o tempo, você pode reescrever seu código legado até finalmente excluir todos os getters que retornam uma Lista ou Conjunto. Esse tipo de refatoração é muito bom depois que você limpa todo o código legado!

Vazgen Torosyan
fonte
7
existe uma razão para isso ser totalmente citado? existe uma fonte?
Xerus
-5

Eu provavelmente teria 2 métodos, um para retornar um Collectione outro para retornar a coleção como a Stream.

class Team
{
    private List<Player> players = new ArrayList<>();

// ...

    public List<Player> getPlayers()
    {
        return Collections.unmodifiableList(players);
    }

    public Stream<Player> getPlayerStream()
    {
        return players.stream();
    }

}

Esse é o melhor de ambos mundos. O cliente pode escolher se deseja a lista ou o fluxo e não precisa criar objetos extras para fazer uma cópia imutável da lista apenas para obter um fluxo.

Isso também adiciona apenas mais 1 método à sua API, para que você não tenha muitos métodos

dkatzel
fonte
1
Porque ele queria escolher entre essas duas opções e perguntou os prós e contras de cada uma. Além disso, fornece a todos uma melhor compreensão desses conceitos.
Libert Piou Piou
Por favor, não faça isso. Imagine as APIs!
François Gautier