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.