Programmation objet avancée
Les entrées/sorties
Plan
●
Définition.
●
Les 3 questions.
●
Principes généraux d'utilisation.
●
Principes « réels » d'utilisation.
●
La sérialisation.
Définition
● flux : représentation abstraite d'une suite de données à destination ou en provenance d'une ressource (fichier, périphérique, mémoire, …)
Þ notion adaptée à la programmation objet Þ flux représenté par une classe
● type des données (du plus au moins simple) :
– octets,
– caractères,
Définition
Dans la plupart des langages, on différencie :
● flux de lecture = lecture des données en provenance de la ressource,
● flux d'écriture = écriture des données à destination de la ressource.
Remarque : normalement, la classe de flux ne dépend que du type de données et pas de la ressource.
Ce n'est pas le cas en Java !
Þ règles au cas par cas mais simples à choisir
Les 3 questions
● Pour choisir les bonnes classes de flux, adaptées au problème, on se pose 3 questions :
– quelle ressource (fichier, mémoire, socket, …) est employée ?
– quels types de données doivent être lu/écrits ?
– dans quel sens (lecture, écriture) va le flux ?
Remarque : dans 99 % des cas, ressource = fichier ou socket
Principes généraux d'utilisation
1. obtenir un objet représentant un flux d'octets : fichiers : FileInputStream, FileOutputStream
socket : InputStream, OutputStream
Remarques :
● lecture = Input, écriture = Output
● pour fichiers : instanciation explicite
FileInputStream fis = new FileInputStream("toto.txt");
● pour sockets : appel à une méthode de la classe Socket
InputStream is = sock.getInputStream();
Principes généraux d'utilisation
2. encapsuler l'objet flux d'octets dans une classe qui
« convertit » les octets en données plus structurées.
Þ objet flux = paramètre du constructeur de la classe encapsulante
Les classes encapsulantes dépendent du type à lire/écrire :
● caractères : InputStreamReader, OutputStreamWriter
● types primaires : DataInputStream, DataOutputStream
● objets : ObjectInputStream, ObjectOutputStream
Principes généraux d'utilisation
Exemple n°1 : lecture caractères dans fichier
Remarque : possibilité écriture « condensée »
File fich = new File("toto.txt");
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(fich); // flux d'octets sur fichier isr = new InputStreamReader(fis); // encapsulation
...
}catch(IOException e) {
System.out.println("pb lecture fichier : "+e.toString());
isr.close();
System.exit(1);
}
isr = new InputStreamReader(new FileInputStream("toto.txt"));
Principes généraux d'utilisation
Exemple n°2 : écriture objet sur socket
Socket sock = ...;
OutputStream os = null;
ObjetOutputStream oos = null;
try {
os = sock.getOutputStream(); // flux octets sur socket oos = new ObjectOutputStream(os); // encapsulation
...
}catch(IOException e) {
System.out.println("pb ecriture socket : "+e.toString());
oos.close();
System.exit(1);
Principes généraux d'utilisation
Remarques :
● flux d'octets/caractères : méthodes de lecture/écriture de tableaux d'octets/caractères Þ pas forcément pratique,
● flux objets : méthodes de lecture/écriture d'octets, caractères, données primaires et objets = réunion de toutes les possibilités,
● écriture avec un type de flux Þ lecture avec le même type.
Exemple :
écriture d'un double avec un ObjectOutputStream + lecture avec
DataInputStream Þ ERREUR !
Principes généraux d'utilisation
Remarques :
● gros volumes de données transmis en petits bouts Þ lectures/écritures peu performantes,
● un même flux peut être encapsulé dans plusieurs classes,
● « fermer » un flux = couper l'accès à la ressource Þ si plusieurs flux existants, tous sont fermés.
Principes « réels » d'utilisation
● simplicité = utiliser un seul type de flux, couvrant tous les besoins de l'application,
Þ en général, flux objet sauf si uniquement du texte
● simplicité = classes « auto » encapsulantes pour les fichiers
FileReader, FileWriter
● possibilités = classes « pratiques » pour manipuler le texte :
BufferedReader, PrintWriter/PrintStream
● performance = classes de bufferisation (mise en tampon) :
BufferedInputStream, BufferedOutputStream
Þ lecture/écriture quand volume cumulé suffisant
Principes « réels » d'utilisation
Exemples n° 3, 4, 5 :
●
lecture ligne de texte dans fichier
BufferedReader br = new BufferedReader(new FileReader("f.txt"));
String line = br.readLine();
PrintWriter pw = new PrintWriter("toto.txt");
int i=5; Date d= new Date(2013, 9, 13);
pw.println("nous sommes le " +d+ "et il fait " +i+ "degrés");
ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
●
écriture texte dans fichier
●
écriture bufferisée sur une socket
Principes « réels » d'utilisation
Exemple n°6 : lecture naïve d'un fichier binaire
File fich = new File("toto.txt");
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(fich); // flux d'octets sur fichier isr = new InputStreamReader(fis); // encapsulation
...
}
catch(IOException e) {
System.out.println("pb lecture fichier : "+e.toString());
isr.close();
System.exit(1);
}
File fich = new File("toto.txt");
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(fich); // flux d'octets sur fichier isr = new InputStreamReader(fis); // encapsulation
...
}
catch(IOException e) {
System.out.println("pb lecture fichier : "+e.toString());
isr.close();
System.exit(1);
}
import java.io.* ; class LitFichier {
public static void main(String[] args) { File fich = new File(args[0]);
byte[] tab = new byte[fich.length()];
FileInputStream fis = null;
try {
fis = new FileInputStream(fich);
fis.read(tab,0,fich.length());
}
catch(IOException e) {
System.out.println("pb lecture fichier "+e.toString());
System.exit(1);
} }}
Principes « réels » d'utilisation
Exemple n°6 bis : lecture+envoi fichier sur une socket
File fich = new File("toto.txt");
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(fich); // flux d'octets sur fichier isr = new InputStreamReader(fis); // encapsulation
...
}
catch(IOException e) {
System.out.println("pb lecture fichier : "+e.toString());
File fich = new File("toto.txt");
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(fich); // flux d'octets sur fichier isr = new InputStreamReader(fis); // encapsulation
...
}
catch(IOException e) {
System.out.println("pb lecture fichier : "+e.toString());
Socket sock = null;
FileInputStream fis = null;
byte[] tab = new byte[4096];
int nbLu;
try {
sock = new Socket(...);
OutputStream os = sock.getOutputStream();
fis = new FileInputStream(...);
while( (nbLu = fis.read(tab,0,4096)) >= 0) { os.write(tab,0,nbLu);
}
Principes « réels » d'utilisation
Remarques :
● Méthodes du type XXXX(...[] buf, int offset, int size) :
– offset est la première case de buf à être remplie/lue,
– size est le nombre de cases à lire/écrire.
● Sauf problème système/dépassement de tableau, les méthodes d'écriture écrivent autant d'octets/caractères que demandé.
● certaines méthodes d'écriture sont par défaut bufferisées (par exemple, celles de ObjectOutputStream).
● Pour forcer l'écriture des données en buffer : flush().
Principes « réels » d'utilisation
Remarques :
● lecture par défaut bloquante = s'il n'y a rien à lire, la méthode attend.
● ATTENTION ! la lecture prend ce qu'il y a de disponible Þ si demande de N octets, le buffer est rempli avec 0 < M <= N octets.
● Par exemple, int read(byte[] buf, int offset, int size) : renvoie le nombre d'octets réellement lus, peut être < size.
Principes « réels » d'utilisation
Exemple n°7 : lecture d'un fichier texte ligne par ligne
File fich = new File("toto.txt");
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(fich); // flux d'octets sur fichier isr = new InputStreamReader(fis); // encapsulation
...
}
catch(IOException e) {
System.out.println("pb lecture fichier : "+e.toString());
isr.close();
System.exit(1);
}
File fich = new File("toto.txt");
FileInputStream fis;
InputStreamReader isr;
try {
fis = new FileInputStream(fich); // flux d'octets sur fichier isr = new InputStreamReader(fis); // encapsulation
...
}
catch(IOException e) {
System.out.println("pb lecture fichier : "+e.toString());
isr.close();
System.exit(1);
}
import java.io.* ; class LitTexte {
public static void main(String[] args) { BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(args[0]));
String line = br.readLine();
while (line != null) {
System.out.println(line);
line = br.readLine();
} }
catch(IOException e) {
System.out.println("pb lecture fichier "+e.toString());
System.exit(1);
} }
La sérialisation
● objet contient attributs Þ transmettre un objet suppose au minimum de transmettre la valeur de ses attributs.
● sérialisation : compactage des valeurs des attributs + informations internes d'un objet, sous forme d'un tableau d'octets.
● désérialisation : décompactage d'un tel tableau pour créer un objet.
● envoyer/recevoir des objets Þ mettre en place un mécanisme de (dé)sérialisation des objets.
La sérialisation
● En Java, pas besoin d'écrire ces méthodes : le compilateur peut les générer automatiquement.
Þ ajout de l'interface Serializable. Exemple :
class MaClasse { ...
}
class MaClasse implements Serializable { ...
}
● Grâce au changement de déclaration, les objets de la classe peuvent être envoyés/reçus sur un flux objet.
Remarque : la plupart des classes de l'API Java sont déjà Serializable.
Þ
La sérialisation
● lecture d'un objet : readObject() renvoie un Object
→ transtyper (= cast) l'objet reçu au bon type Exemple :
MaClasse m = new MaClasse();
ObjectOutputStream oos;
try {
oos = ...;
oos.writeObject(m);
oos.flush();
...
}
MaClasse m;
ObjectInputStream ois;
try {
ois = ...;
m =(MaClasse)ois.readObject();
...
}
catch(IOException e ) { ... }
La sérialisation
Remarques :
● En cas de composition, la (dé)sérialisation ne fonctionne que pour les classes (dé)sérialisables.
● La plupart des classes de l'API Java sont sérialisables, notamment les collections Þ lire/écrire une collection d'objets sérialisables = une seule ligne de code.
class B { int i;
}
class A implements Serializable {
B b; // b ne sera pas (dé)sérialisé double d; // d sera (dé)sérialisé }
List<A> l=new ArrayList<A>();
ObjectOutputStream oos=...;
oos.writeObject(l); // OK
class A implements Serializable { ...
}
La sérialisation
Remarques (bis) :
● Le fait d'utiliser un flux objet pour envoyer des types primaires (avec writeInt(), writeDouble(), …) ne fait pas appel aux mêmes principes de sérialisation que les objets.
● Cependant, contrairement à DataOutputStream, les types primaires ne sont pas envoyés tels quels mais avec une
« entête » indiquant leur type → relecture obligatoire avec un flux objet.
● Qui plus est, l'envoi est bufferisé → flush() pour forcer l'envoi si besoin.