===================== Fichiers en JAVA ===================== Jean Vaucher Universite de Montreal Version 1.3 (mar 2005) (v1.0: Fev. 1999 ) En Java, il existe tellement de classes pour faire des entrees-sorties (ES) qu'il est difficile de savoir lesquelles utiliser. De plus, il faut normallement creer et combiner 2 ou trois objets de ces classes pour faire quoi que ce soit. Le present article explique la raison d'etre de divers groupes de classes et montre comment faire dans les cas habituels. D'abord le nombre de classes d'ES depend de la diversite inherentes aux entree-sorties. Pour decider quelle classe utiliser il faut savoir: Le sens de l'echange: LECTURE ou ECRITURE (ou acces direct) Le medium externe : disque, terminal, acces WEB Le codage de l'information: lignes de caracteres ou du binaire En plus, Java essaie d'anticiper le contexte international des communications futures en utilisant les caracteres "unicode" de 16 bits qui supporte la majorite des alphabets de la planete ( latin, arabe, chinois, inuit, etc...). Ceci double le nombre de classes de fichiers, car il faut prevoir non seulement la lecture (et ecriture) de vieux fichiers de textes en ASCII (codes avec des octets) mais aussi le traitement de textes multi-lingues codes avec des caracteres Unicode qui prennent - en format externe - 1, 2 ou 3 octets. Java utilise le terme STREAM (Flux) pour designer des suites d'OCTETS et READER/WRITER pour designer les suites de CARACTERES. On peut facilement s'imaginer que si on prevoit une classe pour chaque combinaison imaginable de fichier, on aura un tres grand nombre de classes. En Java, il a ete decide de separer les divers aspects d'un flux de donnees en groupes de classes "orthogonales" qui doivent etre combinees en pipeline. Dans plusieurs cas, il y a dedoublement entre de vieilles classes Stream qui traitent les caracteres ASCII et les nouvelles versions (Reader/Writer) pour le Unicode.Malgre cette idee louable, le resultat reste tres lourd: Java se retrouve avec 44 classes de base et l'usager doit creer et combiner 2 ou trois objects de ces classes pour ouvrir un fichier. Un dernier aspect surprenant des ES en Java est que certaines operations qui sont tres faciles a faire avec des langages plus vieux comme Pascal, Fortran, Basic et meme C, sont impossible a realiser simplement en Java. En particulier, nous parlons de la lecture de nombres en format libres ou de l'impression selon un format donne, p.e. "cadres a droite dans un champ de 10 caracteres avec 1 decimale" [En fait ces choses sont possibles mais exigent l'apprentissage de classes additionnelles - nous montrons comment faire plus loin !!! ]. Par contre, avec une seule instruction, on peut ecrire sur disque (et relire plus tard) une structure de donnee complexe comprenant une centaine d'objets relies par des pointeurs. STREAMS ======= Le Stream (flux en francais) est le concept de base des entrees/sorties en Java. Un STREAM represente une suite d'octets qui sont accedes un a un. Il y a InputStream pour la lecture et OutputStream pour l'ecriture. Ces classes sont 'abstraites' et servent a specifier les methodes communes a des sous-classes 'concretes' qui, elles, sont adaptees a diverses sources et destinations d'octets, par exemple: fichier, tableau d'octets, String, socket, pipe, etc.... Java implante aussi des classes READER et WRITER pour gerer des suites de CARACTERES (Unicode) de 16 bits. A l'externe, ces caracteres sont codees par des suites d'octets et les classes READER/WRITER feront appel aux STREAMs pour le transfert des octets. Voici les methodes fournies par les Streams. InputStream (classe abstraite) ------------------------------ - read() -> int Note: retourne -1 quand on rencontre FDF - read( byte [] ) lecture dans un buffer - close() : a appeler pour liberer les ressources OutputStream (classe abstraite) -------------------------------- - write(int byte) - flush() - close() Les classes InputStream et OutputStream etant abstraites, on ne les utilisent pas directement. Pour faire des ES utiles, il faut normallement creer et combiner 2 objets: d'abord un flux 'concret' (sous-classes de Stream) qui depend du support externe des octets et ensuite d'un objet 'fonctionnel' qui va interpreter les octets de diverses facons. Le TERMINAL est represente par 2 objets predefinis: "System.in" qui correspond au clavier et "System.out" qui represente l'ecran. Dans ce qui suit, nous allons examiner d'abord la LECTURE, puis l'ECRITURE LECTURE ======== Il faut d'abord decider de la source des octets: le flux concret. Voici quelques classes prevues pour lire sur: - un fichier: FileInputStream <<< usage courant - un string: StringBufferInputStream - un tableau d'octets: ByteArrayInputStream Pour lire de la CONSOLE, on utilise un objet predefini: "System.in". Il y a une grande variete de classes d'entree; mais, pour nos fins, nous nous bornerons a l'emploi de "System.in" et de "FileInputStream". FileInputStream --------------- Ce type d'objet sert a lire d'un fichier. A la creation, on doit fournir une indication du 'fichier' avec: - un String - un objet "File" (voir plus loin pour emploi des "File") Exemples: InputStream in = new FileInputStream("data1"); <<< Usage courant ou: File fh = new File("data1"); InputStream in2 = new FileInputStream( fh ); Methodes: 'read' ( d'un octet ) et 'close' Classes Fonctionnelles ====================== Les objets "fonctionnels" ne font que composer ou interpreter des suites de caracteres venant d'un flux concret. A la creation, il faut donc fournir un objet de classe INPUTSTREAM ( qui peut etre un flux standard comme System.in). InputStreamReader ------------------ Fait la lecture de caracteres UNICODE - en pratique c'est de l'ASCII - mais si on tombe sur un octet avec code > 127 on va le combiner avec les caracteres qui suivent pour retourner un caractere UNICODE de 16 bits. - read() -> int qui est un caractere de 16 bits ou -1 pour FDF - read( char[] ) ... notez qu'on lit dans un tableau de CARACTERES et pas de BYTE comme avec un inputSTREAM. Autrement meme fonctionalite que "InputStream". En particulier, on ne peut pas lire une LIGNE d'un coup! Exemples: InputStreamReader in = new InputStreamReader( new FileInputStream("data")); InputStreamReader in = new InputStreamReader( System.in ); int i = in.read(); if ( C != -1) char C = (char) i ; // Notez qu'il faut "caster" vers un char // char c = in.read() donnerait une erreur BufferedReader -------------- Finallement, on arrive a la classe souvent utilisee dans les livres d'introduction a Java. Un BufferedReader implante un tampon pour chercher une suite d'octets a chaque appel du systeme d'exploitation. Il est dit dans la documentation que le BufferedReader augmente l'efficacite des ES; mais, en pratique, on l'utilise pour pouvoir lire LIGNE par LIGNE. A la creation d'un fichier de ce type, il faut fournir objet READER qui lui meme exige un objet STREAM. Ceci donne la sequence classique: BufferedReader in = new BufferedReader( new InputStreamReader( System.in )) ; String s = in.readLine(); ** readLine() --> String Retourne la prochaine ligne, excluant les caracteres de fin de ligne. Retourne "null" si on a depasse la Fin des donnees. Note ceci est different d'une chaine vide "" pour une ligne sans caracteres. ** read() --> int On peut aussi lire caracteres par caracteres. FileReader / FileWriter ------------------------ Ces deux classes simplifient l'ES de caracteres (Unicode) sur des fichiers. Strictement, pour lire des lignes de caracteres d'un fichier "data", on devrait dire: BufferedReader in = new BufferedReader( new InputStreamReader( new FileInputStream( "data"))) ; C'est a dire, creer un inputStream d'octets sur le fichier, le passer au Reader qui combine les octets en caracteres pour ensuite alimenter un Buffer. Un FileReader cree lui-meme le FileInputStream etant donne un nom de fichier. Ooouuufff, c'est a peu pret temps qu'on nous simplifie la vie ! On dira donc: BufferedReader in = new BufferedReader( new FileReader( "data")) ; De la meme facon, on pourra dire: PrintWriter out = new PrintWriter( new FileWriter( "Data.out" )); OUTPUT ======= Pour la sortie d'information, la classe abstraite, OutputStream, specifie les operations permises. OutputStream: ------------- - write(int byte) - write( byte [] ) - flush() - close() Les classes concretes correspondantes sont: FileWriter ou FileOutputStream System.out ou System.err La classes fonctionnelle la plus utile est "PrintWriter" qui peut convertir des valeurs internes (pe entier) en format lisible standard. PrintWriter ----------- C'est la classe la plus utile pour faire des sorties "lisibles". Les methodes disponibles sont: - print(...) - println (...) L'"overloading" est utilise pour permettre un formattage different dependant du type du parametre utilise. On peut "printer": - boolean, char, byte, short, int, long, float, double, String et Object Par contre, il est difficile de fixer la taille des champs d'edition ou de fixer le nombre de decimales comme avec Pascal ou C. Il existe aussi une classe "PrintStream", qui traite seulement des caracteres ASCII mais, pour les besoins futurs, il est suggere d'utiliser PrintWriter [ Note: System.out et System.err sont de type PrintStream ] Exemple: Ecriture sur le fichier "data" PrintWriter f = new PrintWriter( new FileWriter ("data")) ; f.print("1 + 1 = "); f.println( 1+1 ); f.close(); EXEMPLE COMPLET =============== L'exemple fait une copie F2 d'un fichier F1 ou F1 et F2 sont specifies en parametres. A la difference des fragments presentes plus haut, ici on ajoute le "import" des classes d'IO et on met le code dans un bloc TRY...CATCH. // Programme qui copie les lignes // de a // % java Copie import java.io.*; public class Copie { public static void main(String args[]) { BufferedReader in; PrintWriter out; try { if (args.length > 0) in = new BufferedReader( new FileReader( args[0] )) ; else in = new BufferedReader( new InputStreamReader( System.in )) ; if (args.length > 1) out = new PrintWriter( new FileWriter (args[1])) ; else out = new PrintWriter( System.out ); String line; while ((line = in.readLine()) != null) out.println(line); in.close(); out.close(); } catch (Exception e) { System.err.println("*** ERR: " + e.getMessage()); } } } =========================================================================== Formattage de nombres =========================================================================== Voici une petite methode "toString" qui peut vous faciliter la vie. Elle convertit une valeur "double" en String. Elle a 3 parametres: X, la valeur a imprimer, N, le nombre de chiffres decimaux et W, la largeur du champ dans lequel vous voulez editer le nombre. Normalement, le nombre est cadre a droite, mais si W est negatif, alors le nombre est cadre a gauche. D'abord un exemple d'emloi. double x = 1.0 / 7; for (int n = 1; n<5; n++) System.out.println( "1/7 : " + toString( x, n, -6) + toString( x, n, 10)); qui donne: 1/7 : 0.1 0.1 1/7 : 0.14 0.14 1/7 : 0.143 0.143 1/7 : 0.1429 0.1429 Et voici le code de la methode: static final String ZEROES = "000000000000"; static final String BLANKS = " "; static String toString( double val, int n, int w) { // rounding double incr = 0.5; for( int j=n; j>0; j--) incr /= 10; String s = Double.toString(val + incr); int n1 = s.indexOf('.'); // chiffres avant la decimale int n2 = s.length() - n1 - 1; // chiffres apres // traitement de la partie decimale if (n>n2) s = s+ZEROES.substring(0, n-n2); else if (n2>n) s = s.substring(0,n1+n+1); // cadrage if( w>0 & w>s.length() ) s = BLANKS.substring(0,w-s.length()) + s; else if ( w<0 & (-w)>s.length() ) { w=-w; s = s + BLANKS.substring(0,w-s.length()) ; } return s; } Pour ceux qui veulent faire selon les "normes" Java, consulter la page suivante avec les conseils de Guy Lapalme. http://www.iro.umontreal.ca/~vaucher/Java/tutorials/Formatting.html =========================================================================== Classe "File" (modifiee: nov. 1999) =========================================================================== Contrairement a ce qu'on pourrait penser, un objet "File" ne represente pas le CONTENU d'un fichier mais les informations sur un fichier donne. On cree un file en donnant une chaine de caractere qui est generallement un PATH a partir du repertoire courant vers un fichier [ ATTENTION ! ca pourrait etre n'importe quelle chaine car FILE ne fait AUCUNE verification ]. Voici un exemple: File f = new File("tmp/toto.java"); On peut aussi combiner un nom de repertoire et un nom de fichier File f = new File("tmp", "toto.java"); Acces au nom ------------ NOTE: a ce niveau, tout se joue sur la chaine de caracteres fournie pour la creation du File...il n'y a AUCUNE verification. Pour l'exemple, avec le repertoire courant: "/u/Fred, et la creation ci-haut: f.getPath() => "tmp/toto.java" - retourne la chaine fournie f.getName() => "toto.java" - donne la partie a droite du "/" f.getParent() => "tmp" - donne la partie a gauche du "/" f.getAbsolutePath() => "/u/Fred/tmp/toto.java" - combine le repertoire courant avec le path. Notez qu'avec un path IMAGINAIRE: f = new File("a/.. "a/.. "z.ZZ" f.getParent() => "a/.. "/u/Fred/a/.. boolean) f.exists() f.canRead() f.canWrite() f.isHidden() ( introduit avec Java 2 ) Acces aux statistiques f.length() => taille du fichier f.lastModified() => taille du fichier Modification f.delete() f.renameTo( String newName) Gestion de repertoires f.mkdir() = "mkdir" f.mkdirs() : au besoin construit tous les sur-repertoires necessaires f.getParent() => Nome du repertoire environnant (String ) d.list() => tableau des noms de fichiers du repertoire "d" ON peut donc lister le contenu d'un repertoire public static void main(String [] arg) throws IOException { ListeFile(new File(arg[0]),""); } static void ListeFile(File d, String tab) throws IOException { System.out.println( d.getPath() ); if (d.isDirectory()) { String [] contents = d.list(); for (int i=0; i byte - readChar() -> char - readInt() -> int - readShort() -> short - readLong() -> long - readFloat() -> int - readDouble() -> double - readLine() -> String ... la ligne sans les caracteres de fin de ligne ( CR et/ou LF ). L'emploi de cette fonction est decouragee. Il est suggere d'utiliser Exemple: DataInputStream in = new DataInputStream( new FileInputStream("data") ); int i = in.readInt(); i = 0; // si on ne tiend pas compte des signes for (int j = 1 ; j <=4 ; ++j) // ce code est equivalent a in.readInt() i = 256*i + in.read(); in.close(); DataOutputStream ----------------- Inverse de la classe precedente. Permet d'ecrire le contenu (en binaire) de variables des differents types de base de Java. Note: pour lire/ecrire des OBJETS , il faut utiliser d'autres types de stream: ObjectInputStream et ObjectOutputStream . - writeByte(byte b) - writeBytes( String s ) <<<< 8 ou 16 bits / cara ??? - writeChar( C ) - writeChars( String ) - write....Short(),...Int(), Float(),... Long()