Diferencia entre las secuencias de Java 8 y los observables de RxJava

144

¿Las secuencias de Java 8 son similares a las observables de RxJava?

Definición de flujo de Java 8:

Las clases en el nuevo java.util.streampaquete proporcionan una API Stream para admitir operaciones de estilo funcional en secuencias de elementos.

rahulrv
fuente
8
Para su información, hay propuestas para introducir más clases como RxJava en JDK 9. jsr166-concurrency.10961.n7.nabble.com/…
John Vint
@JohnVint ¿Cuál es el estado de esta propuesta? ¿Realmente tomará vuelo?
IgorGanapolsky
2
@IgorGanapolsky Oh, sí, definitivamente parece que llegará a jdk9. cr.openjdk.java.net/~martin/webrevs/openjdk9/… . Incluso hay un puerto para RxJava a Flow github.com/akarnokd/RxJavaUtilConcurrentFlow .
John Vint
Sé que esta es una pregunta muy antigua, pero recientemente asistí a esta gran charla de Venkat Subramaniam que tiene una visión perspicaz sobre el tema y se actualiza a Java9: youtube.com/watch?v=kfSSKM9y_0E . Podría ser interesante para las personas que profundizan en RxJava.
Pedro

Respuestas:

152

TL; DR : todas las bibliotecas de procesamiento de secuencia / secuencia ofrecen API muy similar para la construcción de tuberías. Las diferencias están en la API para el manejo de subprocesos múltiples y la composición de tuberías.

RxJava es bastante diferente de Stream. De todas las cosas de JDK, la más cercana a rx.Observable es quizás java.util.stream.Collector Stream + CompletableFuture combo (que tiene un costo de tratar con una capa adicional de mónada, es decir, tener que manejar la conversión entre Stream<CompletableFuture<T>>y CompletableFuture<Stream<T>>).

Existen diferencias significativas entre Observable y Stream:

  • Los flujos están basados ​​en pull, los observables están basados ​​en push. Esto puede sonar demasiado abstracto, pero tiene consecuencias significativas que son muy concretas.
  • Stream solo se puede usar una vez, Observable se puede suscribir muchas veces
  • Stream#parallel()divide la secuencia en particiones, Observable#subscribeOn()y Observable#observeOn()no lo hace; es difícil emular el Stream#parallel()comportamiento con Observable, una vez tuvo un .parallel()método, pero este método causó tanta confusión que el .parallel()soporte se movió a un repositorio separado en github, RxJavaParallel. Más detalles están en otra respuesta .
  • Stream#parallel()no permite especificar un grupo de subprocesos para usar, a diferencia de la mayoría de los métodos de RxJava que aceptan el Programador opcional. Dado que todas las instancias de transmisión en una JVM usan el mismo grupo de bifurcación, la adición .parallel()puede afectar accidentalmente el comportamiento en otro módulo de su programa
  • Las transmisiones carecen de operaciones relacionadas con el tiempo como Observable#interval(), Observable#window()y muchas otras; Esto se debe principalmente a que las transmisiones están basadas en extracción y la transmisión no tiene control sobre cuándo flujo emitir el siguiente elemento aguas abajo.
  • Las transmisiones ofrecen un conjunto restringido de operaciones en comparación con RxJava. Por ejemplo, las corrientes carecen de operaciones de corte ( takeWhile(), takeUntil()); la solución alternativa Stream#anyMatch()es limitada: es una operación terminal, por lo que no puede usarla más de una vez por transmisión
  • A partir de JDK 8, no existe la operación Zip de Stream #, que a veces es bastante útil
  • Los Streams son difíciles de construir por usted mismo, Observable se puede construir de muchas maneras EDITAR: Como se señaló en los comentarios, hay formas de construir Stream. Sin embargo, dado que no hay un cortocircuito no terminal, no puede, por ejemplo, generar fácilmente un flujo de líneas en el archivo (JDK proporciona archivos # líneas y líneas BufferedReader # fuera de la caja, y otros escenarios similares se pueden gestionar construyendo flujo de Iterator).
  • Observable ofrece facilidad de gestión de recursos ( Observable#using()); puede envolver el flujo de E / S o mutex con él y asegurarse de que el usuario no se olvide de liberar el recurso; se eliminará automáticamente al finalizar la suscripción; Stream tiene un onClose(Runnable)método, pero debe llamarlo manualmente o mediante try-with-resources. P.ej. debe tener en cuenta que Files # lines () debe estar encerrado en el bloque try-with-resources.
  • Los observables están sincronizados hasta el final (en realidad no verifiqué si lo mismo es cierto para Streams). Esto le evita pensar si las operaciones básicas son seguras para subprocesos (la respuesta siempre es 'sí', a menos que haya un error), pero la sobrecarga relacionada con la concurrencia estará allí, sin importar si su código lo necesita o no.

Resumen: RxJava difiere significativamente de Streams. Las alternativas reales de RxJava son otras implementaciones de ReactiveStreams , por ejemplo, parte relevante de Akka.

Actualización . Hay un truco para usar el grupo de unión de bifurcación no predeterminado Stream#parallel, consulte Grupo de subprocesos personalizado en la secuencia paralela de Java 8

Actualización . Todo lo anterior se basa en la experiencia con RxJava 1.x. Ahora que RxJava 2.x está aquí , esta respuesta puede estar desactualizada.

Kirill Gamazkov
fuente
2
¿Por qué es difícil construir Streams? Según este artículo, parece fácil: oracle.com/technetwork/articles/java/…
IgorGanapolsky
2
Hay un buen número de clases que tienen un método de 'flujo': colecciones, flujos de entrada, archivos de directorio, etc. Pero, ¿qué sucede si desea crear un flujo desde un bucle personalizado, digamos, iterando sobre el cursor de la base de datos? La mejor manera que he encontrado hasta ahora es crear un iterador, envolverlo con Spliterator y finalmente invocar StreamSupport # fromSpliterator. Demasiado pegamento para un caso simple en mi humilde opinión. También hay Stream.iterate pero produce flujo infinito. La única forma de cortar el desvanecimiento en ese caso es Stream # anyMatch, pero es una operación de terminal, por lo que no se puede separar el productor y el consumidor de la
transmisión
2
RxJava tiene Observable.fromCallable, Observable.create y así sucesivamente. O puede producir Observable infinito de forma segura, luego decir '.takeWhile (condición)', y está de acuerdo con enviar esta secuencia a los consumidores
Kirill Gamazkov
1
Las corrientes no son difíciles de construir por ti mismo. Simplemente puede llamar Stream.generate()y pasar su propia Supplier<U>implementación, solo un método simple desde el que proporciona el siguiente elemento en la secuencia. Hay muchos otros métodos. Para construir fácilmente una secuencia Streamque depende de valores anteriores, puede usar el interate()método, cada uno Collectiontiene un stream()método y Stream.of()construye un a Streampartir de un varargs o matriz. Finalmente StreamSupporttiene soporte para una creación de flujo más avanzada usando spliterators o para tipos primitivos de flujos.
jbx
"Las transmisiones carecen de operaciones de corte ( takeWhile(), takeUntil());" - JDK9 tiene estos, creo, en takeWhile () y dropWhile ()
Abdul
50

Java 8 Stream y RxJava se ve bastante similar. Tienen operadores parecidos (filtro, mapa, flatMap ...) pero no están diseñados para el mismo uso.

Puede realizar tareas asíncronas con RxJava.

Con Java 8 stream, atravesará elementos de su colección.

Puede hacer más o menos lo mismo en RxJava (elementos transversales de una colección) pero, dado que RxJava se enfoca en tareas concurrentes, ..., usa sincronización, bloqueo, ... Entonces, la misma tarea usando RxJava puede ser más lenta que con Java 8 stream.

Se puede comparar con RxJava CompletableFuture, pero eso puede ser capaz de calcular más de un solo valor.

dwursteisen
fuente
12
Vale la pena señalar que su declaración sobre el recorrido de la secuencia solo es cierta para una secuencia no paralela. parallelStreamadmite una sincronización similar de recorridos / mapas / filtros simples, etc.
John Vint
2
No pienso "Entonces, la misma tarea usando RxJava puede ser más lenta que con Java 8 stream". es cierto universalmente, depende en gran medida de la tarea en cuestión.
daschl
1
Me alegra que haya dicho que la misma tarea usando RxJava puede ser más lenta que con Java 8 stream . Esta es una distinción muy importante que muchos usuarios de RxJava desconocen.
IgorGanapolsky
RxJava es sincrónico por defecto. ¿Tiene algún punto de referencia para respaldar su afirmación de que puede ser más lento?
Marcin Koziński
66
@ marcin-koziński puede consultar este punto de referencia: twitter.com/akarnokd/status/752465265091309568
dwursteisen el
37

Existen algunas diferencias técnicas y conceptuales, por ejemplo, las secuencias de Java 8 son secuencias de valores síncronas de valores de un solo uso, mientras que los observables RxJava son secuencias de valores re-observables, basadas en push-pull adaptativas y potencialmente asíncronas. RxJava está dirigido a Java 6+ y también funciona en Android.

akarnokd
fuente
44
El código típico que involucra a RxJava hace un uso intensivo de lambdas que solo están disponibles desde Java 8 en adelante. Entonces puede usar Rx con Java 6, pero el código será ruidoso
Kirill Gamazkov
1
Una distinción similar es que los Observables Rx pueden permanecer vivos indefinidamente hasta que se den de baja. Las transmisiones de Java 8 terminan con operaciones por defecto.
IgorGanapolsky
2
@KirillGamazkov puede usar retrolambda para hacer que su código sea más bonito cuando apunta a Java 6.
Marcin Koziński
Kotlin se ve aún más sexy que la
actualización
30

Java 8 Streams se basan en extracción. Se itera sobre una secuencia de Java 8 que consume cada elemento. Y podría ser una corriente interminable.

RXJava Observableestá por defecto basado en push. Se suscribe a un Observable y se le notificará cuando llegue el siguiente elemento ( onNext), o cuando se complete la transmisión ( onCompleted), o cuando ocurra un error ( onError). Debido a Observableque usted recibe onNext, onCompleted, onErroreventos, se pueden hacer algunas funciones de gran alcance como la combinación de diferentes Observables a una nueva ( zip, merge, concat). Otras cosas que podría hacer es el almacenamiento en caché, la aceleración, ... Y utiliza más o menos la misma API en diferentes idiomas (RxJava, RX en C #, RxJS, ...)

Por defecto, RxJava es de un solo subproceso. A menos que comience a usar Programadores, todo sucederá en el mismo hilo.

Bart De Neuter
fuente
en Stream tienes para cada uno, que es más o menos lo mismo que en Next
Paul
En realidad, las transmisiones suelen ser terminales. "Las operaciones que cierran una tubería de flujo se denominan operaciones terminales. Producen un resultado de una tubería como una Lista, un Entero o incluso vacío (cualquier tipo que no sea Stream)". ~ oracle.com/technetwork/articles/java/…
IgorGanapolsky
26

Las respuestas existentes son completas y correctas, pero falta un ejemplo claro para principiantes. Permítanme poner algunos términos concretos detrás de "push / pull-based" y "re-observable". Nota : Odio el término Observable(es una transmisión por amor de Dios), así que simplemente me referiré a las transmisiones J8 vs RX.

Considere una lista de enteros,

digits = [1,2,3,4,5]

Un J8 Stream es una utilidad para modificar la colección. Por ejemplo, incluso los dígitos se pueden extraer como,

evens = digits.stream().filter(x -> x%2).collect(Collectors.toList())

Esto es básicamente el mapa de Python , filtro, reducción , una adición muy agradable (y muy atrasado) a Java. Pero, ¿qué pasaría si los dígitos no se recopilaran antes de tiempo? ¿Qué pasaría si los dígitos se transmitieran mientras la aplicación se estaba ejecutando? ¿Podríamos filtrar los pares en tiempo real?

Imagine que un proceso de subproceso separado genera números enteros en momentos aleatorios mientras la aplicación se está ejecutando ( ---indica el tiempo)

digits = 12345---6------7--8--9-10--------11--12

En RX, evenpuede reaccionar a cada nuevo dígito y aplicar el filtro en tiempo real

even = -2-4-----6---------8----10------------12

No es necesario almacenar listas de entrada y salida. Si desea una lista de salida, no hay problema que sea transferible también. De hecho, todo es una corriente.

evens_stored = even.collect()  

Es por eso que términos como "sin estado" y "funcional" están más asociados con RX

Adam Hughes
fuente
Pero 5 ni siquiera es ... ¿Y eso parece que J8 Stream es sincrónico, mientras que Rx Stream es asíncrono?
Franklin Yu
1
@FranklinYu gracias, arreglé el error tipográfico 5. Si piensa menos en términos de síncronos vs asíncronos, aunque puede ser correcto, y más en términos de imperativo vs funcional. En J8, primero recolecta todos sus artículos, luego aplica el filtro en segundo lugar. En RX, define la función de filtro independiente de los datos y luego la asocia con una fuente uniforme (una transmisión en vivo o una colección java) ... es un modelo de programación completamente diferente
Adam Hughes,
Estoy muy sorprendido por esto. Estoy bastante seguro de que las transmisiones Java pueden estar hechas de transmisión de datos. ¿Qué te hace pensar lo contrario?
Vic Seedoubleyew
4

RxJava también está estrechamente relacionado con la iniciativa de flujos reactivos y se considera una implementación simple de la API de flujos reactivos (por ejemplo, en comparación con la implementación de los flujos de Akka ). La principal diferencia es que las corrientes reactivas están diseñadas para poder manejar la contrapresión, pero si echa un vistazo a la página de corrientes reactivas, obtendrá la idea. Describen sus objetivos bastante bien y las corrientes también están estrechamente relacionadas con el manifiesto reactivo .

Las secuencias de Java 8 son más o menos la implementación de una colección ilimitada, bastante similar a la secuencia de Scala o la secuencia lenta de Clojure .

Niclas Meier
fuente
3

Java 8 Streams permite el procesamiento de colecciones realmente grandes de manera eficiente, al tiempo que aprovecha las arquitecturas multinúcleo. Por el contrario, RxJava tiene un solo subproceso de forma predeterminada (sin programadores). Por lo tanto, RxJava no aprovechará las máquinas multinúcleo a menos que usted mismo codifique esa lógica.

IgorGanapolsky
fuente
44
Stream también tiene un solo subproceso por defecto, a menos que invoque .parallel (). Además, Rx da más control sobre la concurrencia.
Kirill Gamazkov
@KirillGamazkov Kotlin Coroutines Flow (basado en Java8 Streams) ahora admite concurrencia estructurada: kotlinlang.org/docs/reference/coroutines/flow.html#flows
IgorGanapolsky
Es cierto, pero no dije nada sobre Flow y la concurrencia estructurada. Mis dos puntos fueron: 1) tanto Stream como Rx son de un solo subproceso a menos que cambies explícitamente eso; 2) Rx le brinda un control detallado sobre qué paso realizar en qué grupo de subprocesos, en contraste con Streams, que solo le permite decir "hacerlo paralelo de alguna manera"
Kirill Gamazkov
Realmente no entiendo el punto de la pregunta "¿para qué necesita un grupo de subprocesos?". Como ha dicho, "para permitir el procesamiento de colecciones realmente grandes de manera eficiente". O tal vez quiero que parte de la tarea vinculada a IO se ejecute en un grupo de subprocesos separado. No creo haber entendido la intención detrás de tu pregunta. ¿Inténtalo de nuevo?
Kirill Gamazkov
1
Los métodos estáticos en la clase Programadores permiten obtener grupos de subprocesos predefinidos, así como crear uno desde el Ejecutor. Ver reactivex.io/RxJava/2.x/javadoc/io/reactivex/schedulers/…
Kirill Gamazkov