Java.io.File, Google Guava und Apache Commons IO im Vergleich Blog

Helfer fürs Dateienmanagement

Anzela Minosi
5. Dec 2022

Dank weiterführender Bibliotheken wie Java.io.File, Apache Commons IO oder Google Guava ist es möglich, plattformübergreifende Features im Java-Programm aufrechtzuerhalten. In diesem Artikel lernt ihr, wie diese Bibliotheken eingesetzt werden können.

Auf der Low-level-Ebene kann es vorkommen, dass das Bearbeiten von Dateien unter Unix funktioniert, nicht aber unter Windows – und umgekehrt. Das passiert vor allem dann, wenn Dateien bzw. Pfade aus Strings bestehen und nicht auf Klassen zurückgegriffen wid, die bereits auf Herz und Nieren geprüft wurden. Unter Java stehen für den Umgang mit Dateien mehrere Bibliotheken zur Verfügung.

Java.io.File

Java.io.File ist bereits in der Java-Runtime-Bibliothek enthalten. Zum Einlesen und Erstellen von Dateien werden Streams verwendet (Tabelle 1). Es wird zwischen Text und Bytes unterschieden – so eignet sich die Klasse FileInputStream unter anderem zum Einlesen von Bildern, während die Klasse FileReader Textdateien zu Strings weiterverarbeitet. Allerdings kann die Klasse FileInputStream beide Arten von Daten als Stream zur Verfügung stellen. So muss der Klasse FileInputStream lediglich der Pfad der Datei als String übergeben werden. Beim Beschreiben von Daten wird ähnlich wie beim Einlesen differenziert. Die Klasse FileOutputStream schreibt bytebasierte Daten in eine Datei, während die Klasse FileWriter Textdateien erstellt. Doch beherrscht die Klasse FileOutputStream ebenfalls das Erstellen von Textdateien.

Klasse Funktion
File Dateien bearbeiten
FileInputStream Dateien einlesen
FileOutputStream in Dateien schreiben, Dateien kopieren

Tabelle 1: Wichtige Klassen unter Java.io.File

Ausnahmen spielen bei der Verarbeitung von Dateien eine wichtige Rolle, da damit verbundene Fehler erst zur Programmlaufzeit auftreten. Um Ausnahmen zu vermeiden, könnt ihr zuerst danach fragen, ob überhaupt eine Datei existiert (Listing 1).

public boolean fileExists(String path) {
  File file = new File(path);
  boolean exists = file.exists();
  return exists;
}

So käme es im folgenden Beispiel zu einer Ausnahme, wenn eine nichtexistierende Datei an einen neuen Ort kopiert werden würde. Da die Methode fileExists() das aber vorher überprüft, tritt keine Ausnahme auf (Listing 2).

public void copyFile(String sFrom, String sTo) {
  FileInputStream in = null;
  FileOutputStream out = null;
  try {
    if (fileExists(sFrom)) {
      in = new FileInputStream(sFrom);
      out = new FileOutputStream(sTo);
      int c;
      while ((c = in.read()) != -1) {
        out.write(c);
      }
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }finally {
    try {
      if (in != null) {
        in.close();
      }
      if (out != null) {
        out.close();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

In Listing 2 wird die eingelesene Datei zunächst zu Bytes verarbeitet, ehe sie als Kopie im FileOutputStream landet. Sobald der FileOutputStream geschlossen ist, wird eine neue Kopie der Quelldatei auf der Festplatte angelegt. Allerdings solltet ihr vor dem Schließen eines Streams klären, ob die Streams instanziiert worden sind, da es sonst zu Ausnahmen kommt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Nun können Streams nicht nur Daten bereitstellen beziehungsweise verarbeiten, sondern auch deren Größe ermitteln. Hierzu wendet ihr die Methode available() auf eine Instanz des Typs InputStream an. Anschließend schließt ihr den InputStream, um Ressourcen freizugeben (Listing 3).

public int getFileSize(String path) {
  int size = -1;
  FileInputStream in = null;
  try {
    in = new FileInputStream(path);
    size = in.available();
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  } finally {
    try {
      if (in != null) {
        in.close();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  return size;
}

Neben den Streams beherbergt das Java.io.File-Paket die Klasse File, die den vollständigen Pfad einer Datei ausgeben kann. Zudem existieren im selben Paket Konstanten wie File.Separator, die anstelle von systemabhängigen Trennsymbolen wie dem Slash unter Linux benutzt werden sollte.

Abgesehen davon kann die File-Klasse Dateien löschen. Die folgende selbst definierte Methode funktioniert auch dann ausnahmslos, wenn die eigentliche Datei gar nicht existiert:

public boolean delFile(String path) {
  File file = new File(path);
  return file.delete();
}

Darüber hinaus beherbergt die File-Klasse Methoden, um den Inhalt eines Verzeichnisses auszugeben (Listing 4).

public void listFolder(String sFolder) {
  String [] paths;
  try {
    File file = new File(sFolder);
    paths = file.list();
    for(String path:paths) {
      System.out.println(path);
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
}

Dazu wird der Ordnername als String einer Instanz vom Typ File übergeben. Die auf diese Instanz angewandte Methode list() ermittelt anschließend die Dateien und Unterordner des angegebenen Verzeichnisses. Da diese Methode eine Liste zurückgibt, kann sie in einem Array gespeichert und der Inhalt kann beim Iterieren ausgegeben werden. So zeigt der Aufruf der Methode listFolder() den Inhalt des aktuellen Verzeichnisses an:

listFolder(".")

Google Guava

Das IO-Paket von Google Guava [1] beherbergt etliche Klassen, die das Arbeiten mit Dateien erleichtern. So lässt sich eine Datei mit nur einer Zeile Code erstellen, indem die Klasse Files zum Einsatz kommt (Listing 5).

public File createFile(String path) {
  File f = new File(path);
  try {
    Files.touch(f);
  } catch (IOException e) {
    e.printStackTrace();
  }
  return f;
}

Die Unterschiede zwischen Guava und Java.io.File machen sich vor allem beim Einlesen und Bearbeiten von Dateien deutlich bemerkbar (Tabelle 2).

Klasse Funktion
Files Dateien bearbeiten
ByteSource bytebasierte Daten einlesen
ByteSink bytebasierte Daten schreiben
CharSource Text einlesen
CharSink Text schreiben
Resources Methoden für den Resources-Ordner

Tabelle 2: Wichtige Klassen in Google Guava

Dateien lassen sich unter Guava auf unterschiedliche Art und Weise einlesen. Sofern sich die Dateien im Klassenpfad src/main/resources befinden, können sie mittels der Klasse Resources [2] importiert werden (Listing 6). Anschließend lassen sie sich als String ausgeben beziehungsweise weiterverarbeiten.

public String readFromresource(String path) {
  String sContent = "";
  URL url = Resources.getResource(path);
  try {
    sContent = Resources.toString(url, Charsets.UTF_8);
  } catch (IOException e) {
    e.printStackTrace();
  }
  return sContent;
}

Ihr könnt genauso gut Dateien, die sich nicht im Klassenpfad befinden, als Strings abspeichern. Zunächst wird ein neues File-Objekt für einen Dateipfad erstellt, um es mittels der statischen Methode toString() aus der Files-Klasse in einen String umzuwandeln (Listing 7).

public String readToString(String path) {
  File f = new File(path);
  String sContent = "";
  try {
    sContent = Files.toString(f, Charsets.UTF_8);
  } catch (IOException e) {
    e.printStackTrace();
  }
  return sContent;
}

Ähnlich wie Java.io.File unterscheidet Google Guava zwischen text- und bytebasierten Daten. So lässt sich eine Textdatei zunächst als eine Instanz vom Typ CharSource einlesen und anschließend zum BufferedReader weiterverarbeiten. Wenn mittels der Instanz CharSource die Methode toString() aufgerufen wird, kann die eingelesene Datei zudem als String ausgegeben werden (Listing 8). Sowohl die Klasse CharSource als auch das Pendant, genannt ByteSource, stellen zur Ermittlung der Dateigröße geeignete Methoden zur Verfügung.

public void readInText(String path) {
  File f = new File(path);
  try {
    CharSource bs = Files.asCharSource(f,Charset.defaultCharset());
    BufferedReader reader = bs.openBufferedStream();
    String content = bs.toString();
    System.out.println("File copied. Size: "+ bs.length());
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Daneben existieren zwei verschiedene Lösungen, um bytebasierte Daten zu importieren. Einerseits lassen sich Dateien in einer Instanz vom Typ ByteSource abspeichern. Anschließend können diese zu einem InputStream weiterverarbeitet werden (Listing 9).

public void readInBytes(String path) {
  File f = new File(path);
  try {
    ByteSource bs = Files.asByteSource(f);
    InputStream in = bs.openBufferedStream();
    System.out.println("File copied. Size: "+ bs.size());
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Andererseits kann eine Datei zunächst mittels einer Instanz des Typs FileInputStream eingelesen werden, um sie anschließend mittels der statischen Methode ByteStreams.toByteArray() in ein Bytearray umzuwandeln (Listing 10). In diesem Fall lässt sich die Größe der Datei ermitteln, indem die Methode length() auf das Bytearray angewendet wird.

public void readInStream(String path) {
  try {
    FileInputStream fIn = new FileInputStream(path);
    byte [] bytes = ByteStreams.toByteArray(fIn);
    fIn.close();
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Abgesehen davon kommt es bei Guava zu Ausnahmen, falls nicht vorhandene Dateien eingelesen werden. Das betrifft zudem das Kopieren von Dateien. Ausnahmen lassen sich verhindern, indem die Methode fileExists() (Listing 1) prüft, ob die Quelldatei vorhanden ist. Daneben solltet ihr abfragen, ob Quell- und Zieldatei identisch sind.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Die einfachste Kopiermethode beinhaltet wiederum die Klasse Files, die zwei Argumente vom Typ File entgegennimmt (Listing 11).

public void copy(String sFrom, String sTo) {
  File f1 = new File(sFrom);
  File f2 = new File(sTo);
  try {
    if(fileExists(sFrom) && !(Files.equal(f1,f2))) {
      Files.copy(f1, f2);
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Andererseits lässt sich eine Datei kopieren, indem ein FileOutputStream für die Zieldatei erstellt wird und dieser zusammen mit der Quelldatei in Form eines File-Objekts als Argument der statischen Methode Files.copy übergeben wird. Auch hier ist der Code einfach gehalten (Listing 12).

public void copyToStream(String sFrom, String sTo) {
  File f1 = new File(sFrom);
  try {
    if(fileExists(sFrom) ) {
      FileOutputStream out = new FileOutputStream(sTo);
      Files.copy(f1, out);
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Neben dem Kopieren und Einlesen beherrscht die Klasse Files das Bearbeiten von Dateien. In Listing 13 werden Daten in eine Zieldatei geschrieben, wobei diese von einem Bytearray stammen.

public void write(String sTo, byte [] bytes) {
  File to = new File (sTo);
  try {
    Files.write(bytes,to);
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Die folgenden zwei Schreibbefehle unterscheiden wiederum zwischen Text und Bytes, wobei das Schreiben in beiden Fällen gleich aufgebaut ist. Im Gegensatz zum Einlesen kommt beim Schreiben von Text die Klasse CharSink zum Einsatz. Dabei wird zunächst je ein Objekt vom Typ File für die Quell- und Zieldatei angelegt. Anschließend wird eine Instanz vom Typ CharSink auf Basis der Zieldatei erstellt und der Zeichensatz der neuen Textdatei festgelegt. Den Schreibbefehl löst der Aufruf der Methode write() aus, in dem der Inhalt der Quelldatei als String übergeben wird (Listing 14).

public void writeText(String sFrom, String sTo) {
  File f1 = new File(sFrom);
  File f2 = new File(sTo);
  CharSink bsink = Files.asCharSink(f2,Charset.defaultCharset());
  try {
    bsink.write(Files.toString(f1, Charset.defaultCharset()));
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Das Schreiben von Bytes hingegen regelt die Klasse ByteSink (Listing 15), wobei der Schreibvorgang Listing 14 ähnelt.

public void writeBytes(String sFrom, String sTo) {
  File f1 = new File(sFrom);
  File f2 = new File(sTo);
  ByteSink bsink = Files.asByteSink(f2);
  try {
    bsink.write(Files.toByteArray(f1));
  } catch (IOException e) {
  e.printStackTrace();
  }
}

Google Guava hat weitere nützliche Features auf Lager. So beherbergen die Klassen, die sich zum Einlesen von Textdateien eignen, die Methode readLines(), um damit die Zeilen einer Textdatei als Stringliste abzuspeichern. Mittels der statischen Methoden Files.isDirectory() beziehungsweise Files.isFile() lässt sich außerdem feststellen, ob ein angegebener Pfad auf ein Verzeichnis oder eine Datei zeigt. Auch sind Methoden vorhanden, die sich mit Dateiformaten beziehungsweise Dateinamen beschäftigen. So geben die folgenden Statements das Dateiformat und den Dateinamen ohne dessen Erweiterung aus:

Files.getFileExtension("input.txt");
Files.getNameWithoutExtension("input.txt");

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Apache Commons IO

Apache Commons IO bringt zwei Klassen mit (Tabelle 3), die für die Bearbeitung von Dateien und Ordnern genutzt werden können, nämlich FileUtils [3] und FilenameUtils [4].

Klasse Funktion
FileUtils Operationen an Dateien und Ordnern
FilenameUtils Dateinamen und -formate

Tabelle 3: Wichtige Klassen unter Apache Commons IO

Die dazugehörigen Methoden lassen sich zum Teil ohne Ausnahmebehandlung aufrufen, was die Länge des Codes reduziert. So kann mit nur einer Zeile Code abgefragt werden, ob eine Datei auf der Festplatte vorhanden ist:

public boolean myFileExists(String path) {
  return FileUtils.isRegularFile(new File(path), LinkOption.values());
}

Das zweite Argument der Methode FileUtils.isRegularFile ist vom Typ LinkOption und definiert, ob der Test auch für symbolische Links gelten soll. In obigem Code ist das zwar der Fall, jedoch lassen sich mit dem entsprechenden Argument symbolische Links ignorieren. Passend dazu existiert die statische Methode FileUtils.isDirectory, die nach demselben Prinzip auf Ordner angewendet wird.

 

Darüber hinaus stellt die Klasse FilenameUtils Methoden zur Verfügung, die sich mit Pfaden beschäftigen. So gibt die statische Methode getName() den Dateinamen eines vollständigen Pfades zurück. Jedoch prüft diese Methode nicht, ob die Datei tatsächlich existiert:

public String getCompleteFileName(String path) {
  String fileName = FilenameUtils.getName(path);
  return fileName;
}

Der Dateiname lässt sich auch ohne Erweiterung ausgeben, indem die statische Methode getBaseName() zum Einsatz kommt:

public String getFileName(String path) {
  String fileName = FilenameUtils.getBaseName(path);
  return fileName;
}

Die statische Methode getExtension() hingegen gibt lediglich das Dateiformat aus:

public String getFileExtension(String path) {
  String ext = FilenameUtils.getExtension(path);
  return ext;
}

Um den vollständigen Pfad ohne Dateinamen auszugeben, bedarf es der statischen Methode aus Listing 16.

public String getFullDirectory(String path) {
  File f = new File(path);
  String folder = FilenameUtils.getFullPathNoEndSeparator(f.getAbsolutePath().toString());
  return folder;
}

Abgesehen von Dateinamen kann Apache Commons IO Operationen an Dateien beziehungsweise Ordnern vornehmen. Unter anderem lassen sich mit den Methoden der Klasse FileUtils Dateien (Listing 17) und Ordner erstellen (Listing 18).

public void createFile(String path) {
  File ff = new File(path);
  try {
    FileUtils.touch(ff);
  } catch (IOException e) {
    e.getMessage();
  }
}

So erstellt die statische Methode forceMkdir() einen Ordner samt erforderlichen Unterverzeichnissen, auch wenn dieser bereits existiert. Allerdings kommt es dabei zu keiner Ausnahme (Listing 18).

public void createFolder(String path) {
  File ff = new File(path);
  try {
    FileUtils.forceMkdir(ff);
  } catch (IOException e) {
    e.getMessage();
  }
}

Eingelesene Dateien lassen sich unter Apache Commons IO zu Strings oder Bytes weiterverarbeiten. Textdateien können entweder als String abgespeichert oder jede Zeile einer Textdatei in einer Liste festgehalten werden (Listing 19).

public void readInTextFile(String path) {
  File file = new File(path);
  try {
    List<String> lines = FileUtils.readLines(file,StandardCharsets.UTF_8.name());
  } catch(IOException e) {
    e.printStackTrace();
  }
}

 

Bytebasierte Daten lassen sich wie bei den anderen Bibliotheken zu einem Bytearray weiterverarbeiten. Dies geschieht unter Apache Commons IO mit Hilfe der statischen Methode readFileToByteArray() (Listing 20).

public void readInFile(String path) {
  File file = new File(path);
  try {
    byte [] bytes = FileUtils.readFileToByteArray(file);
  } catch(IOException e) {
    e.printStackTrace();
  }
}

Das Kopieren von Dateien hingegen macht eine Unterscheidung zwischen Text und bytebasierten Daten überflüssig. So lassen sich Dateien in Form von File-Objekten an einen neuen Ort kopieren (Listing 21).

public void copy(String sFrom, String sTo) {
  File f1 = new File(sFrom);
  File f2 = new File(sTo);
  try {
    FileUtils.copyFile(f1, f2);
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Im Gegensatz zu textbasierten Daten können bytebasierte Daten anders geschrieben werden. Jedoch gibt es einen Weg, der für beide Arten von Daten gilt. Das liegt vor allem daran, dass die Quelldatei in Form eines Bytearrays an die statische Methode writeByteArrayToFile() übergeben wird (Listing 22).

public void write(String sFrom, String sTo) {
  File ff = new File(sTo);
  try {
    FileInputStream fIn = new FileInputStream(sFrom);
    FileUtils.touch(ff);
    FileUtils.writeByteArrayToFile(ff, fIn.readAllBytes() );
  } catch (IOException e) {
    e.getMessage();
  }
}

Falls es dennoch auf die Zeichenkodierung eines Texts ankommt, dann kann anhand der statischen Methode writeStringToFile() unter Angabe der Zeichensatzes ein String in die Zieldatei geschrieben werden (Listing 23).

public void writeText(String sTo, String msg) {
  File ff = new File(sTo);
  try {
    FileUtils.touch(ff);
    FileUtils.writeStringToFile(ff, msg, StandardCharsets.UTF_8.name());
  } catch (IOException e) {
    e.getMessage();
  }
}

Darüber hinaus ist es von Zeit zu Zeit erforderlich, sich um nicht mehr benötigte Dateien zu kümmern. Hierzu stehen mehrere statische Methoden zur Verfügung, wobei die Methode deleteQuietly() Dateien löscht, ohne Ausnahmen zu werfen (Listing 24).

public boolean delFile(String path) {
  File f = new File(s);
  boolean deleted = false;
  FileUtils.deleteQuietly(f);
  deleted = fileExists(s);
  return deleted;
}

Apache Commons IO stellt zudem extra Methoden zur Verfügung, um die Größe einer Datei wie folgt zu ermitteln:

public long getFileSize(String path) {
  File f = new File(path);
  return FileUtils.sizeOf(f);
}

Abgesehen von der Verarbeitung von Dateien kann Apache Commons IO den Inhalt eines Verzeichnisses ausgeben. Hierzu wird der statischen Methode listFilesAndDirs() neben dem Pfad ein auf Dateien anzuwendender Filter angegeben, wobei dieser nicht null sein darf. Das dritte Argument legt fest, ob Unterverzeichnisse berücksichtigt werden (Listing 25).

public void list(String path) {
  File dir = new File(path);
  Collection<File> list = FileUtils.listFilesAndDirs(dir, TrueFileFilter.TRUE , TrueFileFilter.TRUE);
  for(File f: list) {
    System.out.println(f.getName());
  }
}

Fazit

Das IO-Paket von Java darf bei den weiterführenden Bibliotheken nicht fehlen, da etliche Methoden unter anderem auf die File-Klasse zurückgreifen, um bestimmte Operationen mit Verzeichnissen oder Dateien vorzunehmen. Sowohl Google Guava als auch Apache Commons IO bieten Methoden an, die das Einlesen, Kopieren und Verschieben von Dateien vereinfachen. Google Guava hebt sich jedoch von Apache Commons IO deutlich ab, indem es die Unterschiede zwichen text- und bytebasierten Daten berücksichtigt. Zusätzlich kann es Dateien aus dem Resources-Ordner weiterverarbeiten. Allerdings lassen sich mittels des IO-Pakets von Java ebenfalls Dateien aus dem Klassenpfad einlesen:

InputStream in = getClass().getResourceAsStream(path);

 

Links & Literatur

[1] Google Guava: https://guava.dev/releases/19.0/api/docs/com/google/common/io/package-summary.html

[2] Resources-Klasse: https://guava.dev/releases/19.0/api/docs/com/google/common/io/Resources.html

[3] FileUtils: https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/FileUtils.html

[4] FilenameUtils: https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/FilenameUtils.html

Top Articles About Blog

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management