Les entrées/sorties Java (sérialisation, accès aux fichiers et
Transcription
Les entrées/sorties Java (sérialisation, accès aux fichiers et
Année 2008-2009 Les entrées/sorties Java (sérialisation, accès aux chiers et connexion réseau) Nicolas Baudru mél : [email protected] page web : nicolas.baudru.perso.esil.univmed.fr 1 Introduction Lors de la conception d'un logiciel, il n'est pas rare de vouloir lire ou écrire dans un chier (par ex., pour sauvegarder des données), ou bien se connecter à un serveur. Indépendamment de la méthode employée, vous utiliserez des techniques d'entrées/sorties (E/S ou I/O en anglais) qui sont pratiquement toujours les mêmes : on écrit ou on lit des données sur/via un support quelconque (généralement un chier ou une connexion réseau). Dans ce cours nous allons étudier ces techniques d'E/S via trois cas d'utilisation : I la sérialisation I la lecture ou l'écriture de/dans des chiers I les connexions réseaux A chaque fois, il nous faudra utiliser des ots d'E/S. 2 Le concept de ots L'API d'E/S Java comprend deux types de ots : I les ots de communication, qui représentent des destinations et des sources telles que des chiers ou des sockets réseau I les ots de traitement qui ne fonctionnent que s'ils sont chaînés à d'autres ots. En général, il est nécessaire de chaîner au moins deux ots : l'un pour représenter la connexion et l'autre pour les appels de méthodes. Exemple : FileOutputStream (voir plus loin) a des méthodes pour écrire des octets dans un chier. Mais comme nous voulons écrire des objets, il faut un ot de traitement de plus haut niveau. La possibilité de mélanger diérentes combinaisons de ots de communication et de traitement procure une très grande souplesse et permet de personnaliser l'utilisation de ces chaînages. 3 La sérialisation Supposons que vous écrivez un jeu (le jeu du zoo par exemple), et que vous souhaitez enregistrer l'état de votre jeu, pour le restaurer plus tard. Deux méthodes (au moins) sont disponibles : I I interroger chaque objet, puis enregistrer la valeur de leurs variables d'instance dans un chier que vous créez. Cette méthode est laborieuse mais le chier produit est lisible par l'homme et peut donc être lu par d'autres applications non-Java. ou le faire à la OO, c'est-à-dire en sérialisant les objets à sauvegarder. Cette méthode est plus ecace et plus adaptée au concept objet, mais le résultat est un chier qui n'a aucun sens pour l'homme. Il est cependant beaucoup plus facile et sûr pour votre programme de restaurer les chiers issus de la sérialisation plutôt que des chiers textes. 4 Écrire un objet sérialisé dans un chier 1 Créer un FileOutputStream. Cet objet sait comment se connecter à un chier et même en créer un. FileOutputStream fos = new FileOutputStream ( " MonZoo . ser " ); 2 Créer un ObjectOutputStream. Cet objet permet d'écrire des objets, mais il ne peut pas se connecter directement à un chier. Il a besoin pour cela d'un intermédiaire , ici un FileOutputStream. On va alors chaîner les deux ots, le ObjectOutputStream au FileOutputStream. ObjectOuputStream 3 Écrire oos = new ObjectOutputStream ( fos ); les objets à sauvegarder dans MonZoo.ser . oos . writeObject ( monChien ); oos . writeObject ( monCanard ); 4 Fermer ObjectOutputStream. La fermeture du premier ot entraîne automatiquement celle des autres. oos . close (); 5 Écrire un objet sérialisé dans un chier Objet cr té es 00101100 11010010 100 s an it d l'objet est sérialisé ObjectOutputStream é aîn ch transformé en octets et écrit dans à 0010110011010010100 FileOutputStream 6 Fichier Qu'est-ce qu'un objet sérialisé ? Les objets sur le tas ont un état. Cet état est représenté par la valeur de ses variables d'instance. Ce sont ces valeurs qui diérencient deux instances d'une même classe. La sérialisation sauvegarde la valeur des variables d'instance dans un chier, ainsi que quelques informations indispensables à la JVM pour restaurer l'objet (comme par exemple son type). Si les variables d'instance de l'objet sont de type primitif, alors la sauvegarde est simple. Mais que doit-on sauvegarder si certaines variables d'instance sont des références ? En fait, dans ce dernier cas, il faut aussi sérialiser tous les objets référencés. Et tous les objets que ces objets référencent doivent l'être aussi. Et ainsi de suite... Heureusement, tout ceci s'eectue de manière automatique par la JVM... 7 Que peut-on sérialiser ? Si vous voulez qu'une classe soit sérialisable, il faut qu'elle implémente l'interface Serialisable. Cette interface est dite de type marqueur car elle n'a aucune méthode à implémenter. Son seul rôle est d'annoncer à la JVM qu'elle est sérialisable. Si une classe est marquée sérialisable, toutes ses sous-classes le sont aussi par héritage. Pour pouvoir sérialiser un objet, il faut que tout objet référencé par l'une des variables d'instance de cet objet soit aussi sérialisable, et ainsi de suite. Si une variable d'instance ne peut pas être sauvegardée parce qu'elle n'est pas sérialisable, vous pouvez la déclarer transient. Dans ce cas le processus de sérialisation ignorera complètement cette variable. Pourquoi une variable ne serait-elle pas sérialisable ? 8 Que peut-on sérialiser ? Exemple import java . io .*; // Serialisable est dans ce package . public class Canin implements Serialisable { } public class Loup extends Canin { } public class Chien { } public class Coq { } public class BasseCour implement Serialisable { Coq c = new coq ; } public class Chenil implements Serialisable { transient Chien c = new Chien (); } Quels sont les objets sérialisables ? 9 Questions Pourquoi toutes les classes ne sont-elles pas sérialisables ? Que devient une variable transient lors de la restauration d'un objet ? Que se passe-t-il si plusieurs variables pointent sur un même objet ? Que signie avoir une sous-classe sérialisable alors que sa superclasse ne l'est pas ? 10 La désérialisation Tout l'intérêt de sauvegarder un objet est de pouvoir le restaurer le moment venu. Le processus de désérialisation est très similaire à celui de la sérialisation : 1 Chaîner un FileInputStream et un ObjectInputStream FileInputStream fis = new FileInputStream ( " MonZoo . ser " ); ObjectInputStream ois = new ObjectInputStream ( fis ); 2 A chaque invocation de readObject, l'objet suivant est récupéré. Object un = ois . readObject (); Object deux = ois . readObject (); 3 Convertir les objets. Loup monLoup = ( Loup ) un ; 4 Fermer les ots. ois . close (); 11 Le processus de désérialisation 1 L'objet est lu dans le ot. 2 La JVM détermine le type de l'objet et essaie de charger la classe de l'objet (elle lance une exception en cas d'échec). 3 Elle alloue au nouvel objet de l'espace sur le tas, mais n'appelle pas le construcuteur de l'objet sérialisé. 4 Si l'objet a une classe non-sérialisable quelque part dans son arbre généalogique, le constructeur de cette classe non-sérialisable s'éxécute (et donc tous les constructeurs des superclasses de cette classe non-sérialisable). Ainsi toutes les superclasses, à partir de la première non-sérialisable, vont réinitialiser leur état. 5 Les variables d'instance reçoivent les valeurs de l'état sérialisé. Les variables temporaires reçoivent null pour les références et la valeur par défaut pour les types primitifs. Les variables transient sont réinitialisées par un appel du constructeur adéquat. Les variables statiques12sont-elles sérialisées ? Numéro de version et désérialisation Que se passerait-il si une classe était modiée avant la désérialisation ? Modications pouvant empêcher la désérialisation : I Supprimer une variable d'instance, I ... Et quoi d'autres ? Modication sans problème : I Ajouter de nouvelles variables d'instance à la classe, I ... Et quoi d'autres ? Heureusement Java met en oeuvre un mécanisme d'estampillage de l'objet sérialisé par un numéro de version (serialVersionID) an de détecter tout changement. 13 Ecrire du texte dans un chier Ecrire du texte (des données de type String) dans un chier est similaire à l'écriture d'un objet sérialisé, sauf que l'on veut écrire des chaînes de caractères et non pas des octets dans le chier. Pour réaliser cela, il sut d'utiliser un FileWriter plutôt qu'un FileOuputStream. Puisque les chaînes de caractères vont être écrites tel quel, il n'est pas nécessaire d'utiliser un ot de traitement comme ObjectOuputStream. import java . io .*; // ce package contient FileWriter class EcrireTexte { public static void main ( String [] args ) { try { // tout le code E / S doit être dans un try / catch FileWriter fw = new FileWriter ( " MonFic . txt " ); // si ce fichier n ' existe pas , il sera créé fw . write ( " bonjour " ); fw . close (); // Ferme le flot } catch ( IOException ex ) { ex . printStackTrace ();} } } 14 La classe Java.io.File Cette classe représente un chier sur le disque, mais pas son contenu. Un objet File peut être vu comme le nom de chemin d'un chier ou d'un répertoire. Un objet File n'a donc pas de méthodes de lecture ou d'écriture, mais il possède une propriété très utile : il permet de représenter un chier de façon beaucoup plus sûre qu'un String. De plus vous pouvez par exemple vérier que le chemin est valide avant de le transmettre comme argument à une autre méthode. La plupart des classes de ots de données dont le constructeur accepte un String accepte aussi un objet de type File (ex. FileWriter, FileInputSream, ...). 15 Exemple d'utilisation de File Créer un objet représentant un chier existant : File f = new File ( " MonFic . txt " ); Créer un répertoire : File rep = new File ( " MonRep " ); rep . mkdir (); Lister le contenu d'un répertoire : if ( rep . isDirectory ()) { String [] contenuRep = rep . list (); for ( int i = 0; i < contenuRep . length ; i ++) { System . out . println ( contenuRep [ i ]); } } Obtenir le chemin absolu d'un chier ou d'un répertoire : System . out . println ( rep . getAbsolutePath ()); Supprimer un chier ou un répertoire : boolean supprime = f . delete (); 16 Les buers Les buers fournissent un contenant temporaire pour grouper les données jusqu'à ce qu'il soit plein. De cette façon, vous pouvez limiter le nombre d'accès au disque dur (qui prennent beaucoup de temps). "Marseille" String t es rit éc ns da "Marseille" "Paris" "Lille" BufferedWriter ns é aîn da ch "Marseille Paris Lille" FileWriter Lille Paris Marseille MonFic.txt Exemple : FileWriter fw = new FileWriter ( " monFic . txt " ); BufferedWriter bw = new BufferedWriter ( fw ); Dans cet exemple les données seront écrites sur le disque lorsque le buer sera plein. En eet, ce dernier transfèrera alors toutes ses données au FileWriter qui écrira toutes les données sur le disque en une seule fois. Si vous voulez envoyer les données avant que le buer soit plein, il sut de le vider par un appel de bw.ush(). 17 Lire du texte dans un chier import java . io .*; // A ne pas oublier class LireFichier { public static void main ( String [] args ) { try { File f = new File ( " MonFic . txt " ); FileReader fr = new FileReader ( f ); // Flot de communication pour les caractères , // qui se connecte à un fichier BufferedReader br = new BufferedReader ( fr ); // Flot de traitement pour les caractères ( buffer ). // Ce flot est chaîné au FileReader String ligne = null ; // contiendra chaque ligne while (( ligne = br . readLine ()) != null ) { System . out . println ( ligne ); } br . close (); } catch ( IOException ex ) { ex . printStackTrace ();} } } 18