Zum Inhalt springen

Java: Aktuelles Arbeitsverzeichnis ermitteln (user.dir, Path, File)

Ein hell gezeichneter Stadtplan auf Sonnengelb, von oben gesehen: Von zwei Messingnadeln läuft je ein roter Faden in derselben Form – nach rechts, ein Stück nach oben, wieder nach rechts – und endet an einer leuchtenden bernsteinfarbenen Perle. Die rechte Route ist eine verschobene Kopie der linken

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.dir das 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:

Fünf Kästen von oben nach unten: Shell mit cd /Users/sven, Arbeitsverzeichnis des JVM-Prozesses, System-Property user.dir, File und Path, relativer Pfad files/server.log aufgelöst zu /Users/sven/files/server.log
File und Path merken sich das Arbeitsverzeichnis beim Start der JVM und rechnen danach mit diesem Wert

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")).

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:

StartumgebungArbeitsverzeichnisEinstellung
Terminal: java und java -jardas Verzeichnis, in dem die Shell stehtcd vor dem Start
Maven: Tests mit Surefiredas Verzeichnis der pom.xml, bei mehreren Modulen das des ModulsParameter workingDirectory
Maven: spring-boot:run und quarkus:devdas Verzeichnis der pom.xml, bei mehreren Modulen das des ModulsParameter workingDirectory bzw. workingDir
Gradle: test, run, bootRun und quarkusDevdas Verzeichnis des Projekts, bei mehreren Teilprojekten das des TeilprojektsEigenschaft workingDir bzw. workingDirectory
IntelliJ IDEA: Anwendungdas in IntelliJ geöffnete ProjektverzeichnisFeld „Working directory“ der Run-Konfiguration
IntelliJ IDEA: JUnit-Testsdasselbe Verzeichnis wie bei Maven bzw. GradleFeld „Working directory“ der Run-Konfiguration
Dockerdas Verzeichnis aus WORKDIR, ohne Angabe /WORKDIR im Dockerfile
systemddas Verzeichnis aus WorkingDirectory=, ohne Angabe /WorkingDirectory= in der Unit-Datei

Zu den Zeilen im Einzelnen:

  • Terminal: Genau wie java startet auch java -jar das 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 der pom.xml – in einem Projekt mit mehreren Modulen in dem Verzeichnis mit der pom.xml des jeweiligen Moduls. Auch spring-boot:run und quarkus:dev starten die Anwendung dort, solange du workingDirectory bzw. workingDir nicht setzt.
  • Gradle: Bei test, run und bootRun steht die Eigenschaft workingDir standardmäßig auf projectDir, bei quarkusDev die Eigenschaft workingDirectory. 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 WORKDIR ist das Arbeitsverzeichnis /. Viele Basis-Images setzen aber selbst ein WORKDIR, 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, sucht files/server.log unter /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:run dagegen 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.dir vom Arbeitsverzeichnis des Prozesses ab, löst Path einen relativen Pfad selbst gegen user.dir auf 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. Nur getAbsolutePath() rechnet mit user.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 NoSuchFileException nennt 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:run und mvn quarkus:dev dagegen im Verzeichnis ihres Moduls.
  • user.dir wird mit System.setProperty() geändert: Seit Java 11 lösen File und Path relative Pfade danach weiter gegen das alte Verzeichnis auf.
  • -Duser.dir wird anstelle von cd verwendet: Files.writeString() und FileOutputStream schreiben dann in verschiedene Verzeichnisse, und die JVM findet einen relativen Class Path nicht mehr.
  • new File("") auf Java 24 oder älter: exists() liefert false, obwohl das Arbeitsverzeichnis existiert.
  • Das Arbeitsverzeichnis aus Path.of("."): Der absolute Pfad endet auf /. und ist für equals() ein anderer Pfad als der aus Path.of("").
  • Ein Pfadvergleich mit einem symbolischen Link: user.dir enthält den Pfad, auf den der Link zeigt, unter macOS z. B. /private/tmp statt /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 mit Path.of(".") und – solange dein Code auf Java 24 oder älter läuft – nicht mit new 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.dir nie selbst, weder mit System.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.

👉 Bewertung abgeben

Lust auf noch mehr Wissen?

In meinem Blog findest du viele Artikel zu Java, Softwarearchitektur und Performance von grundlegenden Konzepten bis hin zu fortgeschrittenen Patterns.

Wenn du tiefer einsteigen willst, schau dir meine Trainings an: praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Zu den Java-Schulungen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche