3.6 Pakete schnüren, Importe und Compilationseinheiten
Die Klassenbibliothek von Java ist mit Tausenden Typen sehr umfangreich und deckt alles ab, was Entwickler von plattformunabhängigen Programmen als Basis benötigen. Dazu gehören Datenstrukturen, Klassen zur Datums-/Zeitberechnung, Dateiverarbeitung usw. Die meisten Typen sind in Java selbst implementiert (und der Quellcode ist in der Regel aus der Entwicklungsumgebung direkt verfügbar), aber einige Teile sind nativ implementiert, etwa wenn es darum geht, aus einer Datei zu lesen.
Wenn wir eigene Klassen programmieren, ergänzen sie sozusagen die Standardbibliothek; im Endeffekt wächst damit die Anzahl der möglichen Typen, die ein Programm nutzen kann.
3.6.1 Java-Pakete
Ein Paket ist eine Gruppe thematisch zusammengehöriger Typen. Pakete lassen sich in Hierarchien ordnen, sodass ein Paket wieder ein anderes Paket enthalten kann; das ist genauso wie bei der Verzeichnisstruktur des Dateisystems. Beispiele für Pakete sind:
java.awt
java.util
com.google
org.apache.commons.math3.fraction
com.tutego.insel
Die Klassen der Java-Standardbibliothek befinden sich in Paketen, die mit java und javax beginnen. Google nutzt die Wurzel com.google; die Apache Foundation veröffentlicht Java-Code unter org.apache. So können wir von außen ablesen, von welchen Typen die eigene Klasse abhängig ist.
3.6.2 Pakete der Standardbibliothek
Die logische Gruppierung und Hierarchie lässt sich sehr gut an der Java-Bibliothek beobachten. Die Java-Standardbibliothek beginnt mit der Wurzel java, einige Typen liegen in javax. Unter diesem Paket liegen weitere Pakete, etwa awt, math und util. In java.math liegen zum Beispiel die Klassen BigInteger und BigDecimal, denn die Arbeit mit beliebig großen Ganz- und Fließkommazahlen gehört eben zum Mathematischen. Ein Punkt und ein Polygon, repräsentiert durch die Klassen Point und Polygon, gehören in das Paket für grafische Oberflächen, und das ist das Paket java.awt.
Wenn jemand eigene Klassen in Pakete mit dem Präfix java setzen würde, etwa java.tutego, würde ein Programmautor damit Verwirrung stiften, da nicht mehr nachvollziehbar ist, ob das Paket Bestandteil jeder Distribution ist. Daher ist dieses Präfix für eigene Pakete verboten.
Klassen, die in einem Paket liegen, das mit javax beginnt, können Teil der Java SE sein wie javax.swing, müssen aber nicht zwingend zur Java SE gehören; dazu folgt mehr in Abschnitt 15.1.2, »Übersicht über die Pakete der Standardbibliothek«.
3.6.3 Volle Qualifizierung und import-Deklaration
Um die Klasse Point, die im Paket java.awt liegt, außerhalb des Pakets java.awt zu nutzen – und das ist für uns Nutzer immer der Fall –, muss sie dem Compiler mit der gesamten Paketangabe bekannt gemacht werden. Hierzu reicht der Klassenname allein nicht aus, denn es kann ja sein, dass der Klassenname mehrdeutig ist und eine Klassendeklaration in unterschiedlichen Paketen existiert.
Typen sind erst durch die Angabe ihres Pakets eindeutig identifiziert. Ein Punkt trennt Pakete, also schreiben wir java.awt und java.util – nicht einfach nur awt oder util. Mit einer weltweit unzähligen Anzahl von Paketen und Klassen wäre sonst eine Eindeutigkeit gar nicht machbar. Es kann in verschiedenen Paketen durchaus ein Typ mit gleichem Namen vorkommen, etwa java.util.List und java.awt.List oder java.util.Date und java.sql.Date. Und org.apache.commons.lang3.tuple hat einen Typ Pair, javafx.util auch. Daher bilden Paket und Typ eine Einheit und das eine ist oder das andere nicht eindeutig.
Um dem Compiler die präzise Zuordnung einer Klasse zu einem Paket zu ermöglichen, gibt es zwei Möglichkeiten: Zum einen lassen sich die Typen voll qualifizieren, wie wir das bisher getan haben. Eine alternative und praktischere Möglichkeit besteht darin, den Compiler mit einer import-Deklaration auf die Typen im Paket aufmerksam zu machen:
| import java.awt.Point; |
Während der Quellcode auf der linken Seite die volle Qualifizierung verwendet und jeder Verweis auf einen Typ mehr Schreibarbeit kostet, ist im rechten Fall beim import nur der Klassenname genannt und die Paketangabe in ein import »ausgelagert«. Alle Typen, die bei import genannt werden, merkt sich der Compiler für diese Datei in einer Datenstruktur. Kommt der Compiler zur Zeile mit Point p = new Point();, findet er den Typ Point in seiner Datenstruktur und kann den Typ dem Paket java.awt zuordnen. Damit ist wieder die unabkömmliche Qualifizierung gegeben.
[»] Hinweis
Die Typen aus java.lang sind automatisch importiert, sodass z. B. ein import java.lang. String; nicht nötig ist.
[+] Tipp
Import-Deklarationen sind der übliche Weg, um Schreibarbeit zu sparen. Eine Ausnahme bilden gleichlautende Typen in unterschiedlichen Paketen. So gibt es in den Paketen java.awt und java.util den Typ List. Ein einfaches import java.awt.* und java.util.* hilft da nicht, weil der Compiler nicht weiß, ob die GUI-Komponente oder die Datenstruktur gemeint ist. Eine volle Qualifizierung löst das Problem.
3.6.4 Mit import p1.p2.* alle Typen eines Pakets erreichen
Greift eine Java-Klasse auf mehrere andere Typen des gleichen Pakets zurück, kann die Anzahl der import-Deklarationen groß werden. In unserem Beispiel nutzen wir mit Point und Polygon nur zwei Klassen aus java.awt, aber es lässt sich schnell ausmalen, was passiert, wenn aus dem Paket für grafische Oberflächen zusätzlich Fenster, Beschriftungen, Schaltflächen, Schieberegler usw. eingebunden werden. In diesem Fall darf ein * als letztes Glied in einer import-Deklaration stehen:
import java.awt.*;
import java.math.*;
Mit dieser Syntax kennt der Compiler alle Typen im Paket java.awt und java.math, sodass die Klassen Point und Polygon genau bekannt sind, wie auch die Klasse BigInteger.
[»] Hinweis
Das * ist nur auf der letzten Hierarchieebene erlaubt und gilt immer für alle Typen in diesem Paket. Syntaktisch falsch sind:
import *; // Syntax error on token "*", Identifier expected
import java.awt.Po*; // Syntax error on token "*", delete this token
Eine Anweisung wie import java.*; ist zwar syntaktisch korrekt, aber dennoch ohne Wirkung, denn direkt im Paket java gibt es keine Typdeklarationen, sondern nur Unterpakete.
Die import-Deklaration bezieht sich nur auf ein Verzeichnis (in der Annahme, dass die Pakete auf das Dateisystem abgebildet werden) und schließt die Unterverzeichnisse nicht ein.
Das * verkürzt zwar die Anzahl der individuellen import-Deklarationen, es ist aber gut, zwei Dinge im Kopf zu behalten:
Falls zwei unterschiedliche Pakete einen gleichlautenden Typ beherbergen, etwa Date in java.util und java.sql, so kommt es bei der Verwendung des Typs zu einem Übersetzungsfehler. Hier muss voll qualifiziert werden.
Die Anzahl der import-Deklarationen sagt etwas über den Grad der Komplexität aus. Je mehr import-Deklarationen es gibt, desto größer werden die Abhängigkeiten zu anderen Klassen, was im Allgemeinen ein Alarmzeichen ist. Zwar zeigen grafische Tools die Abhängigkeiten genau an, doch ein import * kann diese erst einmal verstecken.
[+] Best-Practice
Entwicklungsumgebungen setzen die import-Deklarationen in der Regel automatisch und falten die Blöcke üblicherweise ein. Daher sollte der * nur sparsam eingesetzt werden, denn er »verschmutzt« den Namensraum durch viele Typen und erhöht die Gefahr von Kollisionen.
3.6.5 Hierarchische Strukturen über Pakete und die Spiegelung im Dateisystem
Die zu einem Paket gehörenden Klassen befinden sich normalerweise[ 115 ](Ich schreibe »normalerweise«, da die Paketstruktur nicht zwingend auf Verzeichnisse abgebildet werden muss. Pakete könnten beispielsweise vom Klassenlader aus einer Datenbank gelesen werden. Im Folgenden wollen wir aber immer von Verzeichnissen ausgehen. ) im gleichen Verzeichnis. Der Name des Pakets ist gleich dem Namen des Verzeichnisses (und natürlich umgekehrt). Statt des Verzeichnistrenners (etwa »/« oder »\«) steht ein Punkt.
Nehmen wir folgende Verzeichnisstruktur mit einer Hilfsklasse an:
com/tutego/insel/printer/DatePrinter.class
Hier ist der Paketname com.tutego.insel.printer und somit der Verzeichnisname com/tutego/insel/printer. Umlaute und Sonderzeichen sollten vermieden werden, da sie auf dem Dateisystem immer wieder für Ärger sorgen. Aber Bezeichner sollten ja sowieso immer auf Englisch sein.
Der Aufbau von Paketnamen
Prinzipiell kann ein Paketname beliebig sein, doch Hierarchien bestehen in der Regel aus umgedrehten Domänennamen. Aus der Domäne zur Webseite http://tutego.com wird also com.tutego. Diese Namensgebung gewährleistet, dass Klassen auch weltweit eindeutig bleiben. Ein Paketname wird in aller Regel komplett kleingeschrieben.
3.6.6 Die package-Deklaration
Um die Klasse DatePrinter in ein Paket com.tutego.insel.printer zu setzen, müssen zwei Dinge gelten:
Sie muss sich physikalisch in einem Verzeichnis befinden, also in com/tutego/insel/ printer.
Der Quellcode enthält zuoberst eine package-Deklaration.
Die package-Deklaration muss ganz am Anfang stehen, sonst gibt es einen Übersetzungsfehler (selbstverständlich lassen sich Kommentare vor die package-Deklaration setzen):
package com.tutego.insel.printer;
import java.time.LocalDate;
import java.time.format.*;
public class DatePrinter {
public static void printCurrentDate() {
DateTimeFormatter fmt = DateTimeFormatter.ofLocalizedDate( FormatStyle.MEDIUM );
System.out.println( LocalDate.now().format( fmt ) );
}
}
Hinter die package-Deklaration kommen wie gewohnt import-Deklaration(en) und die Typdeklaration(en).
Um die Klasse zu nutzen, bieten sich wie bekannt zwei Möglichkeiten: einmal über die volle Qualifizierung und einmal über die import-Deklaration. Die erste Variante sieht so aus:
public class DatePrinterUser1 {
public static void main( String[] args ) {
com.tutego.insel.printer.DatePrinter.printCurrentDate(); // 20.09.2020
}
}
Und hier ist die Variante mit der import-Deklaration:
import com.tutego.insel.printer.DatePrinter;
public class DatePrinterUser2 {
public static void main( String[] args ) {
DatePrinter.printCurrentDate(); // 20.09.2020
}
}
[+] Tipp
Eine Entwicklungsumgebung nimmt uns viel Arbeit ab, daher bemerken wir die Dateioperationen – wie das Anlegen von Verzeichnissen – in der Regel nicht. Auch das Verschieben von Typen in andere Pakete und die damit verbundenen Änderungen im Dateisystem und die Anpassungen an den import- und package-Deklarationen übernimmt eine moderne IDE für uns.
3.6.7 Unbenanntes Paket (default package)
Eine Klasse ohne Paketangabe befindet sich im unbenannten Paket (engl. unnamed package) bzw. Default-Paket. Es ist eine gute Idee, eigene Klassen immer in Paketen zu organisieren. Das erlaubt auch feinere Sichtbarkeiten, und Konflikte mit anderen Unternehmen und Autoren werden vermieden. Es wäre ein großes Problem, wenn a) jedes Unternehmen unübersichtlich alle Klassen in das unbenannte Paket setzen und dann b) versuchen würde, die Bibliotheken auszutauschen: Konflikte wären vorprogrammiert.
Eine im Paket befindliche Klasse kann jede andere sichtbare Klasse aus anderen Paketen importieren, aber keine Klassen aus dem unbenannten Paket. Nehmen wir Sugar im unbenannten Paket und Chocolate im Paket com.tutego an:
Sugar.class
com/tutego/insel/Chocolate.class
Die Klasse Chocolate kann Sugar nicht nutzen, da Klassen aus dem unbenannten Paket nicht für Unterpakete sichtbar sind. Nur andere Klassen im unbenannten Paket können Klassen im unbenannten Paket nutzen.
Stände nun Sugar in einem Paket – das auch ein Oberpaket sein kann! –, so wäre das wiederum möglich, und Chocolate könnte Sugar importieren:
com/Sugar.class
com/tutego/insel/Chocolate.class
3.6.8 Compilationseinheit (Compilation Unit)
Eine .java-Datei ist eine Compilationseinheit (Compilation Unit), die aus drei (optionalen) Segmenten besteht – in dieser Reihenfolge:
package-Deklaration
import-Deklaration(en)
Typdeklaration(en)
So besteht eine Compilationseinheit aus höchstens einer Paketdeklaration (nicht nötig, wenn der Typ im Default-Paket stehen soll), beliebig vielen import-Deklarationen und beliebig vielen Typdeklarationen. Der Compiler übersetzt jeden Typ einer Compilationseinheit in eine eigene .class-Datei. Ein Paket ist letztendlich eine Sammlung aus Compilationseinheiten. In der Regel ist die Compilationseinheit eine Quellcodedatei; die Codezeilen könnten grundsätzlich auch aus einer Datenbank kommen oder zur Laufzeit generiert werden.
3.6.9 Statischer Import *
Die import-Deklaration informiert den Compiler über die Pakete, sodass ein Typ nicht mehr voll qualifiziert werden muss, wenn er im import-Teil explizit aufgeführt wird oder wenn das Paket des Typs über * genannt ist.
Falls eine Klasse statische Methoden oder Konstanten vorschreibt, werden ihre Eigenschaften immer über den Typnamen angesprochen. Java bietet mit dem statischen Import die Möglichkeit, die statischen Methoden oder Variablen ohne vorangestellten Typnamen sofort zu nutzen. Während also das normale import dem Compiler Typen benennt, macht ein statisches import dem Compiler Klasseneigenschaften bekannt, geht also eine Ebene tiefer.
[zB] Beispiel
Binde für die Bildschirmausgabe die statische Variable out aus System statisch ein:
import static java.lang.System.out;
Bei der sonst üblichen Ausgabe über System.out.printXXX(…) kann nach dem statischen Import der Klassenname entfallen, und es bleibt beim out.printXXX(…).
Binden wir in einem Beispiel mehrere statische Eigenschaften mit einem statischen import ein:
package com.tutego.insel.oop;
import static java.lang.System.out;
import static javax.swing.JOptionPane.showInputDialog;
import static java.lang.Integer.parseInt;
import static java.lang.Math.max;
import static java.lang.Math.min;
class StaticImport {
public static void main( String[] args ) {
int i = parseInt( showInputDialog( "Erste Zahl" ) );
int j = parseInt( showInputDialog( "Zweite Zahl" ) );
out.printf( "%d ist größer oder gleich %d.%n",
max(i, j), min(i, j) );
}
}
Mehrere Typen statisch importieren
Der statische Import
import static java.lang.Math.max;
import static java.lang.Math.min;
bindet die statische max(…)/min(…)-Methode ein. Besteht Bedarf an weiteren statischen Methoden, gibt es neben der individuellen Aufzählung eine Wildcard-Variante:
import static java.lang.Math.*;
[»] Best Practice
Auch wenn Java diese Möglichkeit bietet, sollte der Einsatz maßvoll erfolgen. Die Möglichkeit der statischen Importe ist nützlich, wenn Klassen Konstanten nutzen wollen, allerdings besteht auch die Gefahr, dass durch den fehlenden Typnamen nicht mehr sichtbar ist, woher die Eigenschaft eigentlich kommt und welche Abhängigkeit sich damit aufbaut. Auch gibt es Probleme mit gleichlautenden Methoden: Eine Methode aus der eigenen Klasse überdeckt statisch importierte Methoden. Wenn also später in der eigenen Klasse – oder Oberklasse – eine Methode aufgenommen wird, die die gleiche Signatur hat wie eine statisch importierte Methode, wird das zu keinem Compilerfehler führen, sondern die Semantik wird sich ändern, weil jetzt die neue eigene Methode verwendet wird und nicht mehr die statisch importierte.