Zum Inhalt springen

Mehrere Klassen, Packages und der Classpath

Ein echtes Programm besteht nicht aus einer Datei, sondern aus Dutzenden oder Hunderten. In diesem Kapitel wachsen wir Schritt für Schritt dorthin: erst zwei Klassen, dann Packages, dann eine saubere Ordnerstruktur und am Ende eine einzige Datei, die man weitergeben kann.

Wir arbeiten weiter mit Notepad und Terminal. Neue Sprachmittel tauchen nur am Rande auf – tippe den Code einfach ab. Was package, import und public genau bedeuten, besprechen wir in späteren Kapiteln.

  • erklären, warum java de.drv.Hallo keine Pfadangabe ist
  • Quelltext und übersetzte Klassen in getrennten Ordnern halten
  • mit dem Classpath festlegen, wo Java nach Klassen sucht
  • ein ausführbares JAR bauen und starten

Lege den Ordner C:\dev\workspace\kapitel03\teil1 an und schreibe zwei Dateien.

Die erste Klasse erzeugt einen Begrüßungstext:

Begruessung.java
public class Begruessung {
public static String fuer(String name) {
return "Hallo " + name + "!";
}
}

Die zweite enthält wie gewohnt die main-Methode und benutzt die erste:

Hallo.java
public class Hallo {
public static void main(String[] args) {
System.out.println(Begruessung.fuer("Welt"));
System.out.println(Begruessung.fuer("DRV"));
}
}

Übersetze nur Hallo.java und schau dir das Verzeichnis an:

PowerShell
cd C:\dev\workspace\kapitel03\teil1
javac Hallo.java
dir
java Hallo
Terminal-Fenster
Hallo Welt!
Hallo DRV!

Obwohl du nur Hallo.java angegeben hast, liegt auch eine Begruessung.class im Ordner. Der Compiler hat bemerkt, dass Hallo die Klasse Begruessung braucht, im aktuellen Verzeichnis nach Begruessung.java gesucht und sie gleich mitübersetzt.

Solange es nur ein paar Klassen gibt, ist ein einzelner Ordner kein Problem. In einem großen Projekt gibt es aber schnell zwei Klassen mit demselben Namen – und irgendeine Ordnung braucht man auch. Dafür gibt es Packages: Jede Klasse gehört zu einem Package, und der Package-Name wird Teil ihres Namens.

Package-Namen werden kleingeschrieben und beginnen üblicherweise mit dem umgekehrten Domain-Namen der Organisation. Aus drv.de wird de.drv. Darunter wird weiter unterteilt, etwa de.drv.util für Hilfsklassen. So sind Namen weltweit eindeutig: Eine Klasse de.drv.Hallo kann nicht mit einer Klasse com.example.Hallo verwechselt werden.

Lege den Ordner C:\dev\workspace\kapitel03\projekt an. Darin schreibst du beide Klassen noch einmal – diesmal mit Package-Angabe in der ersten Zeile:

Begruessung.java
package de.drv.util;
public class Begruessung {
public static String fuer(String name) {
return "Hallo " + name + "!";
}
}
Hallo.java
package de.drv;
import de.drv.util.Begruessung;
public class Hallo {
public static void main(String[] args) {
System.out.println(Begruessung.fuer("Welt"));
System.out.println(Begruessung.fuer("DRV"));
}
}

Die Zeile mit import sagt dem Compiler: Wenn in dieser Datei Begruessung steht, ist de.drv.util.Begruessung gemeint.

Jetzt übersetzen wir wie gewohnt:

PowerShell
cd C:\dev\workspace\kapitel03\projekt
javac Hallo.java
Terminal-Fenster
Hallo.java:3: Fehler: Package de.drv.util ist nicht vorhanden
import de.drv.util.Begruessung;
^
…
3 Fehler

Der Compiler findet Begruessung nicht mehr, obwohl die Datei direkt daneben liegt. Geben wir beide Dateien ausdrücklich an:

PowerShell
javac Hallo.java Begruessung.java
dir

Das klappt, beide .class-Dateien sind da. Also starten:

PowerShell
java Hallo
Terminal-Fenster
Fehler: Hauptklasse Hallo konnte nicht gefunden oder geladen werden
Ursache: java.lang.NoClassDefFoundError: Hallo (wrong name: de/drv/Hallo)

Die Klasse heißt nicht mehr Hallo, sondern de.drv.Hallo – das sagt uns Java sogar. Also mit vollem Namen:

PowerShell
java de.drv.Hallo
Terminal-Fenster
Fehler: Hauptklasse de.drv.Hallo konnte nicht gefunden oder geladen werden
Ursache: java.lang.ClassNotFoundException: de.drv.Hallo

Auch das geht nicht. Warum?

Wenn Java eine Klasse de.drv.Hallo laden soll, sucht es nicht irgendwo nach einer Datei Hallo.class. Es übersetzt den Namen in einen Pfad und schaut genau dort nach:

Klassenname Wo Java sucht
Hallo Hallo.class
de.drv.Hallo de\drv\Hallo.class
de.drv.util.Begruessung de\drv\util\Begruessung.class

Jeder Teil des Package-Namens ist ein Ordner. Genauso sucht der Compiler nach Quelltexten – deshalb hat er vorhin Begruessung.java nicht gefunden: Er hat in de\drv\util\ nachgesehen.

Bringen wir die Dateien also an die richtige Stelle und räumen die alten .class-Dateien weg:

PowerShell
mkdir de\drv\util
Move-Item Hallo.java de\drv
Move-Item Begruessung.java de\drv\util
Remove-Item *.class

Jetzt reicht es wieder, nur die Hauptklasse anzugeben – den Rest findet der Compiler selbst:

PowerShell
javac de\drv\Hallo.java
tree /F
  • Ordnerprojekt
    • Ordnerde
      • Ordnerdrv
        • Hallo.class
        • Hallo.java
        • Ordnerutil
          • Begruessung.class
          • Begruessung.java
PowerShell
java de.drv.Hallo
Terminal-Fenster
Hallo Welt!
Hallo DRV!

Unser Projekt hat zwei Klassen – und die .class-Dateien liegen schon wild zwischen den Quelltexten. Bei hundert Klassen will das niemand mehr sehen. Außerdem gehören nur die Quelltexte ins Archiv bzw. zur Abgabe, die .class-Dateien lassen sich jederzeit neu erzeugen.

Deshalb trennt man üblicherweise:

  • src – die Quelltexte, die du schreibst
  • out – alles, was der Compiler erzeugt

Verschiebe den Ordner de nach src und lösche die .class-Dateien:

PowerShell
mkdir src
Move-Item de src
Get-ChildItem src -Recurse -Filter *.class | Remove-Item

Mit der Option -d (destination) sagst du dem Compiler, wohin er schreiben soll. Die Package-Ordner legt er dort selbst an:

PowerShell
javac -d out src\de\drv\Hallo.java src\de\drv\util\Begruessung.java
tree /F
  • Ordnerprojekt
    • Ordnerout
      • Ordnerde
        • Ordnerdrv
          • Hallo.class
          • Ordnerutil
            • Begruessung.class
    • Ordnersrc
      • Ordnerde
        • Ordnerdrv
          • Hallo.java
          • Ordnerutil
            • Begruessung.java

Diesmal mussten wir beide Dateien angeben: Der Compiler sucht fehlende Quelltexte nur ab dem aktuellen Verzeichnis – und dort liegt jetzt kein de mehr, sondern src.

Starten wir das Programm:

PowerShell
java de.drv.Hallo
Terminal-Fenster
Fehler: Hauptklasse de.drv.Hallo konnte nicht gefunden oder geladen werden
Ursache: java.lang.ClassNotFoundException: de.drv.Hallo

Java sucht de\drv\Hallo.class – aber ausgehend von welchem Ordner? Bisher war das immer das aktuelle Verzeichnis. Jetzt liegen die Klassen in out. Das müssen wir Java sagen, mit dem Classpath:

PowerShell
java -cp out de.drv.Hallo
Terminal-Fenster
Hallo Welt!
Hallo DRV!

Der Aufruf hat jetzt zwei Teile, die man auseinanderhalten muss:

Teil Bedeutung Art
-cp out Wo wird gesucht? Pfad (Ordner oder JAR-Datei)
de.drv.Hallo Was wird gestartet? Klassenname

Ohne -cp ist der Classpath einfach . – das aktuelle Verzeichnis. Deshalb hat bisher alles ohne diese Angabe funktioniert. -cp ist die Kurzform von --class-path; beides bedeutet dasselbe.

Der Classpath kann auch mehrere Einträge enthalten, unter Windows getrennt durch Semikolon: -cp out;lib\eine-bibliothek.jar. So kommen später fremde Bibliotheken ins Spiel.

Unser Programm besteht jetzt aus einem Ordner voller .class-Dateien. Um es weiterzugeben, packt man es in eine einzige Datei: ein JAR (Java Archive). Ein JAR ist technisch einfach ein ZIP-Archiv mit einer festgelegten Struktur.

Das Werkzeug dafür heißt jar und gehört zum JDK:

PowerShell
jar --create --file hallo.jar --main-class de.drv.Hallo -C out .
Option Bedeutung
--create ein neues Archiv anlegen
--file hallo.jar so soll die Datei heißen
--main-class de.drv.Hallo diese Klasse wird beim Start ausgeführt
-C out . wechsle in den Ordner out und nimm von dort alles (.)

Was steckt drin?

PowerShell
jar --list --file hallo.jar
Terminal-Fenster
META-INF/
META-INF/MANIFEST.MF
de/
de/drv/
de/drv/Hallo.class
de/drv/util/
de/drv/util/Begruessung.class

Die Package-Ordner sind auch im Archiv erhalten. Neu ist die Datei META-INF/MANIFEST.MF – darin steht unter anderem, welche Klasse die Hauptklasse ist. Deshalb genügt zum Starten jetzt:

PowerShell
java -jar hallo.jar
Terminal-Fenster
Hallo Welt!
Hallo DRV!

Diese eine Datei läuft auf jedem Rechner mit Java – egal ob Windows, Linux oder macOS.

Fehlermeldung Ursache Lösung
Fehler: Package de.drv.util ist nicht vorhanden Der Compiler findet die Quelldatei nicht. Ordner prüfen: de\drv\util\Begruessung.java; ggf. alle Dateien angeben oder -sourcepath setzen
Ursache: java.lang.ClassNotFoundException: de.drv.Hallo Die Klasse liegt nicht im Classpath. Aus dem Projektordner starten, -cp out angeben
Ursache: java.lang.NoClassDefFoundError: Hallo (wrong name: de/drv/Hallo) Klasse ohne Package-Namen aufgerufen, oder aus dem falschen Ordner gestartet Vollen Namen verwenden: java -cp out de.drv.Hallo
Ursache: java.lang.ClassNotFoundException: de\drv\Hallo Pfad statt Klassenname angegeben Punkte statt Backslashes
Fehler: Hauptklasse de.drv.Hallo konnte nicht gefunden … bei java -jar Beim jar-Aufruf -C out . vergessen – im Archiv liegt dann out/de/drv/… JAR mit -C out . neu bauen

Schon bei zwei Klassen haben wir:

  • Ordner passend zu jedem Package von Hand angelegt
  • Dateien verschoben und .class-Dateien gelöscht
  • lange javac-Aufrufe mit jeder einzelnen Datei getippt
  • den Classpath beim Starten angegeben
  • nach jeder Änderung neu übersetzt – und es manchmal vergessen

Stell dir das mit fünfzig Klassen vor. Genau diese Arbeit nimmt uns eine Entwicklungsumgebung (IDE) ab. Im nächsten Kapitel steigen wir um – und du wirst jeden dieser Schritte dort wiedererkennen.

  1. Schreibe eine dritte Klasse de.drv.util.Verabschiedung mit einer Methode fuer(String name), die "Auf Wiedersehen " + name + "!" zurückgibt. Rufe sie in Hallo auf. Übersetze, starte und baue das JAR neu. Was musst du alles anpassen?
  2. Wechsle mit cd out\de\drv in den Ordner, in dem Hallo.class liegt, und versuche, das Programm von dort zu starten. Warum klappt es weder mit java Hallo noch mit java de.drv.Hallo?
  3. Lösche out\de\drv\util\Begruessung.class und starte mit java -cp out de.drv.Hallo. Was passiert – und warum meldet sich der Fehler erst beim Ausführen und nicht schon beim Übersetzen?
  4. Kopiere hallo.jar, benenne die Kopie in hallo.zip um und öffne sie im Explorer. Öffne META-INF\MANIFEST.MF in Notepad.