oct.
2011
Guava par l'exemple (3/3) : I/O
Dans ce troisième et dernier article, je vous propose de découvrir les fonctionnalités de Guava relatives à la gestio des entrées/sorties (I/O).
Que ce soit pour réaliser des opérations systèmes (copie de fichier, déplacement...), ou pour travailler avec des fichiers textuels ou binaires, le package com.google.common.io
regorge de fonctionnalités pratiques.
Cet article fait partie d'une série :
Opérations systèmes
La classe utilitaire Files
fournit une API de haut niveau pour copier, déplacer, supprimer des fichiers et des répertoires (en attendant que Java 7 et son API Path
se démocratisent en entreprise) :
copy
etmove
, qui acceptent desFile
ou des streamsdeleteDirectoryContents
etdeleteRecursively
pour détruire des répertoires (même non vides).
Il est également possible de comparer deux fichiers bit à bit, et de calculer leur empreinte (CRC, MD5, SHA...) avec :
equal
, pour comparer deux fichiers de manière binairegetChecksum
etgetDigest
pour s'assurer de l'intégrité des fichiers
File tmpDir = new File(System.getProperty("java.io.tmpdir")+"/guava"); File sourceFile = new File(tmpDir, "source.txt"); File destFile = new File(tmpDir, "dest.txt"); File tmpSubDir = new File(tmpDir, "/subdir"); @Before public void prepareTest() throws IOException { tmpDir.mkdir(); tmpSubDir.mkdir(); sourceFile.delete(); assertTrue(sourceFile.createNewFile()); destFile.delete(); FileWriter fw = new FileWriter(sourceFile); fw.write("Hello World"); fw.close(); } @Test public void copyFile() throws IOException { assertTrue(sourceFile.exists()); assertFalse(destFile.exists()); Files.copy(sourceFile, destFile); assertTrue(sourceFile.exists()); assertTrue(destFile.exists()); } @Test public void moveFile() throws IOException { assertTrue(sourceFile.exists()); assertFalse(destFile.exists()); Files.move(sourceFile, destFile); assertFalse(sourceFile.exists()); assertTrue(destFile.exists()); } @Test public void deleteDirectories() throws IOException { Files.deleteDirectoryContents(tmpDir); assertFalse(sourceFile.exists()); assertFalse(destFile.exists()); assertTrue(tmpSubDir.exists()); Files.deleteRecursively(tmpDir); assertFalse(sourceFile.exists()); assertFalse(destFile.exists()); assertFalse(tmpSubDir.exists()); } @Test public void hashes() throws IOException, NoSuchAlgorithmException { long checksum = Files.getChecksum(sourceFile, new CRC32()); assertEquals(1243066710, checksum); byte[] digest = Files.getDigest(sourceFile, MessageDigest.getInstance("SHA")); assertArrayEquals( new byte[]{10, 77, 85, -88, -41, 120, -27, 2, 47, -85, 112, 25, 119, -59, -40, 64, -69, -60, -122, -48}, digest); } @Test public void equal() throws IOException { Files.copy(sourceFile, destFile); assertTrue(sourceFile.exists()); assertTrue(destFile.exists()); assertTrue(Files.equal(sourceFile, destFile)); }
I/O Texte
La lecture de fichiers textes a toujours été un peu pénible en Java. Guava fournit donc des méthodes très pratiques, qui simplifient ou masquent l'utilisation du BufferedReader
.
En écriture, nous avons donc :
append
pour ajouter immédiatement du texte à la fin d'un fichier.newWriter
pour obtenir unBufferedWriter
permettant d'écrire dans le fichier. Attention toutefois, le _writer_ n'ajoute pas de texte au fichier, il écrase son contenu. Pour compléter un fichier existant, utilisezappend
.
Et en lecture :
newReader
renvoie unBufferedReader
prêt à l'emploi.readFirstLine
pour ne lire que la première ligne du fichier.readLines
, qui propose deux versions : la première renvoyant uneList<String>
représentant les lignes du fichier, et la seconde acceptant unLineProcessor
permettant de traiter les lignes au fur et à mesure de leur lecture.toString
aspire tout le contenu du fichier dans un simpleString
.
public static final Charset CHARSET = Charset.forName("UTF-8"); public static final String DALTONS = "Joe Dalton\nJack Danton\nWilliam Dalton\nAverell Dalton"; File tmpDir = new File(System.getProperty("java.io.tmpdir")+"/guava"); File file = new File(tmpDir, "test.txt"); @Before public void prepareTest() throws IOException { tmpDir.mkdir(); file.delete(); assertTrue(file.createNewFile()); FileWriter fw = new FileWriter(file); fw.write(DALTONS); fw.close(); } @Test public void append() throws IOException { assertTrue(file.exists()); Files.append("\nMa Dalton", file, CHARSET); BufferedReader reader = new BufferedReader(new FileReader(file)); String line = reader.readLine(); reader.close(); assertNotNull(line); assertEquals(DALTONS +"\nMa Dalton", line); } @Test public void newBufferedWriter() throws IOException { // Warning : the returned Writer does not append to the file, it overwrites. // To append, use Files.append() instead BufferedWriter writer = Files.newWriter(file, CHARSET); writer.write("Ma Dalton"); writer.close(); BufferedReader reader = new BufferedReader(new FileReader(file)); String line = reader.readLine(); assertNull(reader.readLine()); reader.close(); assertNotNull(line); assertEquals("Ma Dalton", line); } @Test public void newBufferedReader() throws IOException { BufferedReader reader = Files.newReader(file, CHARSET); String line = reader.readLine(); reader.close(); assertNotNull(line); assertEquals("Joe Dalton", line); } @Test public void readFirstLine() throws IOException { String line = Files.readFirstLine(file, CHARSET); assertNotNull(line); assertEquals("Joe Dalton", line); } @Test public void readLines() throws IOException { List<String> lines = Files.readLines(file, CHARSET); assertNotNull(lines); assertEquals(4, lines.size()); assertEquals("Joe Dalton", lines.get(0)); } @Test public void readLinesWithLineProcessor() throws IOException { LineProcessor<Integer> charCounterProcessor = new LineProcessor<Integer>() { private int nbchars = 0; public boolean processLine(String line) throws IOException { nbchars += line.length(); return true; // Continue processing } public Integer getResult() { return nbchars; } }; Integer totalNbChars = Files.readLines(file, CHARSET, charCounterProcessor); assertEquals(DALTONS.length(), totalNbChars.intValue()); } @Test public void fileToString() throws IOException { String message = Files.toString(file, CHARSET); assertEquals(DALTONS, message); }
I/O Binaire
Les fichiers binaires ne sont pas oubliés non plus, même si les méthodes leur étant dédiées sont moins nombreuses :
toByteArray
, qui "aspire" tout le contenu du fichier dans unbyte
readBytes
, qui offre la possibilité de traiter les octets lus au fur et à mesure de leur lecture, via unByteProcessor
.
private static final byte[] DATA = {42, 42, 42, 42, 42}; File tmpDir = new File(System.getProperty("java.io.tmpdir")+"/guava"); File file = new File(tmpDir, "test.bin"); @Before public void prepareTest() throws IOException { tmpDir.mkdir(); file.delete(); assertTrue(file.createNewFile()); FileOutputStream fos = new FileOutputStream(file); fos.write(DATA); fos.close(); } @Test public void toByteArray() throws IOException { byte[] bytes = Files.toByteArray(file); assertArrayEquals(DATA, bytes); } @Test public void readBytes() throws IOException { ByteProcessor<Integer> byteAdder = new ByteProcessor<Integer>() { int total = 0; public boolean processBytes(byte[] bytes, int off, int len) throws IOException { for (int index = off; index < off + len; index++) { total += bytes[index]; } return true; // Continue processing } public Integer getResult() { return total; } }; Integer sumOfBytes = Files.readBytes(file, byteAdder); assertNotNull(sumOfBytes); assertEquals(42 * DATA.length, sumOfBytes.intValue()); }
Conclusion
Guava fait partie des "petites" librairies très pratiques, bien conçues et pouvant rendre de grands services.
Au cours de ces trois articles, nous avons exploré une sélection de classes et méthodes couvrant différents domaines : code courant, manipulation des collections, gestion des input/outputs... Et Guava propose encore plein d'autres options intéressantes que je vous encourage à découvrir.
Merci d'avoir suivi cette série !
Commentaires
Très bonne présentation de Guava que je ne connaissais que de nom.
Petite remarque, JUnit (>4.8 il me semble) fournit la classe TemporaryFolder et l'annotation @Rule pour créer un repertoire temporaire au début du test, ca évite de se soucier la creation/destruction de ce répertoire ;)
Merci pour l'astuce, que je ne connaissais pas. Je vais regarder ça !
Sympa comme article, merci :)
En passant, je citerai aussi FileBackedOutputStream, qui permet de faire d'obtenir un input stream d'un outputstream, le tout avec écriture dans un fichier si besoin afin de ne pas trop monopoliser de mémoire. Vraiment utile et bien plus pratique qu'un PipedOutputStream.
++
Pas si "petite" librairie que ça ... le footprint du jar guava 10.0.1 fait tout de même 1,4Mo ... ce qui me paraît pas mal pour une simple librairie de classes utilitaires.
C'est vraiment le seul inconvénient que je trouve à guava : si on ne souhaite utiliser qu'une partie des classes utilitaires (juste les io, ou les collections) il faut tout de même se tirer le tout.
Au départ de guava, il y avait plusieurs artefacts de dispo, et ils ont fait le choix de tout rassembler dans un seul artefact.
J'aurais préféré un pom "a la" spring avec les artefacts bien modularisés et un artefact type "guava-all" qui tire toutes les deps guava pour éviter les incompat entre versions de modules, ainsi que les déclarations de 50 lignes dans le pom.