¿Debo devolver una Colección o una Transmisión?

163

Supongamos que tengo un método que devuelve una vista de solo lectura en una lista de miembros:

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

    // ...

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

Además, suponga que todo lo que hace el cliente es iterar sobre la lista una vez, inmediatamente. Tal vez para poner a los jugadores en una JList o algo así. ¡El cliente no almacena una referencia a la lista para su posterior inspección!

Dado este escenario común, ¿debería devolver una transmisión en su lugar?

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

¿O está devolviendo un flujo no idiomático en Java? ¿Se diseñaron las secuencias para que siempre se "terminaran" dentro de la misma expresión en que se crearon?

flujo libre
fuente
12
Definitivamente no hay nada de malo en esto como idioma. Después de todo, players.stream()es un método que devuelve una secuencia a la persona que llama. La verdadera pregunta es, ¿realmente desea restringir al llamante a un recorrido único y también negarle el acceso a su colección a través de la CollectionAPI? ¿Quizás la persona que llama solo quiere ir addAlla otra colección?
Marko Topolnik
2
Todo depende. Siempre puedes hacer collection.stream () así como Stream.collect (). Entonces depende de usted y de la persona que llama que usa esa función.
Raja Anbazhagan

Respuestas:

222

La respuesta es, como siempre, "depende". Depende de qué tan grande será la colección devuelta. Depende de si el resultado cambia con el tiempo y de cuán importante es la consistencia del resultado devuelto. Y depende mucho de cómo es probable que el usuario use la respuesta.

Primero, tenga en cuenta que siempre puede obtener una Colección de un Stream, y viceversa:

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

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

Entonces la pregunta es, ¿cuál es más útil para las personas que llaman?

Si su resultado puede ser infinito, solo hay una opción: Stream.

Si su resultado puede ser muy grande, probablemente prefiera Stream, ya que puede que no tenga ningún valor materializarlo todo de una vez, y hacerlo podría generar una gran presión de almacenamiento dinámico.

Si todo lo que va a hacer la persona que llama es iterar a través de él (buscar, filtrar, agregar), debe preferir Stream, ya que Stream ya tiene estos incorporados y no hay necesidad de materializar una colección (especialmente si el usuario no puede procesar el Stream resultado completo.) Este es un caso muy común.

Incluso si sabe que el usuario lo repetirá varias veces o lo mantendrá, puede que desee devolver un Stream en su lugar, por el simple hecho de que cualquier Colección que elija para colocarla (por ejemplo, ArrayList) puede no ser la la forma que desean, y la persona que llama tiene que copiarla de todos modos. si devuelve una transmisión, pueden hacerlo collect(toCollection(factory))y obtenerla exactamente de la forma que deseen.

Los casos anteriores de "preferir Stream" se derivan principalmente del hecho de que Stream es más flexible; puede vincularse tarde a cómo lo usa sin incurrir en los costos y las limitaciones de materializarlo en una Colección.

El único caso en el que debe devolver una Colección es cuando existen requisitos de consistencia sólidos y tiene que producir una instantánea consistente de un objetivo en movimiento. Entonces, querrás poner los elementos en una colección que no cambiará.

Entonces, diría que la mayoría de las veces, Stream es la respuesta correcta: es más flexible, no impone costos de materialización usualmente innecesarios y puede convertirse fácilmente en la Colección de su elección si es necesario. Pero a veces, puede que tenga que devolver una Colección (por ejemplo, debido a los requisitos de coherencia), o puede que desee devolver la Colección porque sabe cómo la usará el usuario y sabe que esto es lo más conveniente para ellos.

Brian Goetz
fuente
66
Como dije, hay algunos casos en los que no vuela, como aquellos en los que desea devolver una instantánea a tiempo de un objetivo en movimiento, especialmente cuando tiene requisitos de consistencia sólidos. Pero la mayoría de las veces, Stream parece la opción más general, a menos que sepa algo específico sobre cómo se usará.
Brian Goetz
8
@Marko Incluso si limita su pregunta de manera tan limitada, todavía no estoy de acuerdo con su conclusión. ¿Quizás esté asumiendo que crear un Stream es de alguna manera mucho más costoso que envolver la colección con un contenedor inmutable? (E, incluso si no lo hace, la vista de flujo que obtiene en el contenedor es peor que la que obtiene del original; debido a que UnmodifiableList no anula spliterator (), efectivamente perderá todo paralelismo). de sesgo de familiaridad; Conoces Collection desde hace años, y eso puede hacerte desconfiar del recién llegado.
Brian Goetz
55
@MarkoTopolnik Claro. Mi objetivo era abordar la pregunta general de diseño de API, que se está convirtiendo en una pregunta frecuente. Con respecto al costo, tenga en cuenta que, si aún no tiene una colección materializada, puede devolverla o envolverla (OP sí, pero a menudo no hay una), materializar una colección en el método getter no es más barato que devolver una secuencia y dejar la persona que llama materializa una (y, por supuesto, la materialización temprana podría ser mucho más costosa, si la persona que llama no la necesita o si devuelve ArrayList pero la persona que llama quiere TreeSet). Pero Stream es nuevo, y la gente a menudo asume que es más costoso que es.
Brian Goetz
44
@MarkoTopolnik Si bien la memoria es un caso de uso muy importante, también hay otros casos que tienen un buen soporte de paralelización, como las secuencias generadas no ordenadas (por ejemplo, Stream.generate). Sin embargo, donde Streams no se ajusta bien es el caso de uso reactivo, donde los datos llegan con latencia aleatoria. Para eso, sugeriría RxJava.
Brian Goetz
44
@MarkoTopolnik No creo que estemos en desacuerdo, excepto quizás que le haya gustado que centremos nuestros esfuerzos de manera ligeramente diferente. (Estamos acostumbrados a esto; no podemos hacer felices a todas las personas). El centro de diseño de Streams se centró en estructuras de datos en memoria; El centro de diseño de RxJava se centra en eventos generados externamente. Ambas son buenas bibliotecas; Además, a ambos no les va muy bien cuando intentas aplicarlos a casos que están fuera de su centro de diseño. Pero solo porque un martillo es una herramienta terrible para el uso de agujas, eso no sugiere que haya algo malo con el martillo.
Brian Goetz
63

Tengo algunos puntos que añadir a la excelente respuesta de Brian Goetz .

Es bastante común devolver un Stream desde una llamada de método de estilo "getter". Consulte la página de uso de Stream en Java 8 javadoc y busque "métodos ... que devuelvan Stream" para los paquetes que no sean java.util.Stream. Estos métodos suelen estar en clases que representan o pueden contener múltiples valores o agregaciones de algo. En tales casos, las API generalmente han devuelto colecciones o matrices de ellas. Por todas las razones que Brian señaló en su respuesta, es muy flexible agregar aquí los métodos de retorno de Stream. Muchas de estas clases ya tienen métodos de devolución de colecciones o matrices, porque las clases son anteriores a la API de Streams. Si está diseñando una nueva API y tiene sentido proporcionar métodos de devolución de Stream, puede que no sea necesario agregar también métodos de devolución de colección.

Brian mencionó el costo de "materializar" los valores en una colección. Para ampliar este punto, en realidad hay dos costos aquí: el costo de almacenar valores en la colección (asignación de memoria y copia) y también el costo de crear los valores en primer lugar. El último costo a menudo se puede reducir o evitar aprovechando el comportamiento de búsqueda de pereza de Stream. Un buen ejemplo de esto son las API en java.nio.file.Files:

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

No solo readAllLinestiene que mantener todo el contenido del archivo en la memoria para almacenarlo en la lista de resultados, sino que también tiene que leer el archivo hasta el final antes de que devuelva la lista. El linesmétodo puede regresar casi inmediatamente después de haber realizado alguna configuración, dejando la lectura del archivo y el salto de línea hasta más tarde, cuando sea necesario, o nada en absoluto. Este es un gran beneficio si, por ejemplo, la persona que llama solo está interesada en las primeras diez líneas:

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

Por supuesto, se puede ahorrar un considerable espacio de memoria si la persona que llama filtra la secuencia para devolver solo líneas que coincidan con un patrón, etc.

Un modismo que parece estar surgiendo es nombrar métodos de retorno de flujo después del plural del nombre de las cosas que representa o contiene, sin un getprefijo. Además, si bien stream()es un nombre razonable para un método de retorno de flujo cuando solo hay un conjunto posible de valores para devolver, a veces hay clases que tienen agregaciones de múltiples tipos de valores. Por ejemplo, suponga que tiene algún objeto que contiene atributos y elementos. Puede proporcionar dos API de retorno de flujo:

Stream<Attribute>  attributes();
Stream<Element>    elements();
Stuart Marks
fuente
3
Grandes puntos ¿Puedes decir más acerca de dónde estás viendo surgir ese idioma de nombres y cuánta tracción (vapor?) Está acumulando? Me gusta la idea de una convención de nomenclatura que haga obvio que está obteniendo una transmisión frente a una colección, aunque a menudo también espero que la finalización del IDE en "get" me diga qué puedo obtener.
Joshua Goldberg
1
También estoy muy interesado en ese lenguaje de nombres
elija el
55
@JoshuaGoldberg El JDK parece haber adoptado este lenguaje de nombres, aunque no exclusivamente. Considere: CharSequence.chars () y .codePoints (), BufferedReader.lines () y Files.lines () existieron en Java 8. En Java 9, se han agregado los siguientes: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Se han agregado otros métodos de retorno de flujo que no usan este modismo, me viene a la mente Scanner.findAll (), pero el modismo de sustantivo plural parece haber tenido un uso justo en el JDK.
Stuart Marks
1

¿Se diseñaron las secuencias para que siempre se "terminaran" dentro de la misma expresión en la que se crearon?

Así es como se usan en la mayoría de los ejemplos.

Nota: devolver un Stream no es tan diferente a devolver un Iterator (admitido con mucho más poder expresivo)

En mi humilde opinión, la mejor solución es encapsular por qué está haciendo esto, y no devolver la colección.

p.ej

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

o si tienes la intención de contarlos

public int countPlayersWho(Predicate<? super Player> test);
Peter Lawrey
fuente
2
El problema con esta respuesta es que requeriría que el autor anticipe cada acción que el cliente quiera hacer y aumentaría en gran medida el número de métodos en la clase.
dkatzel
@dkatzel Depende de si los usuarios finales son el autor o alguien con quien trabajan. Si los usuarios finales son desconocidos, entonces necesita una solución más general. Es posible que aún desee limitar el acceso a la colección subyacente.
Peter Lawrey
1

Si la secuencia es finita y hay una operación esperada / normal en los objetos devueltos que arrojará una excepción marcada, siempre devuelvo una Colección. Porque si va a hacer algo en cada uno de los objetos que puede lanzar una excepción de cheque, odiará la transmisión. Una falta real con las transmisiones es la incapacidad de manejar las excepciones comprobadas con elegancia.

Ahora, tal vez eso sea una señal de que no necesita las excepciones marcadas, lo cual es justo, pero a veces son inevitables.

designbygravity
fuente
1

A diferencia de las colecciones, las secuencias tienen características adicionales . Una secuencia devuelta por cualquier método podría ser:

  • finito o infinito
  • paralelo o secuencial (con un conjunto de subprocesos compartido globalmente predeterminado que puede afectar a cualquier otra parte de una aplicación)
  • ordenado o no ordenado

Estas diferencias también existen en las colecciones, pero allí forman parte del contrato obvio:

  • Todas las colecciones tienen tamaño, Iterator / Iterable puede ser infinito.
  • Las colecciones están explícitamente ordenadas o no ordenadas
  • Afortunadamente, la paralelismo no es algo que a la colección le interese más allá de la seguridad del hilo.

Como consumidor de una secuencia (ya sea desde un retorno de método o como un parámetro de método), esta es una situación peligrosa y confusa. Para asegurarse de que su algoritmo se comporta correctamente, los consumidores de flujos necesitan asegurarse de que el algoritmo no asuma erróneamente las características del flujo. Y eso es algo muy difícil de hacer. En las pruebas unitarias, eso significaría que debe multiplicar todas sus pruebas para que se repitan con el mismo contenido de flujo, pero con flujos que son

  • (finito, ordenado, secuencial)
  • (finito, ordenado, paralelo)
  • (finito, no ordenado, secuencial) ...

El método de protección para secuencias que arrojan una IllegalArgumentException si la secuencia de entrada tiene características que rompen su algoritmo es difícil, porque las propiedades están ocultas.

Eso deja a Stream solo como una opción válida en la firma de un método cuando ninguno de los problemas anteriores es importante, lo que rara vez es el caso.

Es mucho más seguro usar otros tipos de datos en firmas de métodos con un contrato explícito (y sin el procesamiento implícito de la agrupación de subprocesos) que hace que sea imposible procesar accidentalmente datos con suposiciones erróneas sobre el orden, el tamaño o la paralelismo (y el uso de la agrupación de subprocesos).

tkruse
fuente
2
Sus preocupaciones sobre las corrientes infinitas son infundadas; la pregunta es "¿debo devolver una colección o una secuencia". Si la Colección es una posibilidad, el resultado es por definición finito. Por lo tanto, las preocupaciones de que las personas que llaman arriesgarían una iteración infinita, dado que podría haber devuelto una colección , son infundadas. El resto del consejo en esta respuesta es simplemente malo. Me parece que te topaste con alguien que usó Stream en exceso y que estás girando en exceso en la otra dirección. Comprensible, pero un mal consejo.
Brian Goetz el
0

Creo que depende de tu escenario. Puede ser, si hace su Teamimplemento Iterable<Player>, es suficiente.

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

o en un estilo funcional:

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

Pero si desea una API más completa y fluida, una transmisión podría ser una buena solución.

gontard
fuente
Tenga en cuenta que, en el código publicado por el OP, el recuento de jugadores es casi inútil, aparte de una estimación ('¡1034 jugadores jugando ahora, haga clic aquí para comenzar!') Esto se debe a que está devolviendo una vista inmutable de una colección mutable , por lo que el recuento que obtiene ahora puede no ser igual al recuento dentro de tres microsegundos a partir de ahora. Entonces, si bien devolver una Colección le brinda una forma "fácil" de llegar al conteo (y realmente, también stream.count()es bastante fácil), ese número no es realmente muy significativo para otra cosa que no sea la depuración o la estimación.
Brian Goetz
0

Si bien algunos de los encuestados de más alto perfil dieron excelentes consejos generales, me sorprende que nadie haya declarado:

Si ya tiene un "materializado" Collectionen la mano (es decir, ya se creó antes de la llamada, como es el caso en el ejemplo dado, donde es un campo miembro), no tiene sentido convertirlo en a Stream. La persona que llama puede hacerlo fácilmente por sí misma. Mientras que, si la persona que llama desea consumir los datos en su forma original, si los convierte en una, los Streamobliga a realizar un trabajo redundante para volver a materializar una copia de la estructura original.

Daniel Avery
fuente
-1

Quizás una fábrica de Stream sería una mejor opción. La gran victoria de solo exponer colecciones a través de Stream es que encapsula mejor la estructura de datos de su modelo de dominio. Es imposible que cualquier uso de sus clases de dominio afecte el funcionamiento interno de su Lista o Conjunto simplemente exponiendo un Stream.

También alienta a los usuarios de su clase de dominio a escribir código en un estilo Java 8 más moderno. Es posible refactorizar gradualmente a este estilo manteniendo sus captadores existentes y agregando nuevos captadores que regresan Stream. Con el tiempo, puede volver a escribir su código heredado hasta que finalmente haya eliminado todos los captadores que devuelven una Lista o Conjunto. ¡Este tipo de refactorización se siente realmente bien una vez que haya borrado todo el código heredado!

Vazgen Torosyan
fuente
77
¿Hay alguna razón para que esto se cite por completo? hay una fuente?
Xerus
-5

Probablemente tendría 2 métodos, uno para devolver ay Collectionotro para devolver la colección 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();
    }

}

Esto es lo mejor de ambos mundos. El cliente puede elegir si quiere la Lista o la Transmisión y no tiene que hacer la creación de objetos adicionales para hacer una copia inmutable de la lista solo para obtener una Transmisión.

Esto también solo agrega 1 método más a su API para que no tenga demasiados métodos

dkatzel
fuente
1
Porque quería elegir entre estas dos opciones y preguntó los pros y los contras de cada una. Además, proporciona a todos una mejor comprensión de estos conceptos.
Libert Piou Piou
Por favor no hagas eso. ¡Imagina las API!
François Gautier