Il professor Ricci dedica gran parte della lezione a esplorare il ricco ecosistema degli operatori messi a disposizione da ReactiveX (Rx). Gli operatori sono il cuore della programmazione reattiva: permettono di manipolare flussi di dati asincroni con lo stesso tipo di espressivita che si ha con le operazioni funzionali su collezioni (map, filter, reduce), ma con il valore aggiunto della gestione del tempo e dell'asincronia.
Gli operatori si classificano in diverse categorie, come presentato nelle slide e nei lab notes:
Il professor Ricci sottolinea un punto affascinante: l'insieme degli operatori di ReactiveX e Turing-equivalente. Cio significa che qualsiasi computazione puo essere espressa esclusivamente combinando flussi e operatori. Si puo persino implementare un costrutto if usando gli operatori giusti. Questo mostra la potenza espressiva del modello a flussi di dati.
Gli operatori di trasformazione sono quelli che modificano gli elementi emessi da un flusso. Il professore ne mostra diversi esempi concreti nei test predisposti per il corso.
L'operatore map applica una funzione a ciascun elemento del flusso e riemette il risultato. E l'equivalente reattivo di una trasformazione funzionale classica.
Flowable.range(1, 10)
.map(v -> v * v) // quadrato di ogni numero
.subscribe(v -> System.out.println("Quadrato: " + v));
// Output: 1, 4, 9, 16, 25, 36, 49, 64, 81, 100
L'operatore scan applica una funzione cumulativa a ogni elemento, emettendo ogni risultato intermedio. E simile a reduce ma restituisce tutti i valori di passaggio, non solo quello finale.
Flowable.range(1, 5)
.scan((acc, v) -> acc + v)
.subscribe(System.out::println);
// Output: 1, 3, 6, 10, 15
buffer raccoglie periodicamente gli elementi in pacchetti (liste) e li emette come blocchi, riducendo il numero di emissioni. window fa qualcosa di simile ma emette finestre che sono a loro volta Observable, utili per operazioni che richiedono la manipolazione di sottosequenze. groupBy divide il flusso in sottoflussi organizzati per chiave.
Il professor Ricci consiglia di sperimentare con questi operatori nei test del laboratorio: i file Test01, Test02 e successivi contengono esempi funzionanti con Flowable.range, map, filter, scan e zip. Eseguirli e fondamentale per capire la differenza tra operatori che sembrano simili.
FlatMap e forse l'operatore piu potente e pervasivo della programmazione reattiva. Il professor Ricci lo spiega con cura, perche e centrale nell'assignment del corso.
Mentre map trasforma ogni elemento in un nuovo valore, flatMap trasforma ogni elemento in un nuovo flusso e poi appiattisce il risultato in un unico flusso. Da un flusso di elementi si ottiene un flusso di flussi che viene "appiattito" in un singolo flusso di output.
Flowable.range(1, 10)
.flatMap(v -> Flowable.just(v * v) // per ogni v, crea un nuovo flusso…
.subscribeOn(Schedulers.computation())) // …eseguito su un thread diverso
.subscribe(System.out::println);
// I quadrati vengono emessi fuori ordine, perche ogni
// sottoflusso viaggia su un thread diverso!
La flatMap e l'operatore che permette di parallelizzare computazioni indipendenti. Quando combini flatMap con subscribeOn, ogni elemento del flusso originale viene processato su un thread diverso, e i risultati vengono poi riaggregati. Vedremo in dettaglio questo pattern nella sezione dedicata.
Il professore lo descrive cosi: "Se ogni elemento di un flusso e a sua volta un flusso, uso la flatMap per appiattire." E l'operatore giusto quando, analizzando qualcosa, capita che ogni elemento generi un sottoproblema che a sua volta produce un flusso di risultati.
Gli operatori di filtro selezionano quali elementi del flusso far passare, basandosi su condizioni logiche o temporali.
Come nelle lambda funzionali, filter trattiene solo gli elementi che soddisfano un predicato.
Flowable.range(1, 20)
.filter(v -> v % 3 == 0) // solo multipli di 3
.map(v -> v * v)
.subscribe(System.out::println);
// Output: 9, 36, 81, 144, 225, 324
take(n) emette solo i primi n elementi, poi si completa. skip(n) salta i primi n elementi e lascia passare i successivi. takeLast(n) e skipLast(n) lavorano simmetricamente dalla fine del flusso.
debounce e un operatore temporale di grande utilita pratica. Se un elemento viene generato troppo vicino nel tempo rispetto a un elemento precedente (entro una finestra temporale specificata), viene scartato. E il meccanismo classico usato nei sistemi embedded e nelle UI per evitare "rimbalzi": ad esempio, in un campo di ricerca che deve chiamare un'API, non vogliamo inviare una richiesta a ogni singolo carattere digitato, ma solo dopo che l'utente ha smesso di scrivere per un certo intervallo.
sequenceDiagram
participant S as Sorgente eventi
participant D as debounce(200ms)
participant O as Output
S->>S: evento 1 (t=0ms)
S->>S: evento 2 (t=50ms) - scartato!
S->>S: evento 3 (t=180ms) - scartato!
S->>S: evento 4 (t=300ms)
D->>O: passa evento 4 (ultimo, dopo 200ms di silenzio)
Altri operatori temporali includono sample (prende l'ultimo elemento a intervalli regolari), throttleFirst (prende il primo elemento in ogni finestra temporale) e timeout (segnala errore se non arrivano elementi entro un tempo limite).
Spesso e necessario lavorare con piu flussi contemporaneamente. Gli operatori di combinazione permettono di fonderli in modo sincrono o asincrono.
merge combina piu Observable in uno solo, fondendo le emissioni nell'ordine in cui arrivano. Se due flussi emettono valori in parallelo, il risultato sara l'interleaving delle sequenze. L'output e 1,1,2,2,3,3 (interleaving) e si completa solo quando tutti i flussi sorgente hanno completato.
Observable.range(1, 3).mergeWith(Observable.range(1, 3))
.subscribe(System.out::println); // 1,1,2,2,3,3
zip accoppia elementi in modo sincrono: prende il primo elemento del primo flusso e il primo elemento del secondo flusso, li combina tramite una funzione, e produce il primo elemento del flusso risultante. Poi il secondo, e cosi via. L'output e 1,2,3,4,5 (sincrono: attende che entrambi i flussi abbiano prodotto l'elemento i-esimo prima di procedere).
Il professore fa un esempio concreto: un flusso di parole ("the", "quick", "brown", "fox") viene zippato con un flusso di numeri (1, 2, 3, 4, 5), producendo coppie come "1. the", "2. quick", "3. brown", ecc.
combineLatest e diverso: quando uno qualsiasi dei flussi sorgente emette un nuovo valore, l'operatore prende l'ultimo valore di ciascun flusso e li combina. E ideale quando si ha un insieme di fonti che si aggiornano indipendentemente e si vuole sempre lo stato piu recente di tutte.
switch converte un Observable che emette Observable in un singolo Observable che emette solo gli elementi dell'ultimo Observable emesso. Se arriva un nuovo flusso, si "disiscrive" dal precedente e si aggancia al nuovo — utile per scenari di ricerca in tempo reale dove ogni nuova richiesta deve cancellare la precedente.
Il professore cita anche l'operatore join, che combina elementi di due Observable usando finestre temporali sovrapposte, e groupJoin per composizioni piu complesse.
ReactiveX utilizza i marble diagrams (diagrammi a biglie) per rappresentare visivamente il comportamento degli operatori. Sono uno strumento potentissimo sia per capire che per progettare flussi reattivi.
Un marble diagram mostra: in alto, il flusso di input (una linea temporale con palline colorate che rappresentano gli elementi); al centro, l'operatore con i suoi parametri; in basso, il flusso di output risultante. Le linee verticali indicano il completamento, le X indicano errori, le barre di ritardo mostrano intervalli temporali.
flowchart LR
subgraph Input
direction LR
a1((1)) --> a2((2)) --> a3((3)) --> a4((4)) --> a5((5))
end
subgraph Operatore
m[map: v -> v * v]
end
subgraph Output
direction LR
b1((1)) --> b2((4)) --> b3((9)) --> b4((16)) --> b5((25))
end
a1 --> m --> b1
a2 --> m --> b2
a3 --> m --> b3
a4 --> m --> b4
a5 --> m --> b5
I marble diagrams sono cosi importanti che la documentazione ufficiale di ReactiveX (reactivex.io/documentation/operators.html) li usa come primaria forma di documentazione per ogni operatore. Il professore consiglia di consultarli ogni volta che si ha un dubbio sul comportamento di un operatore: "Un'immagine vale piu di mille parole, e nel caso della programmazione reattiva, un marble diagram vale piu di mille righe di codice."
Uno degli aspetti piu importanti della programmazione reattiva e il controllo su quale thread viene eseguita ciascuna parte della catena di elaborazione. Questo e il compito degli Schedulers.
Due operatori fondamentali controllano il threading:
subscribeOn(<Scheduler>) — specifica su quale thread deve essere eseguito l'Observable a monte (la sorgente e tutte le operazioni fino al primo observeOn).observeOn(<Scheduler>) — specifica su quale thread deve essere eseguito il Subscriber a valle (le operazioni successive).Flowable.fromCallable(() -> {
Thread.sleep(1000); // simulazione computazione costosa
return "Done";
})
.subscribeOn(Schedulers.io()) // esecuzione su thread I/O
.observeOn(Schedulers.single()) // osservazione su thread singolo (es. GUI)
.subscribe(System.out::println, Throwable::printStackTrace);
Il professore spiega che questo pattern permette di spostare computazioni bloccanti su thread di background, lasciando il thread principale (o quello della GUI) libero per gestire l'interattivita. E l'equivalente reattivo dell'uso di ExecutorService in Java classico, ma molto piu dichiarativo e composizionale.
Il professor Ricci mette in guardia: se si usa subscribeOn piu volte nella stessa catena, solo il primo ha effetto. Per cambiare thread a meta catena, bisogna usare observeOn. Questo e un errore comune tra i principianti.
ReactiveX mette a disposizione diversi tipi di Scheduler, ciascuno progettato per un carico di lavoro specifico:
| Scheduler | Uso | Pool di thread |
|---|---|---|
Schedulers.computation() | Operazioni CPU-intensive (calcoli, trasformazioni) | Pool fisso: tanti thread quanti i core della CPU |
Schedulers.io() | Operazioni I/O-bound (file, rete, database) | Pool dinamico: cresce e decresce in base al carico |
Schedulers.single() | Esecuzione sequenziale su un unico thread (es. aggiornamento GUI) | Singolo thread, FIFO |
Schedulers.from(Executor) | Pool personalizzato | Wrap di un Executor esistente |
Il professore fa un esempio concreto: subscribeOn(Schedulers.io()) per caricare immagini e observeOn(Schedulers.computation()) per elaborarle (come ridimensionamento o filtri), per poi eventualmente tornare al thread UI per la visualizzazione.
myObservableServices.retrieveImage(url)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.subscribe(bitmap -> processImage(bitmap));
Nota: Schedulers.from(executor) permette di usare pool esistenti (ad esempio un pool fisso di 10 thread, a differenza di computation() che usa tanti thread quanti i core e io() che e illimitato).
Una distinzione fondamentale nell'universo ReactiveX e quella tra Cold e Hot Observable. Il professore la spiega con chiarezza nelle slide e nei lab notes.
Un Cold Observable inizia a emettere elementi solo quando riceve almeno un subscriber. Ogni subscriber riceve l'intera sequenza di elementi a partire dall'inizio, indipendentemente dal momento in cui si e iscritto. E un modello di tipo pull (lazy).
Esempi tipici: Observable.just(), Observable.fromIterable(), Observable.range() — tutti quelli che generano dati su richiesta.
Observable<String> cold = Observable.just("A", "B", "C");
cold.subscribe(s -> System.out.println("Sub1: " + s)); // A, B, C
cold.subscribe(s -> System.out.println("Sub2: " + s)); // A, B, C
// Entrambi i subscriber ricevono TUTTI gli elementi!
Un Hot Observable emette elementi continuamente, indipendentemente dalla presenza di subscriber. I subscriber tardivi ricevono solo gli elementi emessi dopo la loro iscrizione. E un modello di tipo push: la sorgente produce dati al proprio ritmo, e gli observer devono tenere il passo.
Esempi: eventi del mouse, tick di timer, stream di prezzi azionari, messaggi da un websocket. Un Hot Observable puo emettere anche quando non c'e nessun subscriber.
L'eccezione alla regola "nothing happens until you subscribe" — con gli Hot Observable, le cose succedono sempre.
| Caratteristica | Cold | Hot |
|---|---|---|
| Inizio emissione | Quando arriva un subscriber | Immediatamente (o da un evento esterno) |
| Elementi ricevuti | Tutti, dall'inizio | Solo quelli dopo l'iscrizione |
| Modello | Pull (lazy) | Push (eager) |
| Isolamento | Ogni subscriber ha la sua sequenza | Tutti i subscriber condividono la stessa sequenza |
| Backpressure | Gestibile a monte | Richiede strategie esplicite |
Alcuni Hot Observable possono cache o replay la storia delle emissioni, totalmente o parzialmente, permettendo ai subscriber tardivi di recuperare elementi passati.
La backpressure e uno dei concetti piu importanti della programmazione reattiva. Si verifica quando un produttore emette dati piu velocemente di quanto il consumatore riesca a processarli. Senza backpressure, i dati si accumulerebbero in buffer sempre piu grandi, portando a OutOfMemoryError o a un degrado delle prestazioni.
Il professore usa l'analogia della catena di montaggio presente nelle slide: la materia prima grezza fluisce da una sorgente (il Publisher), passa attraverso varie stazioni di lavoro (gli operatori), e arriva al prodotto finito che viene consegnato al consumatore (il Subscriber). Se una stazione di lavoro e piu lenta, deve poter segnalare a monte di rallentare: questo segnale di feedback e la backpressure.
flowchart LR
subgraph Produttore
P[Publisher: 1000 msg/s]
end
subgraph Buffer
B[(Buffer)]
end
subgraph Consumatore
C[Subscriber: 10 msg/s]
end
P -- overflow! --> B -- 10 msg/s --> C
C -- "request(10)" --> B
B -- "backpressure signal" --> P
Il modello push-pull ibrido e la chiave: il downstream puo fare pull di n elementi dall'upstream se sono pronti, ma se non lo sono, verranno spinti (push) dall'upstream appena prodotti. A differenza del modello Observable standard (senza backpressure), RxJava introduce Flowable che supporta nativamente la backpressure.
Ricordate: Observable NON supporta backpressure. Se avete bisogno di controllo del flusso, usate Flowable. Questa distinzione e fondamentale e viene spesso chiesta in sede d'esame, come sottolinea il professore.
ReactiveX offre diverse strategie per gestire la backpressure, ciascuna con caratteristiche specifiche:
| Strategia | Operatore | Comportamento |
|---|---|---|
| Buffering | buffer(size) | Raccoglie elementi in lotti di dimensione fissa e li emette come blocchi |
| Batching | window(size) | Come buffer, ma emette sotto-Observable invece di liste |
| Skipping | sample(duration) | Prende l'ultimo elemento a intervalli regolari, scartando il resto |
| Debouncing | debounce(duration) | Scarta elementi che arrivano troppo ravvicinati nel tempo |
| Throttling | throttleFirst(duration) | Prende il primo elemento di ogni finestra temporale |
Quando il buffer si riempie, si puo specificare una strategia di overflow tramite onBackpressureBuffer(capacity, callback, strategy):
ON_OVERFLOW_ERROR — lancia un'eccezioneON_OVERFLOW_DROP_LATEST — elimina l'elemento piu recente se il buffer e pienoON_OVERFLOW_DROP_OLDEST — elimina l'elemento piu vecchio per fare spazio al nuovoOppure si puo usare onBackpressureDrop() che semplicemente scarta gli elementi in eccesso senza buffer.
Nella programmazione reattiva ci si trova spesso a dover integrare codice esistente che non e reattivo. Il PublishSubject (o piu in generale i Subject) e la soluzione a questo problema.
Un Subject e un particolare tipo di flusso che funge sia da Observable (ci si puo iscrivere) che da Observer (ci si possono inviare dati manualmente). Il professore lo descrive come un canale che fornisce un'API per produrre valori (onNext, onComplete, onError) e consumarli (via subscribe).
PublishSubject<Integer> subject = PublishSubject.create();
// Mi iscrivo al subject come a un qualsiasi Observable
subject.subscribe(v -> System.out.println("Ricevuto: " + v));
// Produco valori manualmente, fuori dalla lambda di creazione!
subject.onNext(1);
subject.onNext(2);
subject.onComplete();
La differenza fondamentale rispetto a Observable.create() e che qui gli onNext vengono chiamati dopo la creazione del flusso, da qualsiasi thread e da qualsiasi parte del codice. Il professore spiega che questo e il meccanismo ideale per "incollare" codice legacy (callback, listener, API sincrone) con codice reattivo.
Il PublishSubject crea un flusso hot: gli elementi emessi prima di una subscribe vengono persi (a meno di usare ReplaySubject che bufferizza e riemette tutta la cronologia). Scegliete il Subject giusto in base alle esigenze: PublishSubject per eventi in tempo reale, BehaviorSubject per l'ultimo valore, ReplaySubject per la cronologia completa.
Il professore mostra un esempio concreto (dal test 04A) di come agganciare i flussi reattivi a una GUI. L'esempio crea un pulsante e converte i suoi eventi di click in un flusso reattivo, permettendo di applicare operatori come map, filter, debounce e throttle agli eventi UI.
Il pattern tipico e:
onNext a ogni click// Creo il ponte tra GUI e mondo reattivo
PublishSubject<Void> onClickSubject = PublishSubject.create();
// Collego la GUI
button.addActionListener(e -> onClickSubject.onNext(null));
// Applico operatori come su qualsiasi flusso
onClickSubject
.debounce(300, TimeUnit.MILLISECONDS) // anti-rimbalzo
.observeOn(Schedulers.single()) // thread UI
.subscribe(e -> System.out.println("Click processato!"));
Questo approccio, nota il professore, permette di separare completamente la logica di business dalla presentazione: la GUI si occupa solo di generare eventi, e il flusso reattivo si occupa di trasformarli, filtrarli e processarli.
Questa e forse la sezione piu importante per l'assignment del corso. Il professore mostra come flatMap + subscribeOn permetta di distribuire computazioni indipendenti su thread multipli in modo dichiarativo.
Flowable.range(1, 10)
.flatMap(v -> Flowable.just(v * v)
.subscribeOn(Schedulers.computation()))
.subscribe(v -> System.out.println(
Thread.currentThread().getName() + ": " + v));
Quando si esegue questo codice, il professore fa notare che:
Il meccanismo e potente: invece di gestire manualmente un pool di thread e una coda di risultati, si dichiara semplicemente che ogni sottoflusso deve girare su un thread di computation, e RxJava si occupa di tutto il resto.
Questo pattern flatMap + subscribeOn e centrale nel secondo assignment. Il professore lo menziona esplicitamente: "Lo trovate in quello che chiamiamo test parallelism, vedrete che le varie map, 1, 2, 3, non sono piu in ordine, perche vengono servite da thread diversi." Saper spiegare perche l'ordine cambia e perche questo e un vantaggio e una conoscenza richiesta.
Il professore dedica una parte della lezione alla gestione dei thread in Vert.x, un framework reattivo per la JVM. Spiega un punto spesso frainteso: contrariamente a quanto si possa pensare, ogni Verticle ha un solo background thread, non un pool dinamico.
Quando si usa executeBlocking() in Vert.x, le computazioni bloccanti vengono accodate allo stesso background thread, non eseguite in parallelo. Il professore lo dimostra con un esempio concreto:
// Immaginate di chiamare executeBlocking due volte
vertx.executeBlocking(promise -> {
// blocking computation started - aspetta 5 secondi
Thread.sleep(5000);
promise.complete(100);
});
vertx.executeBlocking(promise -> {
// blocking computation started - deve aspettare che il primo finisca!
Thread.sleep(5000);
promise.complete(200);
});
Anche se i due compiti sono indipendenti, vengono eseguiti in sequenza sullo stesso background thread: il secondo deve attendere che il primo sia completato. Il professore specifica: "Se vi mettete tanta roba da fare in background, vi rendete conto che non la fa in parallelo. Ha sempre un solo background thread, quindi li accoda."
Questa caratteristica ha implicazioni importanti per la progettazione: se si hanno piu task bloccanti indipendenti, conviene distribuirli su Verticle distinti o usare un pool di thread esterno gestito manualmente.
I thread pool di Vert.x non vanno confusi con gli Schedulers di RxJava. In Vert.x, l'event loop e unico per Verticle e l'executeBlocking accoda i task su un singolo background thread. RxJava, invece, offre Schedulers con pool di dimensioni variabili. Sono due astrazioni diverse che operano a livelli diversi.
Il professore mostra in dettaglio l'operatore zip e la sua versione zipWith, che e un esempio di composizione tra flussi di tipo diverso.
L'esempio concreto mostrato a lezione:
// Sorgente 1: parole
Flowable<String> src1 = Flowable.just("the", "quick", "brown", "fox");
// Sorgente 2: numeri
Flowable<Integer> src2 = Flowable.range(1, 5);
// Zip: accoppia in modo sincrono e combina
src1.zipWith(src2, (parola, numero) -> numero + ". " + parola)
.subscribe(System.out::println);
// Output: "1. the", "2. quick", "3. brown", "4. fox"
Nell'operatore zip, i tipi cambiano: da due flussi di tipo diverso (String e Integer) si produce un nuovo flusso di un terzo tipo (String), determinato dalla funzione di combinazione. Questa flessibilita tipale e una delle caratteristiche che rende gli operatori di ReactiveX cosi potenti per il data flow.
Il professore nota che la zip attende che entrambi i flussi abbiano il prossimo elemento pronto prima di emettere un output. Se un flusso si completa prima, gli elementi rimanenti dell'altro flusso vengono scartati (come nell'esempio, dove "fox" viene comunque zippato con 4, ma il flusso src2 ha 5 elementi, mentre src1 ne ha 4: l'ultimo elemento di src2 non ha corrispondenza).
Altri operatori di composizione menzionati: and/then/when (che usano Pattern e Plan come intermediari), startWith (premette una sequenza di elementi), e join (che usa finestre temporali).
Il professore chiude la lezione con alcune considerazioni importanti. La programmazione reattiva e le Reactive Extensions non sono un "silver bullet" per la programmazione asincrona. Sono efficaci per gestire flussi asincroni di dati/eventi in stile funzionale, ma non come modello di programmazione asincrona general-purpose.
La sfida attuale, dice il professore, e come integrare tutti questi approcci e tecniche:
La programmazione reattiva distribuita e un ambito di ricerca attivo: la gestione dei glitch (inconsistenze temporanee durante la propagazione del cambiamento) in contesto distribuito e ancora una questione aperta a causa di latenze di rete, assenza di clock globale e possibili guasti. I riferimenti a Salvaneschi et al. 2014 e Mogk et al. 2018 sono i punti di partenza su questo tema.
flatMap. Mentre map trasforma ogni elemento in un nuovo valore, flatMap trasforma ogni elemento in un Observable/Flowable e poi appiattisce tutti i sottoflussi in un unico flusso di output.
subscribeOn specifica il thread su cui viene eseguita la sorgente (a monte nella catena). observeOn specifica il thread su cui vengono eseguiti i subscriber e le operazioni a valle. subscribeOn ha effetto solo la prima volta che viene chiamato; per cambiare thread a meta catena serve observeOn.
Serve la backpressure. Con un Flowable (che supporta backpressure), il subscriber puo fare request(n) per segnalare quanti elementi e pronto a processare. Strategie come buffer, sample, debounce o onBackpressureDrop permettono di gestire il sovraccarico. Con un semplice Observable (senza backpressure), si rischia un OutOfMemoryError per buffer overflow.
Uno solo. Contrariamente a quanto si potrebbe pensare, Vert.x ha un singolo background thread per Verticle. Le chiamate a executeBlocking vengono accodate ed eseguite in sequenza, non in parallelo. Per parallelizzare task bloccanti, bisogna usare Verticle distinti o un pool di thread esterno.
Un Cold Observable inizia a emettere solo quando arriva un subscriber e ogni subscriber riceve tutti gli elementi dall'inizio (modello pull/lazy). Un Hot Observable emette continuamente indipendentemente dai subscriber e i subscriber tardivi ricevono solo gli elementi successivi alla loro iscrizione (modello push). Gli Hot Observable violano la regola "nothing happens until you subscribe".
I glitch sono inconsistenze temporanee che si verificano durante la propagazione del cambiamento: una computazione viene eseguita prima che tutte le sue dipendenze siano state aggiornate, combinando valori freschi con valori ancora obsoleti. Un esempio classico: se var2 = var1 * 1 e var3 = var1 + var2, cambiando var1 da 1 a 2, var3 potrebbe diventare momentaneamente 3 (se var3 viene ricalcolato prima di var2) prima di assestarsi a 4. I sistemi reattivi moderni evitano i glitch in contesto single-machine, ma in ambito distribuito il problema e ancora aperto.
Observable NON supporta backpressure. Flowable supporta la backpressure nativamente, permettendo al consumatore di segnalare al produttore di rallentare tramite request(n). Se avete bisogno di controllo del flusso (cosa comune in scenari con produttori veloci e consumatori lenti), usate Flowable.
Perche flatMap crea un nuovo flusso per ogni elemento in ingresso e subscribeOn su Schedulers.computation() fa eseguire ciascun sottoflusso su un thread diverso del pool di computation. I risultati vengono poi riaggregati automaticamente in un unico flusso di output. L'effetto e che elementi diversi vengono processati in parallelo su core diversi.