¿Debo devolver una Colección o un Stream?

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

class Team { private List players = new ArrayList(); // ... public List getPlayers() { return Collections.unmodifiableList(players); } } 

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

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

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

¿O está devolviendo una stream no idiomática en Java? ¿Las transmisiones están diseñadas para estar siempre “terminadas” dentro de la misma expresión en la que fueron creadas?

La respuesta es, como siempre, “depende”. Depende de cuán 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 el usuario pueda usar 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 c = getFooStream().collect(toList()); 

Entonces la pregunta es, ¿cuál es más útil para quienes 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 no haber ningún valor en materializarlo todo a la vez, y hacerlo podría crear una gran presión en el montón.

Si lo único que va a hacer la persona que llama es recorrerlo (buscar, filtrar, agregar), debería preferir Stream, ya que Stream ya tiene estos incorporados y no es necesario materializar una colección (especialmente si el usuario no puede procesar la 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, por el simple hecho de que cualquiera que sea la Colección en la que decida colocarlo (por ejemplo, ArrayList) puede no ser el forma que desean, y luego la persona que llama tiene que copiarlo de todos modos. si devuelve una secuencia, pueden collect(toCollection(factory)) y obtenerla exactamente en la forma que desean.

Los casos anteriores “preferir Stream” 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 restricciones de materializarlo en una Colección.

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

Así que 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, es posible que tenga que devolver una Colección (por ejemplo, debido a los fuertes requisitos de coherencia) o que desee devolver la Colección porque sabe cómo la usará el usuario y sabe que esto es lo más conveniente para él.

Tengo algunos puntos para agregar 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 generalmente están en clases que representan o pueden contener múltiples valores o agregaciones de algo. En tales casos, las API generalmente tienen colecciones devueltas o matrices de ellas. Por todas las razones que Brian mencionó en su respuesta, es muy flexible agregar aquí métodos de devolución de flujo. Muchas de estas clases ya tienen colecciones o métodos de devolución de matriz, porque las clases son anteriores a la API de Streams. Si está diseñando una API nueva y tiene sentido proporcionar métodos de devolución de flujo, es posible que tampoco sea necesario agregar métodos de devolución de colecciones.

Brian mencionó el costo de “materializar” los valores en una colección. Para amplificar 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. Este último costo a menudo se puede reducir o evitar aprovechando el comportamiento de búsqueda de la pereza de Stream. Un buen ejemplo de esto son las API en java.nio.file.Files :

 static Stream lines(path) static List readAllLines(path) 

No solo readAllLines tiene que mantener todo el contenido del archivo en la memoria para almacenarlo en la lista de resultados, sino que también debe leer el archivo hasta el final antes de devolver la lista. El método de lines 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 no lo haga en absoluto. Este es un gran beneficio, si, por ejemplo, la persona que llama está interesada únicamente en las primeras diez líneas:

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

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

Una expresión 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 prefijo get . Además, aunque stream() es un nombre razonable para un método de retorno de flujo cuando solo se puede devolver un conjunto posible de valores, a veces hay clases que tienen agregaciones de varios tipos de valores. Por ejemplo, supongamos que tiene algún objeto que contiene atributos y elementos. Puede proporcionar dos API de retorno de flujo:

 Stream attributes(); Stream elements(); 

En un contexto de clases de equipo / empresa / reutilizables:

En general, diseñe las clases sin tener en cuenta lo que los clientes pueden asumir sobre la clase. Si devuelve un flujo, devuelve una lista potencialmente infinita, y los clientes deberían tratar esto como una lista potencialmente infinita. Es una violación de la seguridad del código y el buen gusto para cualquier otra clase u otro método ejecutar stream.collect(toList()) en una secuencia que no se creó a sí mismo, asumiendo ciegamente que será finito. Si ese fuera un patrón de código deseable, las transmisiones deberían tener algún método como isFinite() para evitar suposiciones erróneas.

Lo que es peor, devolver un Stream para evitar la materialización es igualmente malo, porque parece demasiado fácil materializar un flujo con .collect() , lo que significa que mientras la clase servidora evita la materialización, la clase cliente puede tener la tentación de hacerlo a un alto costo, en en particular, si devuelve transmisiones todo el tiempo como un estilo de código, y no puede decir más qué retornos de flujo deben materializarse y cuáles no. Las interfaces clásicas que insinúan que la materialización es mala es java.util.Iterator (aunque commons-lang define IteratorUtils.toList ()).

Por lo tanto, recomendaría utilizar cualquier método de devolución que no sea de recolección solo cuando desee documentar a sus clientes que el resultado puede ser infinito o no ser adecuado para la materialización.

Que los flujos sean más convenientes para la progtwigción funcional no significa que deban preferirse, es decir, el JDK debería extender su API de recostackción para permitir las mismas operaciones, de modo que los clientes no tengan que llamar a collection.stream().... .

Para asignaciones de tarea, proyectos estrictamente personales u otro código no reutilizado

Desde una mera perspectiva algorítmica, al ignorar el estilo de código, la legibilidad o las preocupaciones de OO-Design, es preferible preferir Streams como en la respuesta aceptada. Pero no veo cómo se puede hacer una recomendación general sobre StackOverflow

¿Las transmisiones están diseñadas para estar siempre “terminadas” dentro de la misma expresión en la que fueron creadas?

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 resumir por qué estás haciendo esto y no devolver la colección.

p.ej

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

o si tiene la intención de contarlos

 public int countPlayersWho(Predicate test); 

Si el flujo es finito y hay una operación esperada / normal en los objetos devueltos que emitirá una excepción marcada, siempre devuelvo una Colección. Porque si va a hacer algo en cada uno de los objetos que pueden arrojar una excepción de verificación, odiará la transmisión. Una verdadera falta con las transmisiones: la incapacidad de lidiar con las excepciones comprobadas con elegancia.

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

Creo que depende de tu situación. Puede ser, si haces que tu Team implemente Iterable , es suficiente.

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

o en el estilo funcional a:

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

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

Tal vez 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 el uso de las clases de dominio afecte el funcionamiento interno de su Lista o Conjunto simplemente al exponer una Secuencia.

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 incrementalmente a este estilo conservando sus captadores existentes y agregando nuevos captadores que devuelven 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 hayas eliminado todo el código heredado!

Probablemente tenga 2 métodos, uno para devolver una Collection y otro para devolver la colección como un Stream .

 class Team { private List players = new ArrayList<>(); // ... public List getPlayers() { return Collections.unmodifiableList(players); } public Stream getPlayerStream() { return players.stream(); } } 

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

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