1. Perché serve la serializzazione
Immaginate che il vostro oggetto sia come le cose che portate con voi in vacanza. La serializzazione è l’impacchettamento di tutto il contenuto della valigia in un contenitore speciale che si può mettere in stiva o spedire per posta. La deserializzazione, di conseguenza, è lo spacchettamento di questo contenitore e l’ottenimento degli oggetti nella forma originaria.
In sostanza, la serializzazione converte un oggetto in un flusso di byte che può essere salvato su file, inviato in rete o semplicemente mantenuto in memoria. La deserializzazione fa l’inverso: ricostruisce l’oggetto da questo flusso. Per dirla in modo molto semplice, la serializzazione è come «congelare» un oggetto per poi «scongelarlo» e riottenerlo nello stesso stato.
Salvataggio dello stato degli oggetti tra le esecuzioni del programma
Uno degli scenari più comuni è il salvataggio dello stato dell’applicazione. Ad esempio, avete un elenco di utenti, i risultati di un gioco o le impostazioni dell’app. Tutto ciò è comodo da conservare direttamente sotto forma di oggetti. Per non perdere i dati tra le esecuzioni, si serializzano su file e, al successivo avvio, si deserializzano.
Un buon esempio è il salvataggio in un gioco. Quando il giocatore completa un livello, il suo progresso viene «congelato» e scritto su file tramite la serializzazione. Il giorno dopo avvia il gioco e il progresso viene «scongelato»: i dati dal file tornano a essere oggetti, e il giocatore continua dallo stesso punto in cui si era fermato.
Creiamo un semplice salvataggio:
import java.io.*;
// La classe del giocatore deve essere Serializable
class Player implements Serializable {
String name;
int score;
Player(String name, int score) {
this.name = name;
this.score = score;
}
}
public class GameSaveExample {
public static void main(String[] args) throws Exception {
// Creiamo un oggetto giocatore
Player player = new Player("Ihor", 1500);
// --- Salvataggio (serializzazione) ---
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
out.writeObject(player);
System.out.println("Progresso salvato!");
}
// --- Caricamento (deserializzazione) ---
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
Player loaded = (Player) in.readObject();
System.out.println("Progresso caricato: " + loaded.name + " con punteggio " + loaded.score);
}
}
}
Attenzione: perché questo codice funzioni, la classe Player deve implementare l’interfaccia Serializable. Maggiori dettagli su di essa nella prossima lezione!
- Player è una classe normale con i campi nome e punteggio, marcata con l’interfaccia Serializable (implements Serializable).
- ObjectOutputStream scrive l’oggetto nel file "save.dat".
- ObjectInputStream legge lo stesso oggetto indietro.
- Di conseguenza otteniamo un vero salvataggio: al prossimo avvio il programma caricherà l’oggetto del giocatore con lo stesso stato.
Invio di oggetti tramite rete e tra JVM
Nei sistemi distribuiti spesso è necessario trasferire oggetti tra programmi diversi o persino tra macchine diverse. Ad esempio, avete un client e un server che devono scambiarsi messaggi. La serializzazione consente di «impacchettare» un oggetto da un lato, inviarlo in rete e «spacchettarlo» dall’altro.
Esempio: il client invia al server un oggetto d’ordine (Order), il server lo riceve, lo deserializza e lo elabora.
Uso nelle tecnologie Java
- RMI (Remote Method Invocation): consente di invocare metodi di oggetti remoti — la serializzazione è necessaria per passare argomenti e valori di ritorno.
- Sessioni HTTP: nei servlet, gli oggetti in sessione vengono serializzati durante il riavvio del container.
- JMS (Java Message Service): i messaggi tra componenti possono essere serializzati.
- Caching: gli oggetti possono essere serializzati per la memorizzazione nella cache (su disco o in un archivio distribuito).
Caching e portabilità
Se volete salvare rapidamente risultati intermedi (ad esempio per il caching), la serializzazione è uno strumento eccellente. Serializzate un oggetto, lo salvate su disco o in memoria e poi lo ripristinate velocemente senza ricalcoli.
2. Esempi di scenari d’uso della serializzazione
Salvataggio di una collezione di utenti su file
Supponiamo di avere una classe User:
public class User {
String name;
int age;
// ... altri campi
}
E avete un elenco di utenti:
List<User> users = new ArrayList<>();
users.add(new User("Vasya", 25));
users.add(new User("Masha", 30));
// ... e così via
Per salvare questo elenco su file lo serializzate. Quando necessario, lo deserializzate e ottenete la stessa lista con gli stessi utenti. Ricordate che la classe User (e tutti i suoi campi) deve supportare la serializzazione, cioè implementare Serializable.
Invio di un messaggio tra client e server
Un esempio classico è una chat. L’utente scrive un messaggio, l’oggetto Message viene serializzato e inviato in rete. Il server riceve il flusso di byte, deserializza l’oggetto, lo elabora e, eventualmente, lo inoltra.
import java.io.*;
import java.net.*;
// Il messaggio deve essere Serializable
class Message implements Serializable {
String text;
Message(String text) {
this.text = text;
}
}
// Server
class Server {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(5000)) {
System.out.println("Il server è in attesa di una connessione...");
Socket socket = serverSocket.accept();
System.out.println("Client connesso!");
try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
Message msg = (Message) in.readObject();
System.out.println("Messaggio ricevuto: " + msg.text);
}
}
}
}
// Client
class Client {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("localhost", 5000)) {
try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
Message msg = new Message("Ciao, server!");
out.writeObject(msg);
System.out.println("Messaggio inviato!");
}
}
}
}
Come funziona:
- Per prima cosa si avvia Server (rimane in attesa di una connessione).
- Poi si avvia Client (si connette a "localhost:5000").
- Il client serializza l’oggetto Message e lo invia tramite il socket.
- Il server riceve il flusso di byte, lo deserializza e stampa il testo.
Qui usiamo i socket (ServerSocket, Socket) — un meccanismo di comunicazione di rete che studierete più avanti. Al momento non contano i dettagli della rete, ma l’idea: il client crea un oggetto Message, lo serializza e lo invia; il server riceve il flusso di byte, lo deserializza di nuovo in un oggetto e stampa il messaggio. Così, anche se non è ancora chiaro cosa siano le classi ServerSocket e Socket, l’esempio mostra il valore della serializzazione: grazie ad essa si può «impacchettare» un oggetto, trasferirlo in rete e, dall’altro lato, spacchettarlo senza conversioni superflue.
Caching degli oggetti
Nelle applicazioni di grandi dimensioni si usa spesso il caching per accelerare l’esecuzione. Ad esempio, i risultati di calcoli complessi vengono serializzati e salvati nella cache (file, database, archivio distribuito). Al successivo accesso il risultato si può ripristinare velocemente deserializzando l’oggetto.
import java.io.*;
// Il risultato dei calcoli che vogliamo mettere in cache
class Result implements Serializable {
int value;
Result(int value) {
this.value = value;
}
}
public class CacheExample {
private static final String CACHE_FILE = "cache.dat";
public static void main(String[] args) throws Exception {
Result result;
// Verifichiamo se esiste una cache
File file = new File(CACHE_FILE);
if (file.exists()) {
// Carichiamo il risultato dalla cache
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
result = (Result) in.readObject();
System.out.println("Caricato dalla cache: " + result.value);
}
} else {
// Calcolo "pesante" (per l'esempio, solo il quadrato di un numero)
int x = 12345;
System.out.println("Calcolo in corso... (è lento)");
result = new Result(x * x);
// Salviamo il risultato nella cache
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
out.writeObject(result);
System.out.println("Salvato nella cache: " + result.value);
}
}
}
}
3. Limitazioni e rischi della serializzazione
La serializzazione è uno strumento potente, ma non privo di insidie. Vediamo le principali limitazioni e i rischi.
Non tutti gli oggetti possono essere serializzati
In Java non tutti gli oggetti possono essere serializzati «out of the box». Ad esempio, gli oggetti collegati a risorse esterne (file, connessioni di rete, stream di I/O) non sono serializzabili. È logico: non si può serializzare un «file aperto» o una connessione di rete «viva» — il loro stato dipende dal sistema operativo e dall’ambiente di esecuzione.
Esempio: una classe con un campo di tipo FileInputStream non può essere serializzata: tentando di serializzarla si verificherà un errore.
Questioni di sicurezza
La serializzazione può rappresentare una potenziale falla di sicurezza. Se deserializzate dati provenienti da una fonte non attendibile (ad esempio da Internet), un attaccante potrebbe inviare un flusso di byte malevolo che porti a comportamenti inattesi del vostro programma e, talvolta, persino all’esecuzione di codice dannoso.
Regola: Non deserializzate mai dati da fonti non affidabili! È come accettare un pacco da un mittente sconosciuto: dentro potrebbe esserci di tutto.
Compatibilità tra versioni
Se modificate la struttura di una classe (ad esempio aggiungete o rimuovete un campo), gli oggetti serializzati in precedenza potrebbero diventare incompatibili con la nuova versione della classe. Questo può portare a errori in fase di deserializzazione. Analizzeremo questo aspetto in dettaglio nelle lezioni successive.
Prestazioni
La serializzazione binaria in Java è abbastanza veloce, ma talvolta non è la più compatta e non sempre è comoda per lo scambio con altri linguaggi di programmazione. Per l’interoperabilità con sistemi esterni si usano spesso formati testuali (JSON, XML).
4. Errori tipici al primo approccio alla serializzazione
Errore n. 1: tentare di serializzare un oggetto che non implementa l’interfaccia Serializable.
Di conseguenza otterrete l’eccezione NotSerializableException. Non dimenticate di indicare esplicitamente implements Serializable nella classe e di assicurarvi che anche tutti i campi siano serializzabili!
Errore n. 2: serializzare oggetti con campi non serializzabili.
Se la vostra classe contiene un campo di un tipo che non supporta la serializzazione (ad esempio uno stream o una connessione al DB), la serializzazione non funzionerà. Soluzione: contrassegnare tali campi come transient (ne parleremo più avanti).
Errore n. 3: deserializzare dati da fonti non affidabili.
Questo può portare a vulnerabilità di sicurezza o persino all’esecuzione di codice malevolo. Fidatevi solo dei dati che sono stati serializzati dalla vostra applicazione!
Errore n. 4: modificare la struttura della classe dopo la serializzazione.
Se avete salvato un oggetto e poi avete aggiunto o rimosso un campo nella classe, al tentativo di deserializzazione si verificherà un errore o compariranno valori «strani». Maggiori dettagli nelle lezioni successive.
GO TO FULL VERSION