Java tiene traccia degli oggetti che sono stati scritti nello stream e le istanze successive sono scritte come un ID, non un vero oggetto serializzato.
Quindi, per il tuo esempio, se scrivi istanza "a" nello stream, il flusso assegna a quell'oggetto un ID univoco (diciamo "1"). Come parte della serializzazione di "a", devi serializzare "b", e lo stream gli dà un altro id ("2"). Se poi scrivi "b" nello stream, l'unica cosa che viene scritta è l'ID, non l'oggetto reale.
Il flusso di input fa la stessa cosa al contrario: per ogni oggetto che legge dallo stream, assegna un numero ID utilizzando lo stesso algoritmo del flusso di output e quel numero ID fa riferimento all'istanza dell'oggetto in una mappa. Quando vede un oggetto che è stato serializzato usando un ID, recupera l'istanza originale dalla mappa.
Questo è il modo in cui documenti API lo descrivono:
Multiple references to a single object are encoded using a reference sharing mechanism so that graphs of objects can be restored to the same shape as when the original was written
Questo comportamento può causare problemi: poiché lo stream contiene un riferimento rigido a ciascun oggetto (in modo che sappia quando sostituire l'ID), è possibile esaurire la memoria se si scrivono molti oggetti temporanei allo stream. Lo risolvi chiamando reset()
.