Aller au contenu principal

Fichier objet

La sérialisation est le processus de conversion d'un objet en un format qui peut être stocké (dans un fichier) ou transmis (sur un réseau). La désérialisation est l'opération inverse : recréer l'objet à partir de ces données.

Il existe plusieurs façons de sérialiser des données en Java. Ce document couvre la sérialisation Java native et ses alternatives modernes.

Sérialisation Java native (ObjectOutputStream)

ObjectOutputStream

  • Rôle principal :
    • ObjectOutputStream est une classe en Java qui permet de sérialiser des objets Java et de les écrire dans un flux de sortie. En d'autres termes, elle convertit un objet Java en une séquence d'octets qui peut être stockée dans un fichier, envoyée sur un réseau, ou utilisée d'autres manières.
  • Fonctionnement :
    • Lorsque vous utilisez ObjectOutputStream.writeObject(Object obj), il prend l'objet obj et le transforme en un flux d'octets. Ce processus inclut non seulement les données de l'objet, mais aussi des informations sur son type, sa structure, et les objets qu'il référence.
    • Pour qu'un objet puisse être sérialisé, sa classe doit implémenter l'interface Serializable. Cela indique à Java que l'objet peut être converti en un flux d'octets.
  • Utilisations courantes :
    • Sauvegarde d'objets dans des fichiers : Permet de stocker l'état d'objets Java pour une utilisation ultérieure.
    • Communication réseau : Permet d'envoyer des objets Java à travers un réseau.
    • Clonage profond d'objets : Peut être utilisé pour créer une copie indépendante d'un objet complexe.
  • Points importants :
    • La sérialisation peut être sensible aux changements dans la structure des classes. Il est important de gérer les versions de sérialisation pour éviter les problèmes de compatibilité.
    • Certains attributs d'un objet peuvent être marqués comme transient, ce qui signifie qu'ils ne seront pas sérialisés.

ObjectInputStream

  • Rôle principal :
    • ObjectInputStream est la classe complémentaire d'ObjectOutputStream. Elle permet de désérialiser des objets Java à partir d'un flux d'entrée. En d'autres termes, elle prend une séquence d'octets (créée par ObjectOutputStream) et la transforme en un objet Java.
  • Fonctionnement :
    • Lorsque vous utilisez ObjectInputStream.readObject(), il lit un flux d'octets à partir de la source (par exemple, un fichier) et reconstitue l'objet Java correspondant.
    • Pour que la désérialisation réussisse, la classe de l'objet doit être disponible dans l'environnement d'exécution Java.
  • Utilisations courantes :
    • Chargement d'objets à partir de fichiers : Permet de restaurer l'état d'objets Java précédemment sauvegardés.
    • Réception d'objets à travers un réseau : Permet de recevoir des objets Java envoyés par un autre programme.
  • Points importants :
    • La désérialisation peut lancer une ClassNotFoundException si la classe de l'objet n'est pas trouvée.
    • La désérialisation peut lancer une InvalidClassException si la version de la classe ne correspond pas à celle utilisée lors de la sérialisation.

Exemple complet

Classe Cours

import java.io.Serializable;

public class Cours implements Serializable {
private static final long serialVersionUID = 1L;

private String code;
private String nom;

public Cours(String code, String nom) {
this.code = code;
this.nom = nom;
}

public String getCode() {
return code;
}

public String getNom() {
return nom;
}

@Override
public String toString() {
return code + " - " + nom;
}
}

Classe Note

import java.io.Serializable;

public class Note implements Serializable {
private static final long serialVersionUID = 1L;

private Cours cours;
private double note; // Note obtenue (ex: 8)
private double total; // Total possible (ex: 10)
private double valeur; // Pondération dans la session (ex: 40%)

public Note(Cours cours, double note, double total, double valeur) {
this.cours = cours;
this.note = note;
this.total = total;
this.valeur = valeur;
}

public Cours getCours() {
return cours;
}

public double getNote() {
return note;
}

public double getTotal() {
return total;
}

public double getValeur() {
return valeur;
}

public void setNote(double note) {
this.note = note;
}

/**
* Calcule le pourcentage obtenu pour cette évaluation
*/
public double getPourcentage() {
return (note / total) * 100;
}

/**
* Calcule la contribution de cette note à la note finale du cours
*/
public double getContribution() {
return getPourcentage() * (valeur / 100);
}

@Override
public String toString() {
return String.format("%s : %.1f/%.1f (%.1f%%) - Vaut %.0f%% de la session",
cours, note, total, getPourcentage(), valeur);
}
}

Classe Etudiant

import java.io.Serializable;
import java.util.ArrayList;

public class Etudiant implements Serializable {
private static final long serialVersionUID = 1L;

private String nom;
private String prenom;
private ArrayList<Note> notes;

public Etudiant(String nom, String prenom) {
this.nom = nom;
this.prenom = prenom;
this.notes = new ArrayList<>();
}

public void ajouterNote(Note note) {
notes.add(note);
}

public String getNom() {
return nom;
}

public String getPrenom() {
return prenom;
}

public ArrayList<Note> getNotes() {
return notes;
}

/**
* Calcule la moyenne pondérée de l'étudiant
*/
public double calculerMoyenne() {
if (notes.isEmpty()) {
return 0.0;
}
double somme = 0;
for (Note note : notes) {
somme += note.getContribution();
}
return somme;
}

/**
* Retourne le total des pondérations
*/
public double getTotalPonderation() {
double total = 0;
for (Note note : notes) {
total += note.getValeur();
}
return total;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Étudiant : ").append(prenom).append(" ").append(nom).append("\n");
sb.append("Notes :\n");
for (Note note : notes) {
sb.append(" - ").append(note).append("\n");
}
sb.append("Total pondération : ").append(String.format("%.0f", getTotalPonderation())).append("%\n");
sb.append("Moyenne pondérée : ").append(String.format("%.2f", calculerMoyenne())).append("%");
return sb.toString();
}
}

Code principal - Sérialisation

import java.io.*;

public class ExempleFichierObjet {
public static void main(String[] args) {
// Création d'un étudiant avec ses notes
Etudiant marie = new Etudiant("Tremblay", "Marie");

Cours prog2 = new Cours("420-C27", "Programmation II");
Cours reseau = new Cours("420-M34", "Réseau 1");
Cours baseDonnees = new Cours("420-B35", "Bases de données");

// note, total, pondération
marie.ajouterNote(new Note(prog2, 8, 10, 40)); // 8/10 pour examen valant 40%
marie.ajouterNote(new Note(prog2, 45, 50, 20)); // 45/50 pour TP valant 20%
marie.ajouterNote(new Note(reseau, 15, 20, 30)); // 15/20 pour examen valant 30%
marie.ajouterNote(new Note(baseDonnees, 9, 10, 10)); // 9/10 pour quiz valant 10%

System.out.println("=== AVANT SÉRIALISATION ===");
System.out.println(marie);
System.out.println();

// ÉCRITURE : Sérialisation de l'objet étudiant
FileOutputStream fileOut = null;
ObjectOutputStream out = null;
try {
fileOut = new FileOutputStream("etudiant.dat");
out = new ObjectOutputStream(fileOut);
out.writeObject(marie);
System.out.println("Étudiant sauvegardé dans etudiant.dat");
System.out.println();
} catch (IOException e) {
System.out.println("Erreur lors de l'écriture : " + e.getMessage());
} finally {
try {
if (out != null) {
out.close();
}
if (fileOut != null) {
fileOut.close();
}
} catch (IOException ex) {
System.out.println("Erreur lors de la fermeture des flux : " + ex.getMessage());
}
}

// LECTURE : Désérialisation de l'objet étudiant
Etudiant etudiantCharge = null;
FileInputStream fileIn = null;
ObjectInputStream in = null;
try {
fileIn = new FileInputStream("etudiant.dat");
in = new ObjectInputStream(fileIn);
etudiantCharge = (Etudiant) in.readObject();
System.out.println("=== APRÈS DÉSÉRIALISATION ===");
System.out.println(etudiantCharge);
} catch (IOException e) {
System.out.println("Erreur lors de la lecture : " + e.getMessage());
} catch (ClassNotFoundException e) {
System.out.println("Classe Etudiant non trouvée : " + e.getMessage());
} finally {
try {
if (in != null) {
in.close();
}
if (fileIn != null) {
fileIn.close();
}
} catch (IOException ex) {
System.out.println("Erreur lors de la fermeture des flux : " + ex.getMessage());
}
}
}
}

Sortie du programme

=== AVANT SÉRIALISATION ===
Étudiant : Marie Tremblay
Notes :
- 420-C27 - Programmation II : 8.0/10.0 (80.0%) - Vaut 40% de la session
- 420-C27 - Programmation II : 45.0/50.0 (90.0%) - Vaut 20% de la session
- 420-R12 - Réseaux : 15.0/20.0 (75.0%) - Vaut 30% de la session
- 420-B15 - Bases de données : 9.0/10.0 (90.0%) - Vaut 10% de la session
Total pondération : 100%
Moyenne pondérée : 81.50%

Étudiant sauvegardé dans etudiant.dat

=== APRÈS DÉSÉRIALISATION ===
Étudiant : Marie Tremblay
Notes :
- 420-C27 - Programmation II : 8.0/10.0 (80.0%) - Vaut 40% de la session
- 420-C27 - Programmation II : 45.0/50.0 (90.0%) - Vaut 20% de la session
- 420-R12 - Réseaux : 15.0/20.0 (75.0%) - Vaut 30% de la session
- 420-B15 - Bases de données : 9.0/10.0 (90.0%) - Vaut 10% de la session
Total pondération : 100%
Moyenne pondérée : 81.50%

Points importants à retenir

  • Serializable : Toutes les classes qui doivent être sérialisées doivent implémenter l'interface Serializable
  • serialVersionUID : Il est recommandé de définir un serialVersionUID pour assurer la compatibilité entre différentes versions des classes
  • Objets imbriqués : Les objets référencés (comme Cours dans Note) doivent aussi être Serializable
  • Fermeture des flux : Toujours fermer les flux dans un bloc finally pour éviter les fuites de ressources
  • Modèle logique : Dans cet exemple, un étudiant possède ses notes, et chaque note est composée d'un cours et d'une valeur - ce qui reflète la réalité

Concepts avancés

Le mot-clé transient

Le mot-clé transient permet d'exclure certains champs de la sérialisation. Cela est utile pour :

  • Les données sensibles (mots de passe)
  • Les données calculées ou dérivées
  • Les références à des ressources non sérialisables (connexions, fichiers ouverts)

Exemple :

import java.io.Serializable;

public class Utilisateur implements Serializable {
private static final long serialVersionUID = 1L;

private String nom;
private String email;
private transient String motDePasse; // NE sera PAS sérialisé
private transient int sessionCount; // NE sera PAS sérialisé

public Utilisateur(String nom, String email, String motDePasse) {
this.nom = nom;
this.email = email;
this.motDePasse = motDePasse;
this.sessionCount = 0;
}

// Après désérialisation, motDePasse sera null et sessionCount sera 0
}

Avantages et inconvénients de la sérialisation Java

Avantages

  • Facile à utiliser (juste implémenter Serializable)
  • Préserve automatiquement les relations entre objets
  • Gère les références circulaires
  • Format binaire compact

Inconvénients

  • Format binaire non lisible par les humains
  • Spécifique à Java (incompatible avec autres langages)
  • Fragile aux changements de structure des classes
  • Problèmes de sécurité potentiels avec la désérialisation
  • Performance moins optimale que des formats modernes

Recommandations

La sérialisation Java native est appropriée lorsque :

  • Vous travaillez exclusivement en Java
  • Vous avez besoin de préserver les graphes d'objets complexes
  • La performance binaire est critique
  • Les données sont temporaires (cache, sessions)