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?
java
collections
java-8
encapsulation
java-stream
flujo libre
fuente
fuente
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 laCollection
API? ¿Quizás la persona que llama solo quiere iraddAll
a otra colección?Respuestas:
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:
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.
fuente
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
: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 tiene que leer el archivo hasta el final antes de que devuelva la lista. Ellines
mé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: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
get
prefijo. Además, si bienstream()
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:fuente
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
o si tienes la intención de contarlos
fuente
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.
fuente
A diferencia de las colecciones, las secuencias tienen características adicionales . Una secuencia devuelta por cualquier método podría ser:
Estas diferencias también existen en las colecciones, pero allí forman parte del contrato obvio:
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
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).
fuente
Creo que depende de tu escenario. Puede ser, si hace su
Team
implementoIterable<Player>
, es suficiente.o en un estilo funcional:
Pero si desea una API más completa y fluida, una transmisión podría ser una buena solución.
fuente
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.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"
Collection
en 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 aStream
. 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, losStream
obliga a realizar un trabajo redundante para volver a materializar una copia de la estructura original.fuente
fuente
Probablemente tendría 2 métodos, uno para devolver ay
Collection
otro para devolver la colección como aStream
.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
fuente