Java ist eine der ältesten noch breit eingesetzten Programmiersprachen der Industrie. Sie prägt Unternehmensanwendungen, Android-Apps und große Datenverarbeitungssysteme. Dieser Artikel führt in die Grundlagen ein und ordnet ein, wann Java die richtige Wahl ist.
Warum Java eine eigene Laufzeitumgebung hat
Java-Code wird nicht direkt in Maschinencode übersetzt, sondern in einen Zwischencode (Bytecode). Dieser läuft auf der Java Virtual Machine (JVM) – einem Programm, das die Zielplattform bereitstellt. Daraus folgen die Eigenschaften, die Java bekannt gemacht haben:
- Plattformunabhängigkeit: Dasselbe kompilierte Programm läuft auf Linux, Windows und macOS. „Write once, run anywhere“ war das ursprüngliche Versprechen.
- Automatische Speicherverwaltung: Die Garbage Collection räumt nicht mehr erreichbare Objekte auf. Manuelle Speicherfreigabe wie in C entfällt.
- Laufzeitoptimierung: Die JVM übersetzt häufig durchlaufene Codepfade im Betrieb in Maschinencode (Just-in-Time-Kompilierung). Programme werden dadurch mit der Laufzeit schneller.
Grundstruktur
// Datei: Begruessung.java — Klassenname und Dateiname muessen uebereinstimmen
public class Begruessung {
// Einstiegspunkt: exakt diese Signatur
public static void main(String[] args) {
System.out.println("Hallo, " + (args.length > 0 ? args[0] : "Welt"));
int anzahl = 3;
double preis = 19.99;
boolean aktiv = true;
String name = "Server"; // String, nicht string
var kurz = "ab Java 10"; // Typ wird abgeleitet
// Arrays sind Objekte mit fester Laenge
String[] dienste = {"nginx", "postgresql", "redis"};
System.out.println(dienste.length);
}
}
javac Begruessung.java # kompilieren -> Begruessung.class
java Begruessung Welt # ausfuehren (ohne .class!)
Klassen und Objekte
Java ist streng objektorientiert: Fast alles steht in einer Klasse. Ein modernes Beispiel mit den seit Java 14 stabilen Records, die für reine Datenklassen viel Schreibarbeit sparen:
public record Server(String name, String ip, int port) {
// Konstruktor, Getter, equals, hashCode und toString automatisch
public String adresse() {
return ip + ":" + port;
}
}
// Verwendung
var s = new Server("web01", "10.0.0.10", 443);
System.out.println(s.adresse());
System.out.println(s.name()); // Getter heisst name(), nicht getName()
// Klassische Klasse mit Verhalten
public class Konto {
private double stand; // private = nur innerhalb der Klasse
public Konto(double start) {
this.stand = start;
}
public void einzahlen(double betrag) {
if (betrag <= 0) throw new IllegalArgumentException("Betrag muss positiv sein");
this.stand += betrag;
}
public double getStand() { return stand; }
}
Vererbung, Schnittstellen, Generics
// Schnittstelle: beschreibt Faehigkeiten, ohne Implementierung
public interface Speicherbar {
void speichern(); // automatisch public abstract
default String beschreibung() { // Standardimplementierung moeglich
return "speicherbares Objekt";
}
}
// Vererbung mit extends (nur eine Basisklasse!)
public class DateiSpeicher implements Speicherbar {
@Override
public void speichern() {
System.out.println("in Datei geschrieben");
}
}
// Generics: Typsicherheit bei Sammlungen
List<String> namen = new ArrayList<>();
namen.add("alpha");
// namen.add(42); // Kompilierfehler - gut so!
Map<String, Integer> zaehler = new HashMap<>();
zaehler.merge("treffer", 1, Integer::sum);
Ausnahmen
try {
var inhalt = Files.readString(Path.of("/etc/hostname"));
System.out.println(inhalt.trim());
} catch (IOException e) {
System.err.println("Lesefehler: " + e.getMessage());
} finally {
System.out.println("wird immer ausgefuehrt");
}
// Eigene Ausnahme
public class KonfigFehler extends Exception {
public KonfigFehler(String msg) { super(msg); }
}
Java unterscheidet geprüfte und ungeprüfte Ausnahmen: Geprüfte müssen im Code behandelt oder weitergereicht werden – der Compiler erzwingt das. Das ist am Anfang lästig, verhindert aber vergessene Fehlerbehandlung.
Modernes Java: Streams und Lambdas
record Dienst(String name, boolean laeuft) {}
var dienste = List.of(
new Dienst("nginx", true),
new Dienst("redis", false),
new Dienst("postgres", true)
);
var laufend = dienste.stream()
.filter(Dienst::laeuft) // Methodenreferenz
.map(Dienst::name)
.sorted()
.toList();
System.out.println(laufend); // [nginx, postgres]
var kaputt = dienste.stream()
.filter(d -> !d.laeuft())
.count();
Werkzeuge und Ökosystem
- Build: Maven und Gradle. Beide verwalten Abhängigkeiten und Build-Abläufe; Gradle ist flexibler, Maven strenger standardisiert.
- Test: JUnit 5 ist der Standard. Mockito ergänzt für Attrappen von Abhängigkeiten.
- Frameworks: Spring Boot dominiert bei Web- und Unternehmensanwendungen; Quarkus und Micronaut optimieren auf schnellen Start und wenig Speicher – relevant für Container.
- Laufzeit: OpenJDK in verschiedenen Distributionen (Temurin, Corretto, Zulu). Seit Java 17 sind die LTS-Versionen 17 und 21 breit im Einsatz.
Wann Java passt – und wann nicht
Geeignet: umfangreiche Unternehmensanwendungen mit langer Lebensdauer, Systeme mit vielen Entwicklern, Android-Apps, Datenverarbeitung mit großen Mengen, Anwendungen, die nach dem Anlaufen dauerhaft hohe Last fahren.
Weniger geeignet: kurze Skripte (dafür ist Python oder Bash besser), sehr speicherknappe Umgebungen (JVM-Grundlast liegt bei einigen Dutzend Megabyte), und Situationen, in denen schnelle Startzeit wichtiger ist als Spitzenleistung – hier sind Go oder Rust oft die passendere Wahl.
Verwandte Grundlagen: JVM, Virtualisierung, Rust: Ownership, Java-Befehle.