
Das aktuelle Arbeitsverzeichnis liefert dir in Java der Ausdruck Path.of("").toAbsolutePath() als Path oder die System-Property user.dir als String.
Das folgende Programm gibt das Arbeitsverzeichnis auf beiden Wegen aus:
Path workingDirectory = Path.of("").toAbsolutePath();
System.out.println(workingDirectory);
System.out.println(System.getProperty("user.dir"));
/Users/sven/Projects/HappyCoders/git-articles/java-file-io
/Users/sven/Projects/HappyCoders/git-articles/java-file-io
Das Arbeitsverzeichnis (englisch: current working directory) ist das Verzeichnis, gegen das Java jeden relativen Pfad auflöst. Path.of("files/server.log") meint also die Datei files/server.log unterhalb des Arbeitsverzeichnisses.
Welches Verzeichnis das ist, legt nicht dein Programm fest, sondern der Prozess, der es startet.
In diesem Artikel erfährst du,
- was das Arbeitsverzeichnis ist und woher Java es kennt,
- wie du das Arbeitsverzeichnis auslesen kannst und worin sich die Wege unterscheiden,
- warum derselbe relative Pfad je nach Startverzeichnis eine andere Datei meint,
- in welchem Verzeichnis Terminal, IntelliJ IDEA, Maven, Gradle, Docker und systemd dein Programm starten,
- warum weder
System.setProperty("user.dir", ...)noch die Option-Duser.dirdas Arbeitsverzeichnis ändern, - wie du Dateien findest, ohne dass dein Programm vom Arbeitsverzeichnis abhängt.
Die Beispiele in diesem Artikel
Den vollständigen Code findest du im GitHub-Repository java-file-io, im Package eu.happycoders.workingdirectory. Jeder Abschnitt hat dort eine eigene Klasse mit einer main()-Methode, die die gezeigten Ausgaben erzeugt.
Die Ausgaben stammen von macOS. Ich habe die meisten Beispiele im Hauptverzeichnis des Repositorys gestartet, bei mir /Users/sven/Projects/HappyCoders/git-articles/java-file-io. Bei dir steht an dieser Stelle das Verzeichnis, in das du das Repository geklont hast.
Einige Beispiele starte ich absichtlich in einem anderen Verzeichnis. Dort steht der Befehl vor der Ausgabe, und damit er überall funktioniert, merke ich mir das Hauptverzeichnis des Repositorys vorher in der Shell-Variablen REPO:
% REPO=$(pwd)
Die Klassen im Repository brauchen Java 25, weil sie die vereinfachte main()-Methode ohne String[] args verwenden. Die Listings im Artikel laufen ab Java 17. Wo eine Ausgabe von der Java-Version abhängt, steht das im Text.
Was ist das Arbeitsverzeichnis?
Das Arbeitsverzeichnis gehört nicht zu Java, sondern zum Betriebssystem: Jeder Prozess hat genau ein Arbeitsverzeichnis. Ein neuer Prozess erbt es von dem Prozess, der ihn startet – ein Java-Programm, das du im Terminal startest, also von der Shell.
Die JVM fragt das Arbeitsverzeichnis beim Start einmal beim Betriebssystem ab – unter Linux und macOS mit der Funktion getcwd(), unter Windows mit GetCurrentDirectoryW(). Das Ergebnis legt sie in der System-Property user.dir ab.
Gegen dieses Verzeichnis lösen beide Datei-APIs relative Pfade auf: java.io.File z. B. in getAbsolutePath() und java.nio.file.Path in toAbsolutePath(). Beide lesen dafür nicht bei jedem Aufruf die System-Property, sondern verwenden den Wert, den sie sich beim Start der JVM gemerkt haben.
Die folgende Grafik zeigt diesen Weg von oben nach unten: Die Shell steht im Home-Verzeichnis /Users/sven und startet von dort die JVM. Der JVM-Prozess erbt das Arbeitsverzeichnis, die JVM legt es in user.dir ab, und File und Path merken sich diesen Wert. Gegen ihn lösen sie den relativen Pfad files/server.log zum absoluten Pfad /Users/sven/files/server.log auf. Die blau umrandeten Kästen gehören zur JVM, die grau umrandeten zum Betriebssystem und zum Ergebnis:
Solange niemand user.dir verändert, zeigen das Arbeitsverzeichnis des Prozesses, die System-Property und der Wert, den sich File und Path gemerkt haben, auf dasselbe Verzeichnis. Was passiert, wenn das nicht mehr gilt, zeigt der Abschnitt Lässt sich das Arbeitsverzeichnis ändern?
Das Arbeitsverzeichnis auslesen
Das Arbeitsverzeichnis kannst du über die System-Property user.dir auslesen, über einen leeren Pfad oder über den Pfad . – die beiden Pfade jeweils als Path oder als File.
user.dir, Path.of("") und new File("")
Das folgende Programm gibt die System-Property user.dir aus und die absoluten Pfade eines leeren Path und eines leeren File:
System.out.println("user.dir: " + System.getProperty("user.dir"));
System.out.println("Path.of(\"\"): " + Path.of("").toAbsolutePath());
System.out.println("new File(\"\"): " + new File("").getAbsolutePath());
user.dir: /Users/sven/Projects/HappyCoders/git-articles/java-file-io
Path.of(""): /Users/sven/Projects/HappyCoders/git-articles/java-file-io
new File(""): /Users/sven/Projects/HappyCoders/git-articles/java-file-io
Alle drei Zeilen zeigen dasselbe Verzeichnis. Ein leerer Pfad ist ein relativer Pfad ohne einen einzigen Namen. Löst Java ihn gegen das Arbeitsverzeichnis auf, bleibt das Arbeitsverzeichnis selbst übrig.
System.getProperty("user.dir") liefert einen String, Path.of("").toAbsolutePath() dagegen einen Path, mit dem du direkt weiterarbeiten kannst – z. B. mit resolve() einen Pfad darunter bilden. Wenn du mit dem Arbeitsverzeichnis Dateien öffnen willst, empfehle ich deshalb Path.of("").toAbsolutePath().
Path.of() gibt es seit Java 11. In älterem Code steht oft Paths.get(""). Das ist dasselbe, denn Paths.get() ruft intern Path.of() auf.
new File("") steht erst seit Java 25 uneingeschränkt für das Arbeitsverzeichnis – warum, zeigt der Abschnitt Ein leeres File bis Java 24.
Der Punkt: Path.of(".") und new File(".")
Statt des leeren Pfads findest du häufig einen Punkt, der in Pfaden für das aktuelle Verzeichnis steht. Das folgende Programm gibt Path.of(".") und new File(".") als absolute Pfade aus, darunter jeweils die bereinigte Variante. Zum Schluss vergleicht es den absoluten Pfad von Path.of(".") mit dem absoluten Pfad von Path.of(""):
Path dot = Path.of(".").toAbsolutePath();
File dotFile = new File(".");
System.out.println("Path.of(\".\"): " + dot);
System.out.println(" normalize(): " + dot.normalize());
System.out.println("new File(\".\"): " + dotFile.getAbsolutePath());
System.out.println(" getCanonicalPath(): " + dotFile.getCanonicalPath());
Path empty = Path.of("").toAbsolutePath();
System.out.println("dot.equals(empty): " + dot.equals(empty));
Path.of("."): /Users/sven/Projects/HappyCoders/git-articles/java-file-io/.
normalize(): /Users/sven/Projects/HappyCoders/git-articles/java-file-io
new File("."): /Users/sven/Projects/HappyCoders/git-articles/java-file-io/.
getCanonicalPath(): /Users/sven/Projects/HappyCoders/git-articles/java-file-io
dot.equals(empty): false
Beide absoluten Pfade enden auf /.: toAbsolutePath() und getAbsolutePath() hängen den relativen Pfad an das Arbeitsverzeichnis an, ohne ihn zu bereinigen. Erst normalize() bzw. getCanonicalPath() entfernen den Punkt. Was diese beiden Methoden sonst noch unterscheidet, zeigt der Artikel über Datei- und Verzeichnisnamen in Java.
Der Punkt am Ende ändert das Ergebnis von Vergleichen: Für equals() ist /…/java-file-io/. ein anderer Pfad als /…/java-file-io, daher liefert die letzte Zeile false. Ich empfehle deshalb den leeren Pfad statt des Punkts.
Ein leeres File bis Java 24
Seit Java 25 steht new File("") für das Arbeitsverzeichnis – so wie ein leerer Path schon immer (JDK-8024695).
Bis einschließlich Java 24 war ein leeres File nur in getAbsolutePath() das Arbeitsverzeichnis. Methoden, die das Dateisystem abfragen, wie exists(), isDirectory() und list(), behandelten es als Datei, die es nicht gibt.
Das folgende Programm ruft diese vier Methoden auf einem leeren File auf:
File empty = new File("");
String[] entries = empty.list();
System.out.println("getAbsolutePath(): " + empty.getAbsolutePath());
System.out.println("exists(): " + empty.exists());
System.out.println("isDirectory(): " + empty.isDirectory());
System.out.println(
"list(): " + (entries == null ? "null" : entries.length + " entries"));
Mit Java 24 gibt es Folgendes aus:
getAbsolutePath(): /Users/sven/Projects/HappyCoders/git-articles/java-file-io
exists(): false
isDirectory(): false
list(): null
Mit Java 25 gibt dasselbe Programm Folgendes aus:
getAbsolutePath(): /Users/sven/Projects/HappyCoders/git-articles/java-file-io
exists(): true
isDirectory(): true
list(): 13 entries
Wenn dein Code auf Java 24 oder älter läuft, solltest du das Arbeitsverzeichnis nicht mit new File("") bestimmen, sondern mit Path.of("") oder mit new File(System.getProperty("user.dir")).
Symbolische Links im Arbeitsverzeichnis
Startest du ein Programm in einem Verzeichnis, das du über einen symbolischen Link erreicht hast, enthält user.dir den Pfad, auf den der symbolische Link zeigt. Unter macOS ist /tmp z. B. ein symbolischer Link auf /private/tmp.
Das folgende Programm gibt user.dir und die Umgebungsvariable PWD aus, in der die Shell den Pfad des aktuellen Verzeichnisses ablegt:
System.out.println("user.dir: " + System.getProperty("user.dir"));
System.out.println("PWD: " + System.getenv("PWD"));
Ich habe es in /tmp gestartet, mit dem absoluten Pfad zu den Klassen im Class Path:
% cd /tmp
% java -cp $REPO/target/classes eu.happycoders.workingdirectory.Ch1SymbolicLink
user.dir: /private/tmp
PWD: /tmp
getcwd() liefert den Pfad, unter dem das Verzeichnis tatsächlich liegt, ohne symbolische Links. Die Shell merkt sich in PWD dagegen den Weg, über den du in das Verzeichnis gewechselt bist.
Darauf musst du achten, wenn du einen Pfad, den dein Programm aus dem Arbeitsverzeichnis bildet, mit einem Pfad aus einer Konfigurationsdatei oder einer Umgebungsvariablen vergleichst: /tmp/report.txt und /private/tmp/report.txt sind dieselbe Datei, aber nicht derselbe Pfad.
Die Ausgabe zeigt außerdem, dass es für das Arbeitsverzeichnis keine Rolle spielt, wo die Klassen liegen: Die JVM lädt sie aus $REPO/target/classes, das Arbeitsverzeichnis ist trotzdem /private/tmp.
Relative Pfade und das Arbeitsverzeichnis
Was das Arbeitsverzeichnis für dein Programm bedeutet, zeigt sich, sobald es eine Datei über einen relativen Pfad öffnet.
Das folgende Programm liest die Datei files/server.log aus dem Repository. Findet es die Datei nicht, gibt es die Exception aus und darunter den absoluten Pfad, unter dem es gesucht hat:
Path log = Path.of("files/server.log");
try {
String content = Files.readString(log);
System.out.println("Read " + content.lines().count() + " lines");
} catch (NoSuchFileException e) {
System.out.println(e);
System.out.println("Absolute path:");
System.out.println(" " + log.toAbsolutePath());
}
Im Hauptverzeichnis des Repositorys gestartet, findet das Programm die Datei:
% java -cp target/classes eu.happycoders.workingdirectory.Ch2RelativePaths
Read 5 lines
Im Verzeichnis target gestartet, findet dasselbe Programm die Datei nicht. Der Class Path lautet dort nur noch classes, weil dieses Verzeichnis jetzt direkt unter dem Arbeitsverzeichnis liegt:
% cd target
% java -cp classes eu.happycoders.workingdirectory.Ch2RelativePaths
java.nio.file.NoSuchFileException: files/server.log
Absolute path:
/Users/sven/Projects/HappyCoders/git-articles/java-file-io/target/files/server.log
Der Code ist derselbe, die Datei ist dieselbe – nur das Arbeitsverzeichnis ist ein anderes. Java sucht files/server.log deshalb unter target/files/server.log.
Beachte die erste Zeile der Ausgabe: Die NoSuchFileException nennt nur den relativen Pfad, so wie du ihn übergeben hast. In welchem Verzeichnis Java gesucht hat, steht nicht darin.
Ich empfehle deshalb, bei einer Datei, die nicht gefunden wird, zusätzlich den absoluten Pfad aus toAbsolutePath() ins Log zu schreiben. Dann siehst du sofort, in welchem Verzeichnis das Programm gelaufen ist.
Wer legt das Arbeitsverzeichnis fest?
Das Arbeitsverzeichnis wird nicht durch dein Programm festgelegt, sondern durch den Prozess, der es startet. Die folgende Tabelle zeigt, in welchem Verzeichnis gängige Startumgebungen ein Java-Programm starten und wo du dieses Verzeichnis einstellen kannst:
| Startumgebung | Arbeitsverzeichnis | Einstellung |
|---|---|---|
Terminal: java und java -jar | das Verzeichnis, in dem die Shell steht | cd vor dem Start |
| Maven: Tests mit Surefire | das Verzeichnis der pom.xml, bei mehreren Modulen das des Moduls | Parameter workingDirectory |
Maven: spring-boot:run und quarkus:dev | das Verzeichnis der pom.xml, bei mehreren Modulen das des Moduls | Parameter workingDirectory bzw. workingDir |
Gradle: test, run, bootRun und quarkusDev | das Verzeichnis des Projekts, bei mehreren Teilprojekten das des Teilprojekts | Eigenschaft workingDir bzw. workingDirectory |
| IntelliJ IDEA: Anwendung | das in IntelliJ geöffnete Projektverzeichnis | Feld „Working directory“ der Run-Konfiguration |
| IntelliJ IDEA: JUnit-Tests | dasselbe Verzeichnis wie bei Maven bzw. Gradle | Feld „Working directory“ der Run-Konfiguration |
| Docker | das Verzeichnis aus WORKDIR, ohne Angabe / | WORKDIR im Dockerfile |
| systemd | das Verzeichnis aus WorkingDirectory=, ohne Angabe / | WorkingDirectory= in der Unit-Datei |
Zu den Zeilen im Einzelnen:
- Terminal: Genau wie
javastartet auchjava -jardas Programm in dem Verzeichnis, in dem die Shell steht, nicht in dem Verzeichnis der JAR-Datei. Der Launcher wechselt das Verzeichnis nicht. - Maven: Das Maven Surefire Plugin startet die Test-JVM in
${basedir}, dem Verzeichnis mit derpom.xml– in einem Projekt mit mehreren Modulen in dem Verzeichnis mit derpom.xmldes jeweiligen Moduls. Auchspring-boot:runundquarkus:devstarten die Anwendung dort, solange duworkingDirectorybzw.workingDirnicht setzt. - Gradle: Bei
test,runundbootRunsteht die EigenschaftworkingDirstandardmäßig aufprojectDir, beiquarkusDevdie EigenschaftworkingDirectory. In einem Build mit mehreren Teilprojekten ist das jeweils das Verzeichnis des Teilprojekts. - IntelliJ IDEA: Eine Application-Konfiguration ohne Eintrag im Feld „Working directory“ startet die Anwendung im Projektverzeichnis, also in dem Verzeichnis, das du in IntelliJ geöffnet hast. Eine JUnit-Konfiguration ist dagegen mit
$MODULE_WORKING_DIR$vorbelegt, und das ist in einem Maven- oder Gradle-Projekt dasselbe Verzeichnis, in dem auch Maven bzw. Gradle die Tests starten. Beides habe ich am Code von IntelliJ IDEA 2026.2.3 geprüft. - Docker: Ohne
WORKDIRist das Arbeitsverzeichnis/. Viele Basis-Images setzen aber selbst einWORKDIR, das dein Dockerfile erbt. - systemd: Ohne
WorkingDirectory=startet ein Dienst der System-Instanz in/, ein Dienst der User-Instanz im Home-Verzeichnis des Benutzers.
Drei Folgen davon:
- Ein Programm, das systemd ohne
WorkingDirectory=startet, suchtfiles/server.logunter/files/server.log. - In einem Projekt mit mehreren Maven-Modulen oder Gradle-Teilprojekten lösen die Tests eines Moduls relative Pfade gegen das Verzeichnis dieses Moduls auf, nicht gegen das Hauptverzeichnis des Projekts.
- In einem solchen Projekt läuft dieselbe Anwendung in IntelliJ IDEA im Hauptverzeichnis des Projekts – dem Verzeichnis, das du geöffnet hast –, mit
mvn spring-boot:rundagegen im Verzeichnis ihres Maven-Moduls. Ein relativer Pfad, der in der IDE funktioniert, findet seine Datei mit Maven deshalb nicht.
Lässt sich das Arbeitsverzeichnis ändern?
Nein. Java hat keine Methode, die das Arbeitsverzeichnis des laufenden Prozesses ändert – kein Gegenstück zur Funktion chdir() unter Linux und macOS.
Zwei Wege sehen so aus, als täten sie es trotzdem: System.setProperty("user.dir", ...) zur Laufzeit und die Option -Duser.dir beim Start.
user.dir zur Laufzeit setzen
Das folgende Programm gibt user.dir und die absoluten Pfade eines File und eines Path für die Datei notes.txt aus. Danach setzt es user.dir auf /tmp und gibt alle drei Werte erneut aus:
System.out.println("Before setProperty():");
System.out.println(" user.dir: " + System.getProperty("user.dir"));
System.out.println(" File: " + new File("notes.txt").getAbsolutePath());
System.out.println(" Path: " + Path.of("notes.txt").toAbsolutePath());
System.setProperty("user.dir", "/tmp");
System.out.println("After setProperty():");
System.out.println(" user.dir: " + System.getProperty("user.dir"));
System.out.println(" File: " + new File("notes.txt").getAbsolutePath());
System.out.println(" Path: " + Path.of("notes.txt").toAbsolutePath());
Before setProperty():
user.dir: /Users/sven/Projects/HappyCoders/git-articles/java-file-io
File: /Users/sven/Projects/HappyCoders/git-articles/java-file-io/notes.txt
Path: /Users/sven/Projects/HappyCoders/git-articles/java-file-io/notes.txt
After setProperty():
user.dir: /tmp
File: /Users/sven/Projects/HappyCoders/git-articles/java-file-io/notes.txt
Path: /Users/sven/Projects/HappyCoders/git-articles/java-file-io/notes.txt
Die System-Property hat danach den neuen Wert, File und Path lösen notes.txt aber weiter gegen das alte Arbeitsverzeichnis auf. Den Grund zeigt der Abschnitt Was ist das Arbeitsverzeichnis?: Beide lesen user.dir nicht bei jedem Aufruf, sondern haben sich den Wert beim Start der JVM gemerkt.
Für File gilt das seit Java 11 (JDK-8194154, Titel: „System property user.dir should not be changed“). Seitdem steht im Javadoc der Klasse System außerdem, dass das Ändern einer Standard-Property unvorhersehbare Folgen haben kann.
user.dir beim Start setzen: -Duser.dir
Beim Start kannst du user.dir mit der Option -Duser.dir=<Verzeichnis> auf ein anderes Verzeichnis setzen. Die JVM legt dann diesen Wert in user.dir ab statt des Verzeichnisses, das das Betriebssystem meldet. Das Arbeitsverzeichnis des Prozesses ändert sich dadurch nicht – und daraus entsteht ein Widerspruch.
Das folgende Programm schreibt die Datei via-nio.txt mit Files.writeString() und die Datei via-io.txt mit einem FileOutputStream. Dann gibt es die absoluten Pfade beider Dateien aus und prüft auf zwei Wegen, ob via-io.txt existiert – einmal mit File.exists() und einmal mit Files.exists():
System.out.println("user.dir: " + System.getProperty("user.dir"));
Path viaNio = Path.of("via-nio.txt");
Files.writeString(viaNio, "written with Files.writeString()\n");
File viaIo = new File("via-io.txt");
try (FileOutputStream out = new FileOutputStream(viaIo)) {
out.write("written with FileOutputStream\n".getBytes(UTF_8));
}
System.out.println("viaNio.toAbsolutePath():");
System.out.println(" " + viaNio.toAbsolutePath());
System.out.println("viaIo.getAbsolutePath():");
System.out.println(" " + viaIo.getAbsolutePath());
System.out.println("viaIo.exists(): " + viaIo.exists());
System.out.println("Files.exists(viaIo.toPath()): " + Files.exists(viaIo.toPath()));
Ich habe es im Hauptverzeichnis des Repositorys gestartet und user.dir auf das Verzeichnis target gesetzt:
% java -Duser.dir=$REPO/target -cp $REPO/target/classes \
eu.happycoders.workingdirectory.Ch4UserDirOption
user.dir: /Users/sven/Projects/HappyCoders/git-articles/java-file-io/target
viaNio.toAbsolutePath():
/Users/sven/Projects/HappyCoders/git-articles/java-file-io/target/via-nio.txt
viaIo.getAbsolutePath():
/Users/sven/Projects/HappyCoders/git-articles/java-file-io/target/via-io.txt
viaIo.exists(): true
Files.exists(viaIo.toPath()): false
Laut den beiden absoluten Pfaden liegen beide Dateien in target. Tatsächlich liegen sie in zwei verschiedenen Verzeichnissen:
% ls via-*.txt target/via-*.txt
target/via-nio.txt
via-io.txt
Files.writeString() hat via-nio.txt in target geschrieben, der FileOutputStream dagegen via-io.txt in das Hauptverzeichnis. Die beiden APIs übergeben den relativen Pfad auf verschiedene Weise an das Betriebssystem:
- NIO.2: Weicht
user.dirvom Arbeitsverzeichnis des Prozesses ab, löstPatheinen relativen Pfad selbst gegenuser.dirauf und übergibt den absoluten Pfad an das Betriebssystem. java.io:FileOutputStreamübergibt den relativen Pfad unverändert, und das Betriebssystem löst ihn gegen das Arbeitsverzeichnis des Prozesses auf. NurgetAbsolutePath()rechnet mituser.dir– und zeigt deshalb einen Pfad, unter dem die Datei nicht liegt.
Die letzten beiden Zeilen der Programmausgabe zeigen dasselbe beim Prüfen: viaIo.exists() fragt das Betriebssystem mit dem relativen Pfad und findet via-io.txt im Hauptverzeichnis. Files.exists(viaIo.toPath()) löst denselben Pfad gegen user.dir auf und sucht in target, wo es die Datei nicht gibt.
Auch den Class Path löst die JVM gegen user.dir auf. Mit einem relativen Class Path findet sie die Klasse, die sie starten soll, nicht mehr:
% java -Duser.dir=$REPO/target -cp target/classes \
eu.happycoders.workingdirectory.Ch4UserDirOption
Error: Could not find or load main class eu.happycoders.workingdirectory.Ch4UserDirOption
Caused by: java.lang.ClassNotFoundException: eu.happycoders.workingdirectory.Ch4UserDirOption
Der Class-Loader bildet aus target/classes mit Path.of() und toRealPath() einen absoluten Pfad, gegen user.dir aufgelöst also …/target/target/classes. Dieses Verzeichnis gibt es nicht, also lässt der Class-Loader den Eintrag weg.
Ich empfehle deshalb, user.dir nie zu überschreiben – weder mit System.setProperty() noch mit -Duser.dir. Beides ändert das Arbeitsverzeichnis des Prozesses nicht, sondern führt nur dazu, dass Teile des JDK mit verschiedenen Verzeichnissen rechnen.
Wenn dein Programm in einem anderen Verzeichnis laufen soll, musst du es dort starten: mit cd im Startskript, mit WorkingDirectory= in systemd, mit WORKDIR im Dockerfile oder – aus einem Java-Programm heraus – mit ProcessBuilder.directory().
Was stattdessen funktioniert
Statt das Arbeitsverzeichnis zu ändern, kannst du ein eigenes Basisverzeichnis festlegen, Dateien über den Class Path laden und Kindprozesse in einem Verzeichnis deiner Wahl starten.
Ein Basisverzeichnis festlegen
Gehören Dateien zur Installation deines Programms – Konfiguration, Daten, Logs –, solltest du ihr Verzeichnis einmal beim Start bestimmen und alle Pfade daran anhängen.
Das folgende Programm liest das Basisverzeichnis aus der Umgebungsvariablen APP_HOME. Fehlt die Variable, nimmt es das Arbeitsverzeichnis. Den Pfad der Log-Datei hängt es mit resolve() an das Basisverzeichnis an:
String appHomeVariable = System.getenv().getOrDefault("APP_HOME", "");
Path appHome = Path.of(appHomeVariable).toAbsolutePath().normalize();
Path log = appHome.resolve("files/server.log");
System.out.println("App home: " + appHome);
System.out.println("Log file: " + log);
System.out.println("Lines: " + Files.readAllLines(log).size());
Ich habe es im Verzeichnis target gestartet, mit APP_HOME=... Es findet die Datei, die das Programm aus dem Abschnitt Relative Pfade und das Arbeitsverzeichnis von dort aus nicht gefunden hat:
% cd target
% APP_HOME=.. java -cp classes eu.happycoders.workingdirectory.Ch5BaseDirectory
App home: /Users/sven/Projects/HappyCoders/git-articles/java-file-io
Log file: /Users/sven/Projects/HappyCoders/git-articles/java-file-io/files/server.log
Lines: 5
toAbsolutePath() macht aus .. den Pfad …/target/.., und normalize() kürzt ihn auf das Hauptverzeichnis.
Ein relativer Wert wie .. hängt allerdings selbst wieder vom Arbeitsverzeichnis ab. Im Startskript solltest du APP_HOME deshalb auf einen absoluten Pfad setzen. Statt einer Umgebungsvariablen kannst du das Verzeichnis auch als Argument übergeben oder als eigene System-Property, z. B. -Dapp.home=/opt/app – nur nicht über user.dir.
Dateien aus dem Class Path laden
Dateien, die zum Programm selbst gehören und die es nur liest – z. B. Standardwerte und Templates –, legst du am besten in den Class Path. Bei Maven und Gradle heißt das: unter src/main/resources, Testdaten unter src/test/resources.
Solche Dateien lädst du mit getResourceAsStream(), unabhängig davon, in welchem Verzeichnis das Programm gestartet wurde. Das folgende Programm lädt die Datei defaults.properties, die im Repository unter src/main/resources/eu/happycoders/workingdirectory liegt:
Properties defaults = new Properties();
try (InputStream in = Ch5Resource.class.getResourceAsStream("defaults.properties")) {
defaults.load(in);
}
System.out.println("user.dir: " + System.getProperty("user.dir"));
System.out.println("Defaults: " + defaults);
Ein Name ohne führenden Schrägstrich bezieht sich auf das Package der Klasse, hier also auf eu/happycoders/workingdirectory.
Ich habe das Programm in /tmp gestartet, außerhalb des Repositorys:
% cd /tmp
% java -cp $REPO/target/classes eu.happycoders.workingdirectory.Ch5Resource
user.dir: /private/tmp
Defaults: {server.port=8080, log.level=INFO}
Obwohl das Arbeitsverzeichnis /private/tmp ist, hat das Programm die Standardwerte aus dem Class Path geladen.
getResourceAsStream() liefert allerdings nur einen InputStream. Dateien, die dein Programm schreibt, gehören deshalb in ein Basisverzeichnis wie im vorigen Abschnitt.
Kindprozesse in einem anderen Verzeichnis starten
Startest du aus Java einen anderen Prozess, erbt dieser das Arbeitsverzeichnis deines Programms. Mit ProcessBuilder.directory() kannst du ihm ein anderes Arbeitsverzeichnis geben.
Das folgende Programm startet zweimal das Kommando pwd, das sein Arbeitsverzeichnis ausgibt – einmal ohne und einmal mit directory(). inheritIO() leitet die Ausgabe von pwd auf die Konsole des Java-Programms, und waitFor() wartet, bis pwd fertig ist, damit die beiden Ausgaben nicht durcheinandergeraten:
System.out.print("Without directory(): ");
new ProcessBuilder("pwd").inheritIO().start().waitFor();
System.out.print("directory(\"target\"): ");
new ProcessBuilder("pwd").directory(new File("target")).inheritIO().start().waitFor();
Without directory(): /Users/sven/Projects/HappyCoders/git-articles/java-file-io
directory("target"): /Users/sven/Projects/HappyCoders/git-articles/java-file-io/target
directory() legt nur das Arbeitsverzeichnis des neuen Prozesses fest. Das Arbeitsverzeichnis deines Programms bleibt, wie es ist.
Häufige Fehler
Die Abschnitte oben zeigen jeden der folgenden Fehler im Detail:
- Ein relativer Pfad wird nur im Hauptverzeichnis getestet: In einem anderen Startverzeichnis sucht Java die Datei an einer anderen Stelle, und die
NoSuchFileExceptionnennt nur den relativen Pfad. - Tests in einem Projekt mit mehreren Maven-Modulen oder Gradle-Teilprojekten erwarten das Hauptverzeichnis: Maven, Gradle und IntelliJ IDEA starten die Tests im Verzeichnis ihres Moduls.
- Eine Anwendung wird nur in IntelliJ IDEA getestet: In einem Projekt mit mehreren Maven-Modulen startet IntelliJ sie im Hauptverzeichnis des Projekts,
mvn spring-boot:runundmvn quarkus:devdagegen im Verzeichnis ihres Moduls. user.dirwird mitSystem.setProperty()geändert: Seit Java 11 lösenFileundPathrelative Pfade danach weiter gegen das alte Verzeichnis auf.-Duser.dirwird anstelle voncdverwendet:Files.writeString()undFileOutputStreamschreiben dann in verschiedene Verzeichnisse, und die JVM findet einen relativen Class Path nicht mehr.new File("")auf Java 24 oder älter:exists()liefertfalse, obwohl das Arbeitsverzeichnis existiert.- Das Arbeitsverzeichnis aus
Path.of("."): Der absolute Pfad endet auf/.und ist fürequals()ein anderer Pfad als der ausPath.of(""). - Ein Pfadvergleich mit einem symbolischen Link:
user.direnthält den Pfad, auf den der Link zeigt, unter macOS z. B./private/tmpstatt/tmp.
Zusammenfassung
Das Arbeitsverzeichnis gehört zum Prozess, nicht zu Java. Die JVM fragt es beim Start beim Betriebssystem ab und legt es in der System-Property user.dir ab. File und Path lösen jeden relativen Pfad dagegen auf.
Auslesen kannst du das Arbeitsverzeichnis mit Path.of("").toAbsolutePath() oder System.getProperty("user.dir"). Ändern kannst du es aus Java heraus nicht: System.setProperty("user.dir", ...) ändert seit Java 11 nur den Wert der Property, und mit -Duser.dir beim Start schreiben Files.writeString() und FileOutputStream in verschiedene Verzeichnisse.
In welchem Verzeichnis dein Programm läuft, bestimmt der Prozess, der es startet – die Shell, IntelliJ IDEA, Maven, Gradle, Docker oder systemd. Maven, Gradle und IntelliJ IDEA starten Tests im Verzeichnis der pom.xml bzw. des Gradle-Projekts, bei mehreren Modulen in dem des jeweiligen Moduls. IntelliJ IDEA startet Anwendungen dagegen im geöffneten Projektverzeichnis, Docker und systemd ohne eigene Angabe in /.
Meine Empfehlungen:
- Lies das Arbeitsverzeichnis mit
Path.of("").toAbsolutePath()aus, nicht mitPath.of(".")und – solange dein Code auf Java 24 oder älter läuft – nicht mitnew File(""). - Schreib den absoluten Pfad aus
toAbsolutePath()ins Log, wenn eine Datei nicht gefunden wird. - Leg für Dateien, die zur Installation gehören, ein Basisverzeichnis fest – über eine Umgebungsvariable, eine eigene System-Property oder ein Argument – und häng alle Pfade mit
resolve()daran an. - Lade Dateien, die zum Programm gehören und die es nur liest, mit
getResourceAsStream()aus dem Class Path. - Setz
user.dirnie selbst, weder mitSystem.setProperty()noch mit-Duser.dir. Soll dein Programm in einem anderen Verzeichnis laufen, musst du es dort starten.
Wie du Pfade mit Path und File zusammensetzt und wie sich getAbsolutePath() von getCanonicalPath() unterscheidet, zeigt der Artikel über Datei- und Verzeichnisnamen in Java aus derselben Serie. Wie du Dateien und Verzeichnisse anlegst, kopierst, verschiebst und löschst, zeigt der Artikel Dateien und Verzeichnisse erstellen, kopieren, verschieben, löschen.
Wenn dir dieser Artikel gefallen hat, freue ich mich über eine Bewertung auf meinem ProvenExpert-Profil.




