Skip to content

Java Spickzettel

Objektorientierte Sprache der Enterprise-Klasse, einmal schreiben, überall ausführen.

01

Erste Schritte

Hello World

Jedes Java-Programm startet bei main(). Der Dateiname muss mit dem Namen der öffentlichen Klasse übereinstimmen (Main.java → Main.class). javac kompiliert zu Bytecode (.class), java führt ihn auf der JVM aus. System.out.println gibt auf stdout aus; printf unterstützt Formatbezeichner (%s, %d, %f, %n für Zeilenumbruch).

java
// Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
        System.out.printf("Name: %s, Age: %d%n", "Alice", 30);
    }
}

// Compile: javac Main.java  -> Main.class
// Run:     java Main
// Package: java -cp . com.example.Main

// Every Java program needs:
// 1. A class (public class, file name must match)
// 2. A main method: public static void main(String[] args)

Variablen & primitive Typen

Java hat 8 primitive Typen (int, long, double, float, boolean, char, byte, short) und Referenztypen (String, Arrays, Objekte). Verwende 'final' für Konstanten. 'var' (Java 10+) leitet den Typ zur Kompilierzeit ab — für lokale Variablen mit offensichtlichen Typen verwenden. Unterstriche in Zahlen (100_000) verbessern die Lesbarkeit.

java
// Primitive types (8 total)
int age = 30;              // 32-bit integer
long bigNum = 100_000L;   // 64-bit integer
double price = 19.99;     // 64-bit float (default for decimals)
float pi = 3.14f;         // 32-bit float
boolean active = true;    // true/false
char grade = 'A';         // 16-bit Unicode character
byte b = 127;             // 8-bit signed
short s = 32767;          // 16-bit signed

// Reference types
String name = "Alice";    // Object (not primitive)
int[] nums = {1, 2, 3};  // Array object

// Constants
final double PI = 3.14159; // can't be reassigned

// var (Java 10+, local type inference)
var count = 42;    // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>

Wrapper-Klassen & Boxing

Wrapper-Klassen (Integer, Double, Boolean usw.) sind Objektversionen von Primitives. Autoboxing/Unboxing konvertiert automatisch. Integer speichert Werte von -128 bis 127 zwischen, daher funktioniert == für kleine Zahlen, schlägt aber bei größeren fehl — immer .equals() verwenden. Wrapper werden für Collections benötigt (die keine Primitives aufnehmen können).

java
// Wrapper classes (Object versions of primitives)
Integer wrapped = Integer.valueOf(42);  // explicit
Integer auto = 42;                       // autoboxing
int unboxed = auto;                      // unboxing

// Useful methods
int max = Integer.MAX_VALUE;     // 2147483647
String bin = Integer.toBinaryString(42);
int parsed = Integer.parseInt("42");
String s = String.valueOf(42);

// Other wrappers: Double, Boolean, Character, Long, Float
Double d = 3.14;
Boolean b = Boolean.TRUE;
Character c = 'A';

// Be careful with == on wrappers
Integer a = 127, b2 = 127;  // a == b2: true (cached)
Integer x = 128, y = 128;   // x == y: false (not cached)
// Always use .equals() for Integer comparison

Packages & Imports

Packages organisieren Klassen und verhindern Namenskonflikte. Konvention: umgekehrter Domainname (com.example.app). Importiere spezifische Klassen oder verwende Wildcards (*). Statische Imports bringen Konstanten und Methoden herein (Math.PI, Math.sqrt). Vollqualifizierte Namen funktionieren ohne Imports, sind aber umständlich. Das java.lang-Package wird automatisch importiert.

java
// Package declaration (must be first line)
package com.example.app;

// Import specific class
import java.util.List;
import java.util.ArrayList;

// Import all classes in a package
import java.util.*;

// Static import (for static members)
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

// Usage
double area = PI * 5 * 5;
double root = sqrt(16);

// Fully qualified name (no import needed)
java.time.LocalDate today = java.time.LocalDate.now();

// Package naming convention: reverse domain
// com.google.gson, org.apache.commons, io.netty.channel

Input & Output

System.out (stdout), System.err (stderr), System.in (stdin). Scanner ist die einfachste Möglichkeit, Konsolen-Eingaben zu lesen — er parst Tokens (nextInt, nextDouble, nextLine). Scanner immer schließen, um Ressourcen freizugeben. Kommandozeilen-Argumente befinden sich in args[] (args[0] ist das erste Argument, nicht der Programmname wie in C).

java
import java.util.Scanner;

// Console output
System.out.println("Hello");      // with newline
System.out.print("No newline");   // without newline
System.out.printf("Pi: %.2f%n", 3.14159); // formatted

// Console input with Scanner
Scanner scanner = new Scanner(System.in);
System.out.print("Enter name: ");
String name = scanner.nextLine();

System.out.print("Enter age: ");
int age = scanner.nextInt();

System.out.printf("Hi %s, age %d%n", name, age);
scanner.close(); // always close

// Command-line arguments
// java Main arg1 arg2
// args[0] = "arg1", args[1] = "arg2"
02

Strings & Formatierung

String-Methoden

Strings sind unveränderlich — Methoden geben neue Strings zurück. Verwende immer .equals() für Inhaltsvergleich (== vergleicht Referenzen). compareTo() gibt negativ/null/positiv für Sortierung zurück (nützlich zum Sortieren). split() gibt ein String[] zurück. Für veränderliche Strings StringBuilder verwenden.

java
String s = "Hello, World";

// Length and access
int len = s.length();        // 12
char c = s.charAt(0);        // 'H'

// Comparison
s.equals("Hello, World");    // true (content comparison)
s.equalsIgnoreCase("hello, world"); // true
s.compareTo("Apple");        // positive (s > "Apple")
"abc".compareTo("abd");      // negative

// Search
s.indexOf("World");          // 7 (-1 if not found)
s.lastIndexOf("l");          // 10
s.contains("World");         // true
s.startsWith("Hello");       // true
s.endsWith("World");         // true

// Extract
s.substring(7);              // "World"
s.substring(0, 5);           // "Hello"

// Transform
s.toUpperCase();             // "HELLO, WORLD"
s.toLowerCase();             // "hello, world"
s.replace("o", "0");         // "Hell0, W0rld"
s.trim();                    // remove whitespace
s.split(", ");               // ["Hello", "World"]

StringBuilder & Konkatenation

String-Konkatenation mit + erzeugt jedes Mal einen neuen String (ineffizient in Schleifen). StringBuilder ist veränderlich und effizient beim inkrementellen Aufbauen von Strings. StringBuffer ist die Thread-sichere Version (selten benötigt). String.join() kombiniert mit einem Trennzeichen. Java 11+ fügt repeat() für String-Multiplikation hinzu.

java
// String concatenation (creates new String each time)
String s = "Hello" + ", " + "World";
String formatted = String.format("%s is %d", "Alice", 30);

// StringBuilder (mutable, efficient for many concatenations)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
sb.insert(5, " there");
sb.delete(5, 11);
sb.reverse();
String result = sb.toString();

// StringBuffer (thread-safe, slower than StringBuilder)
StringBuffer sbf = new StringBuffer("thread-safe");

// Join strings
String joined = String.join(", ", "a", "b", "c"); // "a, b, c"

// Repeat (Java 11+)
String repeated = "ab".repeat(3); // "ababab"

String-Formatierung

printf/format verwendet C-artige Formatbezeichner: %d (int), %f (float), %s (string), %c (char), %b (boolean), %x (hex). Breite (%5d), linksbündig (%-5d), Null-Auffüllung (%05d), Genauigkeit (%.2f). %n ist der plattformspezifische Zeilenumbruch. Text Blocks (Java 15+) mit dreifachen Anführungszeichen ermöglichen mehrzeilige Strings ohne Escaping.

java
// printf / format specifiers
System.out.printf("Int: %d%n", 42);
System.out.printf("Float: %.2f%n", 3.14159);  // 3.14
System.out.printf("String: %s%n", "hello");
System.out.printf("Char: %c%n", 'A');
System.out.printf("Bool: %b%n", true);
System.out.printf("Hex: %x%n", 255);           // ff
System.out.printf("Octal: %o%n", 8);           // 10

// Width and padding
System.out.printf("[%5d]%n", 42);      // [   42]
System.out.printf("[%-5d]%n", 42);     // [42   ]
System.out.printf("[%05d]%n", 42);     // [00042]
System.out.printf("[%8.2f]%n", 3.14);  // [    3.14]

// String.format returns a String
String s = String.format("Name: %s, Age: %d", "Alice", 30);

// Text blocks (Java 15+)
String json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

Reguläre Ausdrücke

Java-Regex verwendet Pattern (kompiliert) und Matcher (auf Eingabe angewendet). String-Methoden (matches, split, replaceAll) sind praktische Abkürzungen. Backslashes müssen in Java-String-Literalen verdoppelt werden (\\d für \d). Gruppen werden mit Klammern erfasst und in Ersetzungen als $1, $2 referenziert. Patterns bei wiederholter Verwendung einmal kompilieren.

java
import java.util.regex.*;

// String methods
"hello123".matches("[a-z]+\d+"); // true
"a,b,c".split(",");               // ["a", "b", "c"]
"hello".replaceAll("l", "L");     // "heLLo"

// Pattern and Matcher
Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher m = p.matcher("Date: 2024-01-15");

if (m.find()) {
    System.out.println(m.group()); // "2024-01-15"
}

// Find all matches
while (m.find()) {
    System.out.println(m.group());
}

// Replace with regex
String result = "2024-01-15".replaceAll(
    "(\\d{4})-(\\d{2})-(\\d{2})",
    "$3/$2/$1"); // "15/01/2024"

// Common patterns
String email = "^[\\w.]+@[\\w.]+\\.\\w+$";
String phone = "^\\d{3}-\\d{4}$";

Zahlen & Math

Die Math-Klasse bietet statische mathematische Funktionen. Math.random() gibt 0.0-1.0 zurück. Für mehr Kontrolle java.util.Random (seedbar) oder java.security.SecureRandom (kryptografisch) verwenden. Integer/Double haben statische Utility-Methoden. Vorsicht mit Fließkomma-Präzision — für finanzielle Berechnungen BigDecimal verwenden.

java
// Math class
double sqrt = Math.sqrt(16);     // 4.0
double pow = Math.pow(2, 10);    // 1024.0
int abs = Math.abs(-5);          // 5
int max = Math.max(3, 7);        // 7
int min = Math.min(3, 7);        // 3
double rounded = Math.round(3.7); // 4
double ceil = Math.ceil(3.1);    // 4.0
double floor = Math.floor(3.9);  // 3.0
double random = Math.random();   // 0.0 to 1.0

// Constants
double pi = Math.PI;             // 3.14159...
double e = Math.E;               // 2.71828...

// Integer/Long methods
int sum = Integer.sum(3, 4);     // 7
int max2 = Integer.max(3, 7);    // 7

// Rounding modes
double r = Math.round(3.5);      // 4 (round half up)
double r2 = Math.floor(3.5 + 0.5); // alternative

// Random (java.util.Random)
import java.util.Random;
Random rand = new Random();
int n = rand.nextInt(100);  // 0-99
double d = rand.nextDouble(); // 0.0-1.0
boolean b = rand.nextBoolean();
03

Kontrollfluss

If / Else

Java if/else funktioniert wie C/C++. Bedingungen müssen boolean sein — kein Truthy/Falsy wie bei JavaScript (0 und nicht-leere Strings sind NICHT truthy). Der ternäre Operator (cond ? a : b) ist ein Ausdruck, keine Anweisung. Verwende geschweifte Klammern auch für einzeilige Bodies (Best Practice für Code-Stil).

java
int score = 85;

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
} else {
    System.out.println("F");
}

// Ternary operator
String grade = score >= 60 ? "Pass" : "Fail";

// Nested if
if (score >= 60) {
    if (score >= 90) {
        System.out.println("Excellent");
    }
}

// Note: conditions must be boolean (no truthy/falsy)
// if (score) { } // Error: int is not boolean

Switch & Expressions

Traditionelles switch hat Fall-through (break verwenden). Java 14+ Switch-Expressions (->) fallen nicht durch und können Werte zurückgeben. Kommas für mehrere case-Labels verwenden (case 1, 2, 3). 'yield' gibt einen Wert aus einem komplexen Block zurück. Switch-Expressions sind erschöpfend — du brauchst einen default oder alle Cases für Enums.

java
// Traditional switch (fall-through)
int day = 3;
switch (day) {
    case 1:
        System.out.println("Mon");
        break;
    case 2:
    case 3:
    case 4:
        System.out.println("Midweek");
        break;
    case 6:
    case 7:
        System.out.println("Weekend");
        break;
    default:
        System.out.println("Invalid");
}

// Switch expression (Java 14+, no fall-through)
String type = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Invalid";
};

// Switch with yield (for complex blocks)
int result = switch (day) {
    case 1, 2, 3, 4, 5 -> {
        int hours = 8;
        yield hours * 5;
    }
    case 6, 7 -> 0;
    default -> -1;
};

Schleifen

Java hat for-, while- und do-while-Schleifen. Die erweiterte for-Schleife (for-each) funktioniert mit Arrays und jedem Iterable. break verlässt die Schleife; continue springt zur nächsten Iteration. Für Collections for-each oder Streams gegenüber indizierten Schleifen bevorzugen. do-while läuft mindestens einmal (selten verwendet).

java
// For loop
for (int i = 0; i < 5; i++) {
    System.out.println(i);
}

// Enhanced for (for-each)
int[] nums = {1, 2, 3, 4, 5};
for (int n : nums) {
    System.out.println(n);
}

List<String> names = List.of("Alice", "Bob");
for (String name : names) {
    System.out.println(name);
}

// While loop
int count = 0;
while (count < 3) {
    System.out.println(count);
    count++;
}

// Do-while (runs at least once)
int i = 0;
do {
    System.out.println(i);
    i++;
} while (i < 3);

// Break and continue
for (int j = 0; j < 10; j++) {
    if (j == 5) break;      // exit loop
    if (j % 2 == 0) continue; // skip iteration
    System.out.println(j);
}

Markiertes Break & Continue

Labels (outer:) erlauben das Abbrechen/Fortsetzen äußeren Schleifen aus verschachtelten Schleifen. Dies wird selten benötigt — das Auslagern in eine Methode mit return ist meist sauberer. Labels werden vor die Schleife platziert, gefolgt von einem Doppelpunkt. break label verlässt die markierte Schleife; continue label springt zu deren nächster Iteration.

java
// Labels for breaking out of nested loops
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i == 1 && j == 1) {
            break outer; // exits both loops
        }
        System.out.println(i + "," + j);
    }
}

// Labeled continue
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) {
            continue outer; // skip to next i
        }
        System.out.println(i + "," + j);
    }
}

// Alternative: extract to method and use return
void findPair(int[][] matrix, int target) {
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            if (matrix[i][j] == target) return; // exit method
        }
    }
}

Arrays

Arrays haben feste Länge (für dynamische ArrayList verwenden). Arrays.sort() sortiert in-place. Arrays.toString() gibt eine lesbare Darstellung. Arrays.copyOf() erstellt eine Kopie mit neuer Länge. Bei mehrdimensionalen Arrays kann jede Zeile eine unterschiedliche Länge haben (gezackte Arrays). Arrays für Utility-Methoden auf Arrays verwenden.

java
// Declare and initialize
int[] nums = {1, 2, 3, 4, 5};
int[] empty = new int[5]; // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]

// Access and modify
nums[0] = 10;
int first = nums[0]; // 10
int length = nums.length; // 5

// Multidimensional
int[][] matrix = {{1, 2}, {3, 4}};
int val = matrix[0][1]; // 2

// Arrays utility class
import java.util.Arrays;
int[] sorted = {3, 1, 2};
Arrays.sort(sorted); // [1, 2, 3]
int[] copy = Arrays.copyOf(nums, 3);
String str = Arrays.toString(nums); // "[10, 2, 3, 4, 5]"
boolean eq = Arrays.equals(nums, copy);

// Fill
int[] filled = new int[5];
Arrays.fill(filled, 42); // [42, 42, 42, 42, 42]

// Binary search (sorted array only)
int idx = Arrays.binarySearch(sorted, 2); // index of 2
04

Methoden & Funktionen

Methodendefinition

Java-Methoden sind immer innerhalb einer Klasse. 'static' bedeutet, dass die Methode zur Klasse gehört (ohne Instanz aufrufbar). Der Rückgabetyp (int, String, void) wird vor dem Namen deklariert. Parameter sind typisiert. Java hat keine Standard-Parameterwerte — stattdessen Methodenüberladung verwenden.

java
public class Calculator {
    // Method with return type
    public static int add(int a, int b) {
        return a + b;
    }

    // Void method (no return)
    public static void printResult(int result) {
        System.out.println("Result: " + result);
    }

    // Method with default (no overloading needed)
    public static String greet(String name, String greeting) {
        return greeting + ", " + name + "!";
    }

    public static void main(String[] args) {
        int sum = add(3, 4);
        printResult(sum);

        String msg = greet("Alice", "Hello");
        System.out.println(msg);
    }
}

Methodenüberladung

Methodenüberladung erlaubt mehrere Methoden mit demselben Namen, aber unterschiedlichen Parameterlisten (Typ, Anzahl oder Reihenfolge). Java löst Überladungen zur Kompilierzeit basierend auf Argumenttypen auf. Überladung ist häufig für Konstruktoren und Utility-Methoden. Es ist unterschiedlich zu Overriding (das Vererbung und Runtime-Dispatch beinhaltet).

java
public class MathUtils {
    // Overloaded methods (same name, different params)
    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }

    public static int add(int a, int b, int c) {
        return a + b + c;
    }

    public static String add(String a, String b) {
        return a + b;
    }
}

// Java picks the most specific match
MathUtils.add(1, 2);        // int version -> 3
MathUtils.add(1.5, 2.5);    // double version -> 4.0
MathUtils.add(1, 2, 3);     // 3-param version -> 6
MathUtils.add("Hello", "!"); // String version -> "Hello!"

Varargs & Pass-by-Value

Varargs (Type... name) erlauben variable Argumente, als Array empfangen. Java ist immer Pass-by-Value: Primitives werden kopiert, Objektreferenzen werden kopiert (zeigen aber auf dasselbe Objekt). Das Modifizieren eines Parameters innerhalb einer Methode beeinflusst also nicht die Variable des Aufrufers, aber das Modifizieren des Objekts, auf das er zeigt, schon.

java
// Varargs: variable number of arguments
public static int sum(int... nums) {
    int total = 0;
    for (int n : nums) {
        total += n;
    }
    return total;
}

sum(1, 2, 3);           // 6
sum(1, 2, 3, 4, 5);     // 15
sum();                  // 0 (empty array)
int[] arr = {1, 2, 3};
sum(arr);               // 6 (pass array to varargs)

// Java is ALWAYS pass-by-value
public static void modify(int x) {
    x = 100; // doesn't affect the caller's variable
}

int n = 5;
modify(n);
System.out.println(n); // still 5

// For objects, the reference is passed by value
public static void addItem(List<String> list) {
    list.add("new"); // modifies the same list object
}

Rekursion

Rekursion liegt vor, wenn eine Methode sich selbst aufruft. Immer einen Base Case zum Abbrechen haben. Java optimiert keine Endrekursion (im Gegensatz zu einigen Sprachen), daher kann tiefe Rekursion StackOverflowError verursachen. Für Performance-kritische oder tiefe Rekursion in Iteration umwandeln. Memoisierung (Caching von Ergebnissen) kann rekursive Lösungen wie Fibonacci beschleunigen.

java
// Factorial
public static int factorial(int n) {
    if (n <= 1) return 1;        // base case
    return n * factorial(n - 1); // recursive case
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120

// Fibonacci
public static int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

// Tail recursion (Java doesn't optimize this)
public static int factorialTail(int n, int acc) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}
// Call: factorialTail(5, 1)

// Be careful: deep recursion causes StackOverflowError
// For deep recursion, use iteration or a loop instead

Lambda-Ausdrücke

Lambdas (Java 8+) sind anonyme Funktionen. Der Typ ist ein funktionales Interface (eine abstrakte Methode). Häufige: Function<T,R> (Input→Output), Predicate<T> (Boolean-Test), Consumer<T> (konsumieren, kein Return), Supplier<T> (produzieren, kein Input). Methodenreferenzen (String::length) sind eine Abkürzung für Lambdas, die eine einzelne Methode aufrufen.

java
import java.util.function.*;

// Lambda syntax: (params) -> expression
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// With type annotations
BinaryOperator<Integer> multiply = (Integer a, Integer b) -> a * b;

// Multi-line lambda
Function<String, String> process = s -> {
    String upper = s.toUpperCase();
    return upper.substring(0, 3);
};

// Predicate (boolean test)
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty; // method reference

// Consumer (no return)
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // method reference

// Supplier (no input, produces value)
Supplier<Double> random = () -> Math.random();

// Usage
int result = square.apply(5); // 25
boolean even = isEven.test(4); // true
printer.accept("Hello"); // prints "Hello"
05

Klassen & OOP

Klasse & Konstruktor

Klassen sind Vorlagen für Objekte. Fields halten Zustand, Methoden definieren Verhalten. Konstruktoren initialisieren neue Objekte ('this' verwenden, um Fields von Parametern zu unterscheiden). @Override zeigt an, dass eine Methode eine Superclass-Methode überschreibt (toString stammt von Object). Kapselung: private Fields, public Getter/Setter.

java
public class Person {
    // Fields (instance variables)
    private String name;
    private int age;

    // Constructor
    public Person(String name, int age) {
        this.name = name;  // 'this' refers to the current instance
        this.age = age;
    }

    // Methods
    public String getName() { return name; }
    public int getAge() { return age; }

    public void setAge(int age) {
        if (age >= 0) this.age = age;
    }

    public String greet() {
        return "Hi, I'm " + name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

// Usage
Person p = new Person("Alice", 30);
System.out.println(p.getName());  // "Alice"
System.out.println(p);            // uses toString()

Zugriffsmodifikatoren & Static

Zugriffsmodifikatoren: public (überall), private (nur Klasse), protected (Klasse + Subclasses + Package), default/package-private (gleiches Package). Statische Member gehören zur Klasse, nicht zu Instanzen — über alle Objekte geteilt. Statische Initialisierer laufen einmal beim Laden der Klasse. Static für Konstanten (static final), Utility-Methoden und Zähler verwenden.

java
public class BankAccount {
    // Access modifiers:
    public String owner;      // accessible everywhere
    private double balance;   // class only
    protected String type;    // class + subclasses + same package
    String id;                // package-private (default)

    // Static field (shared by all instances)
    private static int accountCount = 0;

    // Static constant
    public static final double MIN_BALANCE = 100.0;

    // Static method (call without instance)
    public static int getAccountCount() {
        return accountCount;
    }

    // Static initializer (runs once when class loads)
    static {
        System.out.println("BankAccount class loaded");
    }

    public BankAccount(String owner) {
        this.owner = owner;
        this.balance = MIN_BALANCE;
        accountCount++; // increment shared counter
    }
}

int count = BankAccount.getAccountCount(); // static method call

Vererbung & super

Java verwendet 'extends' für Klassenvererbung (nur einfache Vererbung). super() ruft den Eltern-Konstruktor auf (muss erste Zeile sein). @Override zeigt Methoden-Overriding an (Runtime-Polymorphismus). Ein Dog IS-A Animal. Vererbung für 'is-a'-Beziehungen verwenden; Komposition (has-a) für Code-Wiederverwendung. Java 17+ unterstützt Sealed Classes zur Einschränkung der Vererbung.

java
// Parent class
class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor");
    }

    public void eat() {
        System.out.println(name + " is eating");
    }
}

// Child class (extends)
class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // must be first line — call parent constructor
        this.breed = breed;
    }

    // Override parent method
    @Override
    public void eat() {
        super.eat(); // call parent's eat()
        System.out.println(name + " the " + breed + " eats dog food");
    }

    public void bark() {
        System.out.println("Woof!");
    }
}

Dog dog = new Dog("Rex", "Labrador");
dog.eat();   // calls Dog's eat()
dog.bark();  // Dog-specific method

Abstrakte Klassen & Interfaces

Abstrakte Klassen können nicht instanziiert werden und können sowohl abstrakte (ohne Body) als auch konkrete Methoden haben. Interfaces definieren Verträge — alle Methoden sind standardmäßig public abstract. Java 8+ erlaubt Default-Methoden (mit Body) und statische Methoden in Interfaces. Eine Klasse erweitert eine abstrakte Klasse, kann aber mehrere Interfaces implementieren. Abstrakte Klassen für geteilten Code, Interfaces für Verträge verwenden.

java
// Abstract class (can't be instantiated)
abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    // Abstract method (must be implemented by subclasses)
    public abstract double area();

    // Concrete method (inherited)
    public String describe() {
        return color + " " + this.getClass().getSimpleName();
    }
}

// Interface (pure contract, Java 8+ can have default methods)
interface Drawable {
    void draw(); // abstract by default

    // Default method (Java 8+)
    default void drawTwice() {
        draw();
        draw();
    }

    // Static method in interface
    static Drawable empty() {
        return () -> System.out.println("nothing");
    }
}

// A class can extend one class and implement multiple interfaces
class Circle extends Shape implements Drawable {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing " + describe());
    }
}

Polymorphismus & Casting

Polymorphismus: eine Eltern-Referenz kann ein Kind-Objekt halten. Methodenaufrufe dispatchen zur Implementierung des tatsächlichen Objekts (Runtime-Polymorphismus). instanceof vor Downcasting verwenden, um ClassCastException zu vermeiden. Java 16+ Pattern Matching (instanceof Circle c) kombiniert Check und Cast. equals() und hashCode() zusammen überschreiben für korrektes Verhalten in Collections.

java
// Polymorphism: one interface, many forms
Shape s1 = new Circle("red", 5);
Shape s2 = new Square("blue", 3);

// Calls the overridden method (runtime dispatch)
System.out.println(s1.area()); // Circle's area
System.out.println(s2.area()); // Square's area

// instanceof check
if (s1 instanceof Circle) {
    Circle c = (Circle) s1; // downcast
    System.out.println("Radius: " + c.radius);
}

// Pattern matching (Java 16+)
if (s1 instanceof Circle c) {
    System.out.println("Radius: " + c.radius); // c is already cast
}

// Upcasting (automatic)
Circle circle = new Circle("green", 2);
Shape shape = circle; // upcast (no explicit cast needed)

// Object class methods (all classes inherit from Object)
circle.equals(circle);   // reference equality by default
circle.hashCode();       // hash code
circle.getClass();       // Class<Circle>
circle.toString();       // string representation

Records & Enums

Records (Java 16+) sind unveränderliche Datenklassen — der Compiler generiert Konstruktor, Getter, equals, hashCode und toString. Für DTOs und Value Objects verwenden. Enums sind typsichere Konstanten, die Fields, Methoden und Konstruktoren haben können. Enums implementieren Comparable und haben values()- und valueOf()-Methoden. Beide sind essenziell für modernes Java.

java
// Record (Java 16+): concise data class
public record Point(int x, int y) {}

// Equivalent to a class with:
// - final fields x, y
// - constructor
// - getters x(), y()
// - equals, hashCode, toString

Point p = new Point(3, 4);
System.out.println(p.x());      // 3
System.out.println(p.y());      // 4
System.out.println(p);          // Point[x=3, y=4]

// Compact constructor (validation)
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

// Enum (named constants)
public enum Direction {
    UP, DOWN, LEFT, RIGHT;

    public Direction opposite() {
        return switch (this) {
            case UP -> DOWN;
            case DOWN -> UP;
            case LEFT -> RIGHT;
            case RIGHT -> LEFT;
        };
    }
}

Direction d = Direction.UP;
Direction opp = d.opposite(); // DOWN
06

Collections & Generics

List (ArrayList & LinkedList)

ArrayList wird durch ein Array unterstützt (schnelles get/set, langsames insert/delete in der Mitte). LinkedList wird durch eine doppelt verkettete Liste unterstützt (schnelles insert/delete an den Enden, langsamer Random-Access). List.of() erstellt unveränderliche Listen. ArrayList für die meisten Fälle verwenden; LinkedList nur für häufige End-Operationen. Beide implementieren das List-Interface.

java
import java.util.*;

// ArrayList (fast random access, slow insert/delete in middle)
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add(0, "Carol");     // insert at index
list.set(1, "Dave");      // replace at index
String name = list.get(0); // "Carol"
list.remove(0);            // remove by index
list.remove("Dave");       // remove by value
int size = list.size();    // 1
boolean has = list.contains("Bob");

// LinkedList (fast insert/delete at ends)
LinkedList<Integer> linked = new LinkedList<>();
linked.addFirst(1);
linked.addLast(2);
linked.removeFirst();
linked.peek(); // see first element

// Immutable list (Java 9+)
List<String> immutable = List.of("a", "b", "c");
// immutable.add("d"); // UnsupportedOperationException

// Iterate
for (String s : list) {
    System.out.println(s);
}
list.forEach(System.out::println); // method reference

Set (HashSet & TreeSet)

Set speichert eindeutige Elemente. HashSet ist am schnellsten, aber ungeordnet. TreeSet hält Elemente sortiert (natürliche Reihenfolge oder Comparator). LinkedHashSet behält die Einfügereihenfolge. Set-Operationen: addAll (Union), retainAll (Schnittmenge), removeAll (Differenz). Für benutzerdefinierte Objekte in einem HashSet equals() und hashCode() überschreiben.

java
import java.util.*;

// HashSet (fast, unordered)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // duplicate ignored
System.out.println(set.size()); // 2
System.out.println(set.contains("apple")); // true
set.remove("banana");

// TreeSet (sorted, slower)
Set<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.add(2);
System.out.println(sorted); // [1, 2, 3]

// LinkedHashSet (maintains insertion order)
Set<String> ordered = new LinkedHashSet<>();
ordered.add("c");
ordered.add("a");
ordered.add("b");
System.out.println(ordered); // [c, a, b]

// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(2, 3, 4));
a.addAll(b);    // union: [1, 2, 3, 4]
a.retainAll(b); // intersection: [2, 3]
a.removeAll(b); // difference: [1]

// Immutable set
Set<String> immutable = Set.of("x", "y", "z");

Map (HashMap & TreeMap)

Map speichert Schlüssel-Wert-Paare. HashMap ist am schnellsten (ungeordnet). TreeMap sortiert nach Schlüsseln. LinkedHashMap behält die Einfügereihenfolge. getOrDefault vermeidet null-Checks. compute/merge sind mächtig zum Aktualisieren von Werten. Für benutzerdefinierte Schlüssel equals() und hashCode() überschreiben. Map.of() erstellt unveränderliche Maps (Java 9+).

java
import java.util.*;

// HashMap (fast, unordered)
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // overwrite

// Access
int age = ages.get("Alice"); // 31
int defaultAge = ages.getOrDefault("Eve", 0); // 0

// Check
boolean has = ages.containsKey("Alice");
boolean hasVal = ages.containsValue(25);

// Remove
ages.remove("Bob");

// Iterate
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

ages.forEach((key, val) -> System.out.println(key + "=" + val));

// Compute (Java 8+)
ages.compute("Alice", (k, v) -> v + 1); // increment
ages.putIfAbsent("Carol", 28);
ages.merge("Alice", 1, Integer::sum); // add 1

// TreeMap (sorted by keys)
Map<String, Integer> sorted = new TreeMap<>();
// LinkedHashMap (maintains insertion order)
Map<String, Integer> ordered = new LinkedHashMap<>();

Queue & Deque

Queue ist FIFO (first-in-first-out). Deque ist doppelseitig (kann an beiden Enden hinzufügen/entfernen). PriorityQueue ordnet Elemente nach natürlicher Reihenfolge oder einem Comparator (standardmäßig Min-Heap). Für einen Stack ArrayDeque (push/pop) anstelle der veralteten Stack-Klasse verwenden. ArrayDeque ist schneller als LinkedList für Queue/Deque-Operationen.

java
import java.util.*;

// Queue (FIFO)
Queue<String> queue = new LinkedList<>();
queue.add("first");   // throws if full (capacity-restricted)
queue.offer("second"); // returns false if full
String head = queue.peek(); // see head (null if empty)
String removed = queue.poll(); // remove and return head

// Deque (double-ended)
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.peekFirst(); // 1
deque.peekLast();  // 2
deque.pollFirst(); // 1
deque.pollLast();  // 2

// PriorityQueue (min-heap by default)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3);
pq.add(1);
pq.add(2);
System.out.println(pq.poll()); // 1 (smallest first)

// Max-heap (reverse order)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(1);
maxHeap.add(3);
System.out.println(maxHeap.poll()); // 3 (largest first)

// Stack (legacy, prefer Deque)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // add to front
stack.push(2);
stack.pop();   // 2 (remove from front)

Generics

Generics ermöglichen typsichere Collections und Klassen. <T> ist ein Typparameter. Beschränkte Typen (<T extends Comparable<T>>) beschränken auf Typen mit bestimmtem Verhalten. Wildcards: ? (beliebig), ? extends T (kovariant, nur Lesen), ? super T (kontravariant, nur Schreiben). Generics verwenden Type Erasure — Typen werden zur Kompilierzeit geprüft, zur Laufzeit gelöscht.

java
// Generic class
public class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(42);

// Generic method
public static <T> T firstOf(List<T> list) {
    return list.get(0);
}

String first = firstOf(List.of("a", "b"));

// Bounded type parameter
public static <T extends Comparable<T>> T max(List<T> list) {
    T result = list.get(0);
    for (T item : list) {
        if (item.compareTo(result) > 0) {
            result = item;
        }
    }
    return result;
}

// Wildcards
void process(List<?> list) { }          // any type
void processNums(List<? extends Number> list) { } // Number or subclass
void addNums(List<? super Integer> list) { }      // Integer or superclass

Iteratoren & Comparable

Iterator erlaubt sicheres Entfernen während der Iteration (it.remove()). ListIterator fügt bidirektionale Traversierung und set/add hinzu. Comparable definiert natürliche Reihenfolge (compareTo). Comparator definiert benutzerdefinierte Reihenfolge (comparing, comparingInt, reversed, thenComparing). Comparator.comparing() für fließendes Sortieren verwenden. Collections.sort() verwendet die natürliche Reihenfolge.

java
import java.util.*;

// Iterator
List<String> list = List.of("a", "b", "c");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
    // it.remove(); // safe removal during iteration
}

// ListIterator (bidirectional)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    lit.set(lit.next().toUpperCase()); // replace
}

// Comparable (natural ordering)
public class Person implements Comparable<Person> {
    String name;
    int age;

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

// Comparator (custom ordering)
Comparator<Person> byName = Comparator.comparing(p -> p.name);
Comparator<Person> byAgeDesc = Comparator.comparingInt((Person p) -> p.age).reversed();

List<Person> people = new ArrayList<>();
people.sort(byName);
people.sort(byAgeDesc);
Collections.sort(people); // uses Comparable
07

Streams & Funktional

Stream-Grundlagen

Streams (Java 8+) bieten deklarative Datenverarbeitung. Erstellen mit .stream() (Collections) oder Stream.of(). Zwischenoperationen (filter, map, sorted) sind lazy — sie führen nur aus, wenn eine Terminal-Operation (collect, reduce, count, forEach) aufgerufen wird. toList() (Java 16+) ist eine prägnante Alternative zu collect(Collectors.toList()).

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

// Filter and collect
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // [2, 4, 6]

// Map (transform)
List<String> doubled = nums.stream()
    .map(n -> "num" + n)
    .collect(Collectors.toList());

// Reduce
int sum = nums.stream().reduce(0, Integer::sum); // 21
int product = nums.stream().reduce(1, (a, b) -> a * b);

// Count
long count = nums.stream().filter(n -> n > 3).count(); // 3

// Find
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
boolean anyMatch = nums.stream().anyMatch(n -> n > 5);
boolean allMatch = nums.stream().allMatch(n -> n > 0);

// ForEach
nums.stream().forEach(System.out::println);

// toList() shortcut (Java 16+)
List<Integer> result = nums.stream().filter(n -> n > 3).toList();

Stream-Operationen

sorted() ordnet Elemente (natürlich oder mit Comparator). distinct() entfernt Duplikate. limit(n)/skip(n) paginieren. flatMap flattet verschachtelte Streams — essenziell für Eins-zu-viel-Transformationen. peek() ist zum Debuggen (Side Effects). groupingBy erstellt Maps, die Elemente nach einem Klassifizierer gruppieren. Streams sind lazy — Operationen verketten effizient.

java
List<String> names = List.of("Alice", "Bob", "Charlie", "David");

// Sorted
List<String> sorted = names.stream()
    .sorted()
    .toList();

// Sorted by length
List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .toList();

// Distinct
List<Integer> distinct = List.of(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .toList(); // [1, 2, 3]

// Limit and Skip
List<Integer> limited = nums.stream()
    .skip(2)   // skip first 2
    .limit(3)  // take next 3
    .toList();

// FlatMap (flatten nested structures)
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList(); // [1, 2, 3, 4]

// Peek (debug, side-effect)
nums.stream()
    .peek(n -> System.out.println("before: " + n))
    .filter(n -> n > 2)
    .peek(n -> System.out.println("after: " + n))
    .toList();

// Grouping
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

Collectors & Reduktion

Collectors bieten reichhaltige Reduktionsoperationen: joining (Strings verketten), groupingBy (nach Schlüssel gruppieren), partitioningBy (nach Boolean aufteilen), toMap (Map erstellen), summarizingInt (Statistiken: count, sum, min, max, average). Collectors können zusammengesetzt werden (groupingBy mit Downstream-Collector). Diese ersetzen umständliche Schleifen durch deklarative Einzeiler.

java
import java.util.stream.*;

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
);

// Join strings
String joined = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"

// Group by
Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

// Partition (boolean)
Map<Boolean, List<Person>> partition = people.stream()
    .collect(Collectors.partitioningBy(p -> p.getAge() > 28));

// Count by group
Map<Integer, Long> countByAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));

// Summarizing
IntSummaryStatistics stats = people.stream()
    .collect(Collectors.summarizingInt(Person::getAge));
System.out.println(stats.getAverage()); // 30.0
System.out.println(stats.getMax());     // 35

// To map
Map<String, Integer> nameToAge = people.stream()
    .collect(Collectors.toMap(Person::getName, Person::getAge));

// Reducing
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));

Optional

Optional<T> ist ein Container, der einen Wert enthalten kann oder nicht. Es erzwingt explizite Behandlung von Abwesenheit — keine NullPointerException mehr. of() für nicht-null Werte verwenden, ofNullable() für möglicherweise-null. Mit map/flatMap/filter verketten. Niemals get() ohne isPresent() verwenden — orElse/orElseThrow bevorzugen. Optional ist für Rückgabetypen konzipiert, nicht für Fields.

java
import java.util.Optional;

// Creating Optional
Optional<String> present = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null); // empty if null

// Checking
present.isPresent(); // true
empty.isEmpty();     // true (Java 11+)

// Getting values
String val = present.get(); // throws if empty (avoid!)
String safe = present.orElse("default");
String computed = present.orElseGet(() -> computeDefault());
String orThrow = present.orElseThrow(() -> new RuntimeException("missing"));

// Transform (map/flatMap)
Optional<Integer> length = present.map(String::length); // Optional[5]
Optional<String> upper = present.map(s -> s.toUpperCase());

// Filter
Optional<String> filtered = present.filter(s -> s.length() > 3);

// ifPresent
present.ifPresent(s -> System.out.println(s));
present.ifPresentOrElse(
    s -> System.out.println("Got: " + s),
    () -> System.out.println("Empty")
);

// Chaining (avoid null checks)
String result = getUser(1)
    .map(User::getProfile)
    .map(Profile::getEmail)
    .orElse("no email");

Funktionale Interfaces

Funktionale Interfaces haben genau eine abstrakte Methode (können mehrere Default-Methoden haben). @FunctionalInterface ist optional, dokumentiert aber die Absicht. Java bietet viele in java.util.function: Function, Predicate, Consumer, Supplier, plus Bi- und primitive Varianten. Diese statt eigener Interfaces verwenden, wenn möglich. Sie ermöglichen Lambda-Ausdrücke und Methodenreferenzen.

java
import java.util.function.*;

// Built-in functional interfaces
Function<String, Integer> strToInt = Integer::parseInt;
BiFunction<String, String, String> concat = String::concat;

Predicate<String> isEmpty = String::isEmpty;
BiPredicate<String, String> contains = String::contains;

Consumer<String> printer = System.out::println;
BiConsumer<String, Integer> printPair = (s, i) -> System.out.println(s + ":" + i);

Supplier<List<String>> listFactory = ArrayList::new;

// Primitive specializations
IntFunction<String> intToStr = String::valueOf;
ToIntFunction<String> length = String::length;
IntPredicate isPositive = n -> n > 0;
IntConsumer intPrinter = System.out::println;
IntSupplier randomInt = () -> (int)(Math.random() * 100);

// Binary operators
BinaryOperator<Integer> max = Integer::max;
IntBinaryOperator sum = Integer::sum;

// Unary operators
UnaryOperator<String> trim = String::trim;
IntUnaryOperator negate = n -> -n;

// Custom functional interface
@FunctionalInterface
interface StringProcessor {
    String process(String input);

    // Can have default methods
    default StringProcessor andThen(StringProcessor after) {
        return input -> after.process(process(input));
    }
}
08

Exceptions & I/O

Try / Catch / Finally

try/catch/finally behandelt Exceptions. finally läuft immer (für Cleanup verwenden). Multi-catch (catch A | B) behandelt mehrere Exceptions zusammen. Try-with-resources schließt automatisch jedes AutoCloseable (Dateien, Verbindungen, Streams) — gegenüber manuellem finally-Cleanup bevorzugt. Ressourcen werden in umgekehrter Reihenfolge der Deklaration geschlossen.

java
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Math error: " + e.getMessage());
} catch (Exception e) {
    System.out.println("General error: " + e);
} finally {
    // Always runs (even if return/throw in try/catch)
    System.out.println("Cleanup");
}

// Multi-catch (Java 7+)
try {
    riskyOperation();
} catch (IOException | SQLException e) {
    // Handle both exceptions the same way
    log.error(e);
}

// Try-with-resources (auto-close, Java 7+)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"));
     PrintWriter pw = new PrintWriter("output.txt")) {
    String line = br.readLine();
    pw.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
// br and pw are auto-closed (in reverse order)

Checked vs. Unchecked

Checked Exceptions (extends Exception) müssen mit 'throws' deklariert oder gefangen werden — der Compiler erzwingt dies. Für behebbare Bedingungen verwenden (Datei nicht gefunden, Netzwerkfehler). Unchecked Exceptions (extends RuntimeException) brauchen keine Deklaration — für Programmierfehler verwenden (Null Pointer, ungültiges Argument). Die Debatte: Checked Exceptions erzwingen Behandlung, können aber Code überladen; viele Frameworks bevorzugen Unchecked.

java
// Checked exceptions (must be declared or caught)
public void readFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    // IOException is checked — compiler enforces handling
}

// Unchecked exceptions (RuntimeException, no need to declare)
public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("Divisor cannot be zero");
        // RuntimeException — no 'throws' needed
    }
    return a / b;
}

// Common checked exceptions:
// IOException, SQLException, ClassNotFoundException

// Common unchecked exceptions:
// NullPointerException, IllegalArgumentException,
// IndexOutOfBoundsException, ArithmeticException,
// ClassCastException, IllegalStateException

// Custom checked exception
class DataException extends Exception {
    public DataException(String msg) { super(msg); }
}

// Custom unchecked exception
class ValidationException extends RuntimeException {
    public ValidationException(String msg) { super(msg); }
}

File-I/O (NIO.2)

NIO.2 (java.nio.file) ist die moderne Datei-API. Files.readString/writeString (Java 11+) sind praktisch für Text. Files.lines() gibt einen lazy Stream zurück — effizient für große Dateien (mit try-with-resources schließen). Path.of() ersetzt die alte File-Klasse. Files.createDirectories() erstellt den vollständigen Pfad. IOException immer behandeln.

java
import java.nio.file.*;
import java.io.*;

// Read entire file (small files)
List<String> lines = Files.readAllLines(Path.of("input.txt"));
String content = Files.readString(Path.of("config.json")); // Java 11+
byte[] bytes = Files.readAllBytes(Path.of("image.png"));

// Write file
Files.writeString(Path.of("output.txt"), "Hello, World!");
Files.write(Path.of("data.bin"), bytes);

// Append
Files.writeString(Path.of("log.txt"), "entry\n",
    StandardOpenOption.APPEND, StandardOpenOption.CREATE);

// Stream lines (large files, lazy)
try (Stream<String> lineStream = Files.lines(Path.of("large.txt"))) {
    lineStream.filter(l -> l.contains("ERROR"))
              .forEach(System.out::println);
}

// Copy, move, delete
Files.copy(Path.of("src.txt"), Path.of("dest.txt"));
Files.move(Path.of("old.txt"), Path.of("new.txt"));
Files.delete(Path.of("temp.txt"));

// Check existence
boolean exists = Files.exists(Path.of("file.txt"));

// Create directories
Files.createDirectories(Path.of("a/b/c"));

Reader & Writer (Text)

BufferedReader/Writer sind effizient für Text-I/O (Pufferung reduziert System-Calls). PrintWriter bietet printf-artige Formatierung. Scanner parst Eingaben (nextInt, nextDouble, nextLine). InputStreamReader überbrückt Byte-Streams zu Character-Streams (Charset für Nicht-UTF-8 angeben). Immer try-with-resources verwenden, um sicherzustellen, dass Streams geschlossen werden.

java
import java.io.*;
import java.nio.file.*;

// BufferedReader (efficient text reading)
try (BufferedReader br = Files.newBufferedReader(Path.of("input.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// BufferedWriter (efficient text writing)
try (BufferedWriter bw = Files.newBufferedWriter(Path.of("output.txt"))) {
    bw.write("First line");
    bw.newLine();
    bw.write("Second line");
}

// PrintWriter (convenient formatting)
try (PrintWriter pw = new PrintWriter("output.txt")) {
    pw.println("Hello");
    pw.printf("Name: %s, Age: %d%n", "Alice", 30);
}

// Scanner (parsing input)
try (Scanner sc = new Scanner(Path.of("data.txt"))) {
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // Parse tokens
        Scanner lineSc = new Scanner(line);
        if (lineSc.hasNextInt()) {
            int n = lineSc.nextInt();
        }
    }
}

// InputStreamReader (bytes to chars, e.g., from InputStream)
Reader reader = new InputStreamReader(System.in);

Date & Time (java.time)

java.time (Java 8+) ist die moderne Datum/Uhrzeit-API, die das alte Date/Calendar ersetzt. LocalDate (nur Datum), LocalTime (nur Uhrzeit), LocalDateTime (beides), ZonedDateTime (mit Zeitzone). Alle sind unveränderlich und Thread-sicher. Period für Datumsdifferenzen, Duration für Zeitdifferenzen verwenden. DateTimeFormatter zum Parsen/Formatieren. Instant für maschinelle Zeitstempel (UTC).

java
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;

// Current date/time
LocalDate today = LocalDate.now();      // 2024-01-15
LocalTime now = LocalTime.now();        // 14:30:45.123
LocalDateTime dt = LocalDateTime.now(); // both
ZonedDateTime zdt = ZonedDateTime.now(); // with timezone

// Create specific
LocalDate date = LocalDate.of(2024, 1, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime specific = LocalDateTime.of(2024, 1, 15, 14, 30);

// Parsing and formatting
LocalDate parsed = LocalDate.parse("2024-01-15");
String formatted = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// "15/01/2024"

// Manipulation (immutable, returns new)
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);

// Period (date-based)
Period age = Period.between(LocalDate.of(1990, 1, 1), today);
System.out.println(age.getYears()); // 34

// Duration (time-based)
Duration dur = Duration.between(time, LocalTime.now());
System.out.println(dur.toMinutes());

// Instant (machine time, UTC)
Instant instant = Instant.now();
Instant epoch = Instant.ofEpochSecond(0);

Concurrency-Grundlagen

Java-Concurrency: Thread (Low-Level), ExecutorService (Thread-Pools — bevorzugt), CompletableFuture (asynchrone Komposition, wie Promises). parallelStream() verwendet den ForkJoinPool für parallele Verarbeitung. synchronized-Blöcke schützen geteilten Zustand. Atomic-Variablen (AtomicInteger usw.) bieten lock-freie Thread-sichere Operationen. Für komplexe Concurrency java.util.concurrent-Collections verwenden (ConcurrentHashMap, BlockingQueue).

java
import java.util.concurrent.*;

// Create a thread
Thread thread = new Thread(() -> {
    System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
thread.join(); // wait for completion

// ExecutorService (thread pool)
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(1000);
    return 42;
});
Integer result = future.get(); // blocks until done
executor.shutdown();

// CompletableFuture (async, Java 8+)
CompletableFuture<String> cf = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenApply(String::toUpperCase);
String asyncResult = cf.join(); // "HELLO WORLD"

// Parallel stream
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.parallelStream().mapToInt(Integer::intValue).sum();

// Synchronized
synchronized (this) {
    // only one thread at a time
}

// Atomic variables
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
09

Lambda-Ausdrücke

Lambda-Syntax-Grundlagen

Lambdas (Java 8+) sind prägnante Implementierungen funktionaler Interfaces. Syntax: (params) -> expression oder (params) -> { statements; }. Der Compiler leitet Parametertypen vom Zieltyp ab. Ein-Parameter-Lambdas können Klammern weglassen; Null-Parameter brauchen leere Klammern. Lambdas ermöglichen funktionale Programmierung und sind das Rückgrat der Streams-API.

java
// Anonymous class (verbose, pre-Java 8)
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Old way");
    }
};

// Lambda expression (Java 8+)
Runnable r2 = () -> System.out.println("Lambda");

// With parameters and body
Comparator<Integer> cmp = (a, b) -> {
    int diff = a - b;
    return diff;
};

// Type inference (omit types)
Comparator<Integer> cmp2 = (a, b) -> a - b;

// Single param, no parentheses needed
Consumer<String> printer = s -> System.out.println(s);

// Zero params need empty parens
Runnable noop = () -> {};

Funktionale Interfaces

Ein funktionales Interface hat genau eine abstrakte Methode (SAM-Typ). Die @FunctionalInterface-Annotation lässt den Compiler dies erzwingen. Lambdas können nur funktionale Interfaces targeten. Default- und statische Methoden sind erlaubt und brechen nicht die Single-Method-Regel. Dies ist die Grundlage, die Lambdas in Javas Typsystem ermöglicht.

java
// Functional interface: exactly one abstract method
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}

// Implement with lambda
MathOperation add = (a, b) -> a + b;
MathOperation mul = (a, b) -> a * b;
MathOperation max = (a, b) -> Math.max(a, b);

int result = add.operate(3, 4);  // 7
int m = mul.operate(3, 4);       // 12

// @FunctionalInterface is optional but recommended
// It prevents accidentally adding a second abstract method
// Default and static methods don't count toward the limit
interface StringProcessor {
    String process(String s);
    default StringProcessor andThen(StringProcessor next) {
        return s -> next.process(this.process(s));
    }
}

Eingebaute funktionale Interfaces

java.util.function bietet ~40 fertige funktionale Interfaces, sodass man selten eigene schreibt. Die vier Kern-Stücke: Function (transformieren), Predicate (testen), Consumer (konsumieren), Supplier (produzieren). Bi-Varianten nehmen zwei Argumente. Primitive Varianten (IntFunction, ToIntFunction usw.) vermeiden Autoboxing-Overhead. Diese statt eigener Interfaces verwenden.

java
import java.util.function.*;

// Function<T,R>: input -> output
Function<String, Integer> len = String::length;
int n = len.apply("hello");  // 5

// Predicate<T>: input -> boolean (for filtering)
Predicate<String> isEmpty = String::isEmpty;
boolean e = isEmpty.test("");  // true

// Consumer<T>: input -> void (side effects)
Consumer<String> print = System.out::println;
print.accept("hi");

// Supplier<T>: no input -> output (factories, lazy)
Supplier<Double> random = Math::random;
double r = random.get();

// BiFunction<T,U,R>: two inputs -> output
BiFunction<String, Integer, String> repeat =
    (s, i) -> s.repeat(i);

// Primitives variants avoid boxing
IntFunction<String> f = i -> "n=" + i;
IntPredicate positive = i -> i > 0;
ToIntFunction<String> length = String::length;
IntBinaryOperator sum = (a, b) -> a + b;

Methodenreferenzen

Methodenreferenzen (::) sind eine Abkürzung für Lambdas, die nur eine einzelne Methode aufrufen. Vier Arten: static (Class::static), bound instance (obj::method), unbound instance (Class::method — erster Parameter wird zum Receiver), und Konstruktor (Class::new). Verwenden, wenn ein Lambda nur an eine Methode weiterleitet — sie sind lesbarer. Sonst bei expliziten Lambdas bleiben.

java
import java.util.*;

List<String> names = List.of("alice", "bob", "charlie");

// Lambda form
names.forEach(s -> System.out.println(s));
// Method reference (shorthand)
names.forEach(System.out::println);

// Four kinds of method references:

// 1. Static method: ClassName::staticMethod
names.stream().map(String::toUpperCase);

// 2. Instance method of particular object: instance::method
var printer = System.out;
names.forEach(printer::println);

// 3. Instance method of arbitrary object: ClassName::instanceMethod
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

// 4. Constructor: ClassName::new
Supplier<ArrayList<String>> factory = ArrayList::new;
ArrayList<String> list = factory.get();

Variablen erfassen (Effectively Final)

Lambdas können lokale Variablen erfassen, aber sie müssen final oder 'effectively final' (nie neu zugewiesen) sein. Dies liegt daran, dass Lambdas den Stack-Frame überleben können. Um das zu umgehen, ein einzelnes Element-Array oder ein AtomicInteger/Holder-Objekt verwenden. Instanz- und statische Fields haben keine solche Einschränkung. 'this' innerhalb eines Lambdas verweist auf die umschließende Klasseninstanz, nicht auf das Lambda selbst.

java
import java.util.function.*;

int x = 10;
// Capturing a local variable — must be final or effectively final
Supplier<Integer> getter = () -> x * 2;
System.out.println(getter.get());  // 20

// x = 20;  // ERROR: would break the lambda capture
// Local variables captured by lambdas must be final/effectively final

// Workaround: use an array or wrapper (mutable container)
int[] counter = {0};
Runnable inc = () -> counter[0]++;
inc.run();
inc.run();
System.out.println(counter[0]);  // 2

// Instance/static fields CAN be modified (no restriction)
class Holder {
    int value = 0;
    Runnable bump = () -> value++;  // OK, field access
}

// 'this' inside a lambda refers to the enclosing instance
class Outer {
    String name = "Outer";
    Runnable r = () -> System.out.println(this.name);  // "Outer"
}

Konstruktorreferenzen

Konstruktorreferenzen (ClassName::new) erstellen prägnant neue Instanzen. Nützlich mit Collectors.toCollection() zur Wahl des Ergebnis-Typs, bei Array-Erstellung (Type[]::new) und in Factory-Patterns. Für Records und unveränderliche Objekte sind Konstruktorreferenzen der idiomatische Weg, Kopien zu erstellen. Sie paaren natürlich mit Function/Supplier-Zielen.

java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

// Supplier constructor reference
Supplier<StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();

// Function constructor reference (with one arg)
Function<String, StringBuilder> sbFromString = StringBuilder::new;
StringBuilder named = sbFromString.apply("Hello");

// In streams: collect into a specific collection
List<String> names = List.of("a", "b", "c");
ArrayList<String> copy = names.stream()
    .collect(Collectors.toCollection(ArrayList::new));

// Array constructor reference
IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(5);  // new String[5]

// Copying via constructor
record Point(int x, int y) {}
Function<Point, Point> copyCtor = Point::new;
Point p = copyCtor.apply(new Point(1, 2));
10

Optional & Null-Sicherheit

Optional erstellen

Optional ist ein Container, der einen Wert halten kann oder nicht. empty() für keinen Wert verwenden, of() wenn der Wert sicher nicht-null ist (wirft sonst NPE), und ofNullable() wenn null möglich ist. Optional zwingt Aufrufer, den Abwesenheits-Fall explizit zu behandeln. Niemals null zurückgeben, wo Optional erwartet wird — das zunichtemachen den Zweck.

java
import java.util.Optional;

// empty() — no value
Optional<String> empty = Optional.empty();

// of() — value must be non-null (throws NPE if null)
Optional<String> present = Optional.of("hello");

// ofNullable() — accepts null safely
Optional<String> maybe = Optional.ofNullable(getName());

// From a stream that may produce 0 or 1 elements
Optional<Integer> first = List.of(1, 2, 3).stream().findFirst();

// Common helper pattern
public Optional<User> findUser(long id) {
    User u = db.lookup(id);
    return Optional.ofNullable(u);
}

String getName() { return Math.random() > 0.5 ? "Alice" : null; }
class User {}
class Db { User lookup(long id) { return null; } }
Db db = new Db();

Werte sicher konsumieren

ifPresent/ifPresentOrElse gegenüber isPresent+get bevorzugen. orElse gibt einen konstanten Default zurück; orElseGet nimmt einen Supplier, sodass der Default lazy berechnet wird (wichtig, wenn der Default teuer ist). orElseThrow konvertiert Abwesenheit zu einer Exception. Das Ziel ist, niemals blind .get() aufzurufen — das führt das NPE-Risiko wieder ein, das Optional beseitigen sollte.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// isPresent / isEmpty (Java 11+)
if (name.isPresent()) {
    System.out.println(name.get());  // "Alice"
}
// Avoid .get() without checking — throws NoSuchElementException

// ifPresent: run action only if value exists
name.ifPresent(System.out::println);

// ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("No name")
);

// orElse: provide default
String s1 = name.orElse("Anonymous");

// orElseGet: lazy default (computed only if needed)
String s2 = name.orElseGet(() -> expensiveDefault());

// orElseThrow: throw if absent
String s3 = name.orElseThrow(() -> new IllegalStateException("missing"));

String expensiveDefault() { return "computed"; }

Transformieren mit map & flatMap

map transformiert den enthaltenen Wert (Optional<T> -> Optional<R>). flatMap wird verwendet, wenn die Mapping-Funktion selbst ein Optional zurückgibt, was verschachtelte Optionals verhindert. filter behält den Wert nur, wenn ein Predicate passt. Das Verketten von map/filter/flatMap ermöglicht Pipelines, die beim ersten empty kurzschließen — viel sauberer als verschachtelte null-Checks.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// map: transform the value if present
Optional<Integer> length = name.map(String::length);  // Optional[5]
Optional<String> upper = name.map(String::toUpperCase);

// flatMap: when the transform itself returns Optional
// (avoids Optional<Optional<T>>)
public Optional<String> findEmail(long id) {
    return Optional.ofNullable(db.get(id));
}
Optional<String> email = Optional.of(1L)
    .flatMap(this::findEmail);  // Optional<email> not Optional<Optional<email>>

// filter: keep only if predicate matches
Optional<Integer> adultAge = Optional.of(25)
    .filter(a -> a >= 18);  // Optional[25]
Optional<Integer> kid = Optional.of(10)
    .filter(a -> a >= 18);  // Optional.empty

// Chain transformations
String label = Optional.of("alice")
    .map(String::strip)
    .filter(s -> !s.isEmpty())
    .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
    .orElse("unknown");  // "Alice"

class Db { String get(long id) { return "[email protected]"; } }
Db db = new Db();
Optional<String> findEmail(long id) { return Optional.ofNullable(db.get(id)); }

Zu vermeidende Anti-Patterns

Optional ist für Rückgabetypen konzipiert, nicht für Fields oder Parameter. Es ist nicht Serializable und bringt als Field Overhead. .get() nicht ohne Check verwenden, und isPresent()+get() nicht verwenden — das ist nur umständliches null-Checking. Collections drücken bereits Leere aus, also nicht in Optional wickeln. Optional als Rückgabetyp-Signal verwenden, dass ein Wert abwesend sein kann.

java
import java.util.Optional;

// BAD: using Optional for fields (not serializable, wastes memory)
class Bad {
    private Optional<String> name;  // DON'T
}

// GOOD: use plain field, return Optional from accessor
class Good {
    private String name;
    public Optional<String> getName() { return Optional.ofNullable(name); }
}

// BAD: Optional as method parameter (clutters API)
public void process(Optional<String> input) {}  // DON'T

// GOOD: method overloading or nullable param
public void process(String input) {}
public void process() { process(null); }

// BAD: .get() without check
String x = findName().get();  // throws if empty

// BAD: .isPresent() + .get() — defeats the purpose
Optional<String> opt = findName();
if (opt.isPresent()) {
    use(opt.get());  // just use ifPresent or map instead
}

// BAD: returning Optional from collections
public Optional<Item> find(...) {
    // Collections already express emptiness — return empty List, not Optional<List>
    return Optional.ofNullable(items);
}

Optional mit Streams

Optional.stream() (Java 9+) liefert einen Stream von 0 oder 1 Element, was es ermöglicht, Optionals elegant aus einem Stream herauszuflatten. Dies ist der sauberste Weg, abwesende Werte während der Stream-Verarbeitung zu überspringen. Es vermeidet das umständliche filter(isPresent).map(get)-Pattern und hält die Pipeline deklarativ.

java
import java.util.*;
import java.util.stream.*;

// stream() on Optional: 0 or 1 element stream
Optional<String> opt = Optional.of("hi");
opt.stream().forEach(System.out::println);

// Useful: flatMap Optional out of a stream
class User {
    String email;  // may be null
    User(String e) { email = e; }
    Optional<String> getEmail() { return Optional.ofNullable(email); }
}

List<User> users = List.of(
    new User("[email protected]"),
    new User(null),
    new User("[email protected]")
);

// Extract emails, skipping nulls — clean with Optional::stream
List<String> emails = users.stream()
    .flatMap(u -> u.getEmail().stream())
    .toList();  // [[email protected], [email protected]]

// Without Optional::stream you'd need filter+map
List<String> emails2 = users.stream()
    .map(User::getEmail)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .toList();
11

Streams API Deep Dive

Streams erstellen

Streams können aus Collections, Arrays oder statischen Factories erstellt werden. iterate() und generate() produzieren unendliche Streams — immer mit limit() folgen. iterate mit einem Predicate (Java 9+) ist sicherer als bloßes iterate. IntStream/LongStream/DoubleStream vermeiden Boxing für numerische Arbeit. Streams sind einmalig: Sobald eine Terminal-Operation läuft, ist der Stream konsumiert.

java
import java.util.*;
import java.util.stream.*;

// From collections
Stream<String> s1 = List.of("a", "b").stream();
Stream<String> s2 = Set.of("x").stream();

// From arrays
int[] nums = {1, 2, 3};
IntStream s3 = Arrays.stream(nums);
Stream<String> s4 = Arrays.stream(new String[]{"a", "b"});

// Static factory methods
Stream<Integer> s5 = Stream.of(1, 2, 3);
Stream<Integer> s6 = Stream.empty();
Stream<Integer> s7 = Stream.iterate(1, n -> n * 2);  // infinite
Stream<Integer> s8 = Stream.iterate(1, n -> n < 100, n -> n + 1);  // bounded (Java 9+)
Stream<Double> s9 = Stream.generate(Math::random);  // infinite
Stream<String> s10 = Stream.ofNullable(null);  // 0 or 1 element (Java 9+)

// From functions (infinite, must limit)
List<Integer> powers = Stream.iterate(1, n -> n * 2)
    .limit(10)
    .toList();

// Numeric ranges
IntStream range = IntStream.range(0, 5);       // 0,1,2,3,4
IntStream closed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

Zwischenoperationen

Zwischenoperationen sind lazy — sie laufen nicht, bis eine Terminal-Operation aufgerufen wird. filter behält Elemente, map transformiert 1:1, flatMap transformiert 1:viele. distinct/sorted/limit/skip sind zustandsbehaftet. takeWhile/dropWhile (Java 9+) stoppen beim ersten nicht-passenden Element (im Gegensatz zu filter, der alles scannt). peek für Debugging verwenden, nicht für Side Effects in Produktion.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);

// filter: keep matching
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0).toList();

// map: transform
List<String> labels = nums.stream()
    .map(n -> "n=" + n).toList();

// flatMap: one-to-many
List<Integer> expanded = List.of(List.of(1, 2), List.of(3))
    .stream().flatMap(List::stream).toList();  // [1,2,3]

// distinct: remove duplicates
List<Integer> uniq = nums.stream().distinct().toList();

// sorted
List<Integer> asc = nums.stream().sorted().toList();
List<Integer> desc = nums.stream().sorted(Comparator.reverseOrder()).toList();

// peek: inspect (mainly for debugging)
nums.stream().peek(n -> System.out.println("seen " + n)).count();

// limit / skip
List<Integer> first3 = nums.stream().limit(3).toList();
List<Integer> after2 = nums.stream().skip(2).toList();

// takeWhile / dropWhile (Java 9+)
List<Integer> lt5 = nums.stream().takeWhile(n -> n < 5).toList();

Collectors: Gruppierung & Partitionierung

Collectors.groupingBy ist das SQL GROUP BY von Java-Streams. Die Klassifizierer-Funktion definiert den Schlüssel; ein optionaler Downstream-Collector verarbeitet jede Gruppe (counting, summing, mapping usw.). partitioningBy ist ein Spezialfall mit einem Boolean-Predicate (genau zwei Buckets). Einen TreeMap-Supplier für sortierte Schlüssel übergeben. Diese komponieren mächtig — man kann mehrstufige Gruppierungen aufbauen.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, String city, int age) {}

List<Person> people = List.of(
    new Person("Alice", "NYC", 30),
    new Person("Bob", "LA", 25),
    new Person("Carol", "NYC", 35),
    new Person("Dave", "LA", 40)
);

// groupingBy: Map<key, List<item>>
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice,Carol], LA=[Bob,Dave]}

// groupingBy with downstream collector
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city, Collectors.counting()));

Map<String, Integer> sumAgeByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.summingInt(Person::age)));

Map<String, List<String>> namesByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.mapping(Person::name, Collectors.toList())));

// partitioningBy: Map<Boolean, List> (2 buckets)
Map<Boolean, List<Person>> byAge = people.stream()
    .collect(Collectors.partitioningBy(p -> p.age() >= 30));

// groupingBy with TreeMap for sorted keys
Map<String, List<Person>> sorted = people.stream()
    .collect(Collectors.groupingBy(Person::city, TreeMap::new, Collectors.toList()));

Reduktion & Statistiken

reduce kombiniert alle Elemente zu einem einzigen Wert — eine Identity für Sicherheit bei leeren Streams angeben. summaryStatistics gibt count/sum/min/max/avg in einem Durchlauf. Collectors.joining ist praktisch zum Erstellen begrenzter Strings. teeing (Java 12+) führt zwei Collectoren parallel aus und führt deren Ergebnisse zusammen — nützlich, wenn man zwei Aggregate (wie min und max) in einem Durchlauf braucht.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5);

// reduce: combine all into one
int sum = nums.stream().reduce(0, Integer::sum);          // 15
Optional<Integer> product = nums.stream().reduce((a, b) -> a * b);  // Optional[120]

// Built-in summary collectors
IntSummaryStatistics stats = nums.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();
// stats.getCount(), getSum(), getMin(), getMax(), getAverage()

// Common terminal collectors
long count = nums.stream().collect(Collectors.counting());
double avg = nums.stream().collect(Collectors.averagingInt(Integer::intValue));
int max = nums.stream().collect(Collectors.maxBy(Comparator.naturalOrder())).orElse(0);

// join strings
String joined = List.of("a", "b", "c").stream()
    .collect(Collectors.joining(", ", "[", "]"));  // "[a, b, c]"

// teeing (Java 12+): two collectors, merge results
record Range(int min, int max) {}
Range range = nums.stream().collect(Collectors.teeing(
    Collectors.minBy(Comparator.naturalOrder()),
    Collectors.maxBy(Comparator.naturalOrder()),
    (mn, mx) -> new Range(mn.orElse(0), mx.orElse(0))
));

Numerische Streams

IntStream/LongStream/DoubleStream sind primitive Spezialisierungen, die Autoboxing-Overhead vermeiden — für numerische Arbeit verwenden. mapToInt/mapToLong/mapToDouble konvertieren Objekt-Streams zu primitiven Streams; boxed() geht zurück. Primitive Streams haben spezialisierte Terminal-Operationen (sum, average, max), die OptionalInt/Double zurückgeben, um leere Streams zu behandeln. Großartig für Performance-sensitive numerische Pipelines.

java
import java.util.stream.*;
import java.util.*;

// IntStream / LongStream / DoubleStream avoid boxing
IntStream range = IntStream.rangeClosed(1, 100);
int sum = range.sum();                       // 5050
double avg = IntStream.of(1, 2, 3).average().orElse(0);

// mapToInt / mapToLong / mapToDouble from object stream
int totalAge = people().stream().mapToInt(Person::age).sum();

// boxed: convert primitive stream back to object stream
List<Integer> list = IntStream.range(0, 5).boxed().toList();

// mapToObj: primitive -> objects
List<String> labels = IntStream.range(1, 4)
    .mapToObj(i -> "item-" + i).toList();

// asLongStream / asDoubleStream
LongStream longs = IntStream.range(0, 5).asLongStream();

// Common numeric operations
int max = IntStream.of(3, 1, 4, 1, 5).max().orElse(Integer.MIN_VALUE);
boolean anyEven = IntStream.of(1, 3, 5).anyMatch(n -> n % 2 == 0);

// Iterate to build numeric sequences
List<Integer> fib = Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]})
    .limit(10).mapToInt(a -> a[0]).boxed().toList();

List<Person> people() { return List.of(new Person("a", "c", 30)); }
record Person(String name, String city, int age) {}

Parallele Streams

parallelStream teilt Arbeit über den gemeinsamen ForkJoinPool auf (auf CPU-Cores dimensioniert). Nur für große Datensätze mit CPU-intensiven, zustandslosen, reihenfolgeunabhängigen Operationen verwenden — bei kleinen Daten übersteigt der Overhead den Nutzen. Geteilten veränderlichen Zustand vermeiden (verursacht Races). I/O in parallelen Streams blockiert den gemeinsamen Pool — einen benutzerdefinierten ForkJoinPool für blockierende Arbeit verwenden. Messen, bevor man annimmt, dass parallel schneller ist.

java
import java.util.*;
import java.util.stream.*;

// parallelStream: uses common ForkJoinPool
long sum = List.of(1, 2, 3, 4, 5).parallelStream()
    .mapToInt(Integer::intValue).sum();

// Convert sequential to parallel
long count = IntStream.range(0, 1_000_000).parallel()
    .filter(n -> n % 2 == 0).count();

// Order may differ — use forEachOrdered if order matters
List.of(1, 2, 3, 4).parallelStream()
    .forEachOrdered(System.out::println);

// Collecting preserves encounter order (but work is parallel)
List<Integer> doubled = IntStream.range(0, 1000).parallel()
    .map(n -> n * 2).boxed().toList();

// Custom thread pool (avoid blocking the common pool)
import java.util.concurrent.ForkJoinPool;
ForkJoinPool pool = new ForkJoinPool(8);
int result = pool.submit(() ->
    IntStream.range(0, 1000).parallel().sum()
).get();

// WHEN to use parallel: large dataset, CPU-heavy per element,
// order-independent, stateless operations
// WHEN NOT: small data, I/O-bound, shared mutable state, ordered ops
12

Generics Deep Dive

Generische Klassen & Methoden

Generics ermöglichen typsicheren wiederverwendbaren Code. Klassen deklarieren Typparameter (<T>); Methoden können auch (<T> vor dem Rückgabetyp). Der Diamond-Operator <> leitet den Typ zur Konstruktion ab. Generics werden zur Kompilierzeit geprüft — sie machen Collections und APIs sicherer, indem sie Typfehler früh abfangen statt zur Laufzeit via ClassCastException.

java
// Generic class
public class Box<T> {
    private T value;
    public void set(T v) { value = v; }
    public T get() { return value; }
}

Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get();  // no cast needed

// Multiple type parameters
public class Pair<K, V> {
    private final K key;
    private final V value;
    public Pair(K k, V v) { key = k; value = v; }
    public K key() { return key; }
    public V value() { return value; }
}

Pair<String, Integer> p = new Pair<>("age", 30);

// Generic method (independent of class type params)
public static <T> T first(List<T> list) {
    return list.get(0);
}

// Generic method with multiple type params
public static <K, V> Map<K, V> zip(List<K> keys, List<V> values) {
    Map<K, V> m = new HashMap<>();
    for (int i = 0; i < keys.size(); i++) m.put(keys.get(i), values.get(i));
    return m;
}

Beschränkte Typparameter

Beschränkte Typparameter (<T extends Bound>) beschränken, welche Typen verwendet werden können, und erlauben das Aufrufen von Methoden der Bound. <T extends Number> bedeutet, T muss ein Number oder Subtyp sein. Mehrere Bounds verwenden & — höchstens eine Klasse (muss zuerst sein), der Rest Interfaces. Bounds sind essenziell für Algorithmen, die spezifische Fähigkeiten benötigen (Vergleichbarkeit, numerische Operationen).

java
// Upper bound: T must be a subtype of Number
public static <T extends Number> double sum(List<T> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}

sum(List.of(1, 2, 3));        // Integer is a Number
sum(List.of(1.0, 2.5));       // Double is a Number
// sum(List.of("a"));          // compile error

// Multiple bounds: T must extend all (first is class, rest interfaces)
interface Comparable<T> { int compareTo(T o); }
interface Serializable {}

public static <T extends Number & Comparable<T> & Serializable>
    T max(List<T> list) {
    T best = list.get(0);
    for (T t : list) if (t.compareTo(best) > 0) best = t;
    return best;
}

// Bound lets you call methods of the bound
public static <T extends CharSequence> int totalLength(List<T> items) {
    int len = 0;
    for (CharSequence c : items) len += c.length();  // can call .length()
    return len;
}

Wildcards: ?, extends, super

Wildcards machen generische Typen flexibel. ? extends T (kovariant) erlaubt das Lesen von T, aber nicht das Schreiben — für Produzenten verwenden. ? super T (kontravariant) erlaubt das Schreiben von T, aber nur das Lesen von Object — für Konsumenten verwenden. Die PECS-Regel (Producer Extends, Consumer Super) leitet an, was zu verwenden ist. copy(dest, src) ist das klassische Beispiel: dest ist ein Konsument (super), src ist ein Produzent (extends).

java
import java.util.*;

// ? (unbounded wildcard) — any type
void printAll(List<?> list) {
    for (Object o : list) System.out.println(o);
}

// ? extends T (upper-bounded / covariant) — producer (PECS: Producer Extends)
double sum(List<? extends Number> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}
sum(List.of(1, 2, 3));     // List<Integer> OK
sum(List.of(1.0, 2.0));    // List<Double> OK
// nums.add(5);  // ERROR: can't add (don't know exact type)

// ? super T (lower-bounded / contravariant) — consumer (PECS: Consumer Super)
void addNumbers(List<? super Integer> list) {
    list.add(1); list.add(2); list.add(3);  // OK to add Integer
}
addNumbers(new ArrayList<Number>());  // OK
addNumbers(new ArrayList<Object>());  // OK
// Number n = list.get(0);  // only safe to read as Object

// PECS rule: Producer Extends, Consumer Super
// If you read from a collection, use ? extends T
// If you write to a collection, use ? super T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}

Type Erasure

Java-Generics verwenden Type Erasure — generische Typen existieren nur zur Kompilierzeit; zur Laufzeit sind List<String> und List<Integer> beide nur List. Dies ermöglicht Rückwärtskompatibilität, hat aber Grenzen: man kann kein new T() machen, keine generischen Arrays erstellen, instanceof mit Generics verwenden oder überladene Methoden mit gleich-erased Signaturen haben. Heap Pollution entsteht, wenn ungeprüfte Casts falsche Typen in Generics legen und Fehler zur Laufzeit verschieben.

java
import java.util.*;

// At runtime, generic types are erased to their bounds (or Object)
// List<String>, List<Integer>, List<?> all become List at runtime

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// Runtime: both are just ArrayList

// You CANNOT do these due to erasure:
// new T()              — can't instantiate type param
// new T[]              — can't create generic array
// instanceof List<String>  — only instanceof List (raw)
// class MyException<T> extends Exception  — can't extend Throwable generically
// static T field       — no static generic fields

// Erasure means overloads clash:
// void process(List<String> list) {}
// void process(List<Integer> list) {}  // ERROR: same erasure

// Checking types at runtime requires Class<T>
public static <T> List<T> filter(List<?> items, Class<T> type) {
    List<T> result = new ArrayList<>();
    for (Object o : items) {
        if (type.isInstance(o)) result.add(type.cast(o));
    }
    return result;
}

// Heap pollution: when unchecked warnings lead to runtime ClassCastException
List<String> polluted = (List<String>)(List) List.of(1, 2);  // unchecked
// String s = polluted.get(0);  // ClassCastException at runtime

Generische Methoden & Inferenz

Typ-Inferenz lässt den Compiler Typargumente aus dem Kontext bestimmen (Argumente und Zieltyp), sodass man sie selten explizit schreibt. Der Diamond-Operator <> ist Inferenz für Konstruktoren. Target-Typing verwendet den erwarteten Typ der Variablen. Einen expliziten Type-Witness (Class.<T>method()) nur verwenden, wenn Inferenz Mehrdeutigkeit nicht auflösen kann. Inferenz macht generischen Code genauso lesbar wie nicht-generischen Code.

java
import java.util.*;

// Type inference: compiler figures out T from arguments
public static <T> T pick(T a, T b) { return Math.random() > 0.5 ? a : b; }
String s = pick("hello", "world");      // T inferred as String
Number n = pick(1, 2.0);                // T inferred as Number (common supertype)

// Target typing: inference uses the expected type
List<String> list = Collections.emptyList();  // T inferred from target

// Inference with method chains
List<Integer> nums = List.of(1, 2, 3);
String joined = nums.stream()
    .map(Object::toString)              // Stream<String>
    .collect(Collectors.joining(","));  // inferred

// Explicit type witness (rarely needed)
Collections.<String>emptyList();

// Generic constructor
class Holder<T> {
    private T value;
    <U extends T> Holder(U init) { value = init; }  // constructor type param
    T get() { return value; }
}
Holder<Number> h = new Holder<>(42);  // U=Integer, T=Number

import java.util.stream.Collectors;
class Math { static double random() { return 0.5; } }

Generische Interfaces & Patterns

Generische Interfaces (wie Repository<T,ID>) definieren wiederverwendbare Verträge. Das selbst-referenzierende Bound-Pattern (class X implements Comparable<X>) stellt sicher, dass compareTo nur denselben Typ akzeptiert. Das Type-Token-Pattern (Class<T> als Schlüssel verwenden) umgeht Erasure und bietet Runtime-Typsicherheit in heterogenen Containern. Diese Patterns sind das Rückgrat von Frameworks wie Spring Data.

java
// Generic interface
interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    void save(T entity);
}

// Implement with concrete types
class UserRepository implements Repository<User, Long> {
    public Optional<User> findById(Long id) { /* ... */ return Optional.empty(); }
    public List<User> findAll() { return List.of(); }
    public void save(User entity) {}
}

// Generic interface with self-referencing bound (Comparable pattern)
interface Comparable<T> {
    int compareTo(T other);
}
class Temperature implements Comparable<Temperature> {
    private final double celsius;
    Temperature(double c) { celsius = c; }
    public int compareTo(Temperature other) {
        return Double.compare(celsius, other.celsius);
    }
}

// Generic builder pattern
class Builder<T> {
    private T value;
    public Builder<T> set(T v) { value = v; return this; }
    public T build() { return value; }
}

// Type token pattern for runtime type safety
class TypeSafeMap {
    private final Map<Class<?>, Object> map = new HashMap<>();
    public <T> void put(Class<T> type, T value) { map.put(type, value); }
    public <T> T get(Class<T> type) { return type.cast(map.get(type)); }
}

record User(String name) {}
import java.util.Optional;
13

Annotationen

Eingebaute Annotationen

Javas eingebaute Annotationen: @Override (fängt Tippfehler beim Overriding ein — immer verwenden), @Deprecated (signalisiert, dass API nicht verwendet werden sollte, mit since/forRemoval-Metadaten), @SuppressWarnings (unterdrückt spezifische Warnungen — sparsam verwenden), @FunctionalInterface (erzwingt die SAM-Regel). Dies sind die alltäglichen Annotationen, die Compile-Zeit-Sicherheit und Dokumentation verbessern.

java
import java.util.*;

// @Override: declares intent to override (compiler checks)
class Animal {
    public void sound() { System.out.println("..."); }
}
class Dog extends Animal {
    @Override
    public void sound() { System.out.println("Woof"); }
}

// @Deprecated: marks API as outdated
class OldApi {
    @Deprecated(since = "1.5", forRemoval = true)
    public void legacy() {}
}

// @SuppressWarnings: silence compiler warnings
@SuppressWarnings("unchecked")
List<String> list = (List<String>) new ArrayList();

// @FunctionalInterface: enforces single abstract method
@FunctionalInterface
interface Op { int apply(int a, int b); }

// Common warning keys: unchecked, deprecation, rawtypes, null

// Java 17+ sealed-related
@Deprecated
class ToRemove {}

Benutzerdefinierte Annotationen

Benutzerdefinierte Annotationen werden mit @interface deklariert. Member sehen aus wie Methoden, sind aber Annotations-Attribute — sie können Default-Werte haben. @Target verwenden, um einzuschränken, wo sie gilt (TYPE, METHOD, FIELD usw.), und @Retention zur Steuerung der Verfügbarkeit. Marker-Annotationen (ohne Member) taggen nur Elemente. Annotationen selbst tragen kein Verhalten — Prozessoren (Reflection, Annotation-Tools) lesen sie und handeln.

java
import java.lang.annotation.*;

// Define an annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
    String value() default "";
    long timeout() default 0L;
}

// Use it
class MyTests {
    @Test
    public void quickCheck() {}

    @Test(timeout = 5000)
    public void slowCheck() {}

    @Test("custom-name")
    public void named() {}
}

// Annotation with default values
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Entity {
    String table() default "";
    String[] columns() default {};
}

@Entity(table = "users", columns = {"id", "name"})
class User {}

// Marker annotation (no members)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {}

Retention & Target

@Retention steuert, wie lange eine Annotation überlebt: SOURCE (nur Kompilierzeit, wie @Override), CLASS (in Bytecode, aber nicht zur Laufzeit sichtbar — der Default), RUNTIME (via Reflection zugänglich). @Target beschränkt, wo eine Annotation erscheinen kann. Java 8+ fügte TYPE_USE und TYPE_PARAMETER hinzu, sodass man Generics und Casts annotieren kann (List<@NonNull String>). RUNTIME nur wählen, wenn man Reflection-Zugriff braucht.

java
import java.lang.annotation.*;

// RetentionPolicy.SOURCE: discarded by compiler (e.g., @Override)
@Retention(RetentionPolicy.SOURCE)
@interface CompileOnly {}

// RetentionPolicy.CLASS: kept in .class but not loaded (default)
@Retention(RetentionPolicy.CLASS)
@interface BytecodeOnly {}

// RetentionPolicy.RUNTIME: available via reflection at runtime
@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeVisible {}

// ElementType targets
@Target(ElementType.TYPE)         // classes, interfaces, enums
@interface ForType {}

@Target(ElementType.METHOD)
@interface ForMethod {}

@Target(ElementType.FIELD)
@interface ForField {}

@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@interface ForTypeUse {}

// Java 8+ type-use annotations (annotate any type occurrence)
@RuntimeVisible String[] names;  // example usage
List<@RuntimeVisible String> typed;

Annotationen via Reflection lesen

Annotationen mit RUNTIME-Retention können via Reflection gelesen werden: isAnnotationPresent() prüft Existenz, getAnnotation() ruft sie ab. So verdrahten Frameworks (Spring, JUnit, JAX-RS) deklarativ Verhalten — man taggt Methoden/Klassen, das Framework scannt und dispatcht. Annotation-Verarbeitung zur Kompilierzeit (Annotation Processors) ist eine Alternative für Code-Generierung ohne Runtime-Reflection-Kosten.

java
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Route {
    String path();
    String method() default "GET";
}

class Api {
    @Route(path = "/users", method = "GET")
    public void listUsers() {}

    @Route(path = "/users", method = "POST")
    public void createUser() {}
}

// Scan methods for @Route at runtime
for (Method m : Api.class.getDeclaredMethods()) {
    if (m.isAnnotationPresent(Route.class)) {
        Route r = m.getAnnotation(Route.class);
        System.out.println(r.method() + " " + r.path() + " -> " + m.getName());
    }
}
// Output:
// GET /users -> listUsers
// POST /users -> createUser

// Reading annotations on a class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table { String name(); }

@Table(name = "orders")
class Order {}
Table t = Order.class.getAnnotation(Table.class);
System.out.println(t.name());  // "orders"

Repeatable & Meta-Annotationen

@Repeatable (Java 8+) erlaubt, dieselbe Annotation mehrfach anzuwenden, indem eine Container-Annotation definiert wird. @Inherited lässt Annotationen an Subclasses propagieren (nur für Klassen-Level-Annotationen). @Documented inkludiert die Annotation in Javadoc. @Target mit ANNOTATION_TYPE erstellt Meta-Annotationen (Annotationen, die andere Annotationen annotieren) — so baut Spring komponierbare Annotation-Stereotype wie @RestController = @Controller + @ResponseBody.

java
import java.lang.annotation.*;

// Repeatable: allow same annotation multiple times
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule {
    String cron();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules {
    Schedule[] value();  // container annotation
}

// Make Schedule repeatable
@Repeatable(Schedules.class)
@interface Schedule2 {
    String cron();
}

// Now you can repeat it (Java 8+)
class Job {
    @Schedule2(cron = "0 0 * * *")
    @Schedule2(cron = "0 30 * * *")
    public void run() {}
}

// Meta-annotations: annotations on annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)  // can only annotate other annotations
@interface TestCategory {}

@TestCategory
@Retention(RetentionPolicy.RUNTIME)
@interface UnitTest {}

// Inherited: subclass inherits the annotation
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Persistent {}
@Persistent class Base {}
class Child extends Base {}  // Child also has @Persistent

// Documented: appears in Javadoc
@Documented
@interface PublicApi {}
14

Reflection

Class-Objekt & Klassen abrufen

Jeder geladene Typ hat ein einzigartiges Class-Objekt — der Einstiegspunkt zur Reflection. Abrufen via Klassen-Literal (Type.class), instance.getClass() oder Class.forName() (dynamisches Laden, wirft ClassNotFoundException). Das Class-Objekt exponiert Name, Modifier, Superclass, Interfaces und Typ-Checks (isInterface, isArray, isEnum, isRecord). isAssignableFrom prüft polymorphe Beziehungen.

java
import java.lang.reflect.*;

// Three ways to get a Class object
Class<String> c1 = String.class;                  // class literal
Class<?> c2 = "hello".getClass();                 // from instance
Class<?> c3 = Class.forName("java.lang.String");  // by name (throws checked)

// Basic introspection
Class<?> c = String.class;
System.out.println(c.getName());          // "java.lang.String"
System.out.println(c.getSimpleName());    // "String"
System.out.println(c.getPackage());       // "package java.lang"
System.out.println(c.getSuperclass());    // "class java.lang.Object"
System.out.println(Modifier.toString(c.getModifiers()));  // "public final"

// Check type relationships
System.out.println(c.isInterface());      // false
System.out.println(c.isArray());          // false
System.out.println(c.isEnum());           // false
System.out.println(c.isRecord());         // false (Java 16+)
System.out.println(CharSequence.class.isAssignableFrom(c));  // true

// Primitive class objects
Class<?> intClass = int.class;
Class<?> intArrayClass = int[].class;
System.out.println(intClass.isPrimitive());  // true

Felder, Methoden, Konstruktoren inspizieren

getDeclaredFields/Methods/Constructors geben ALLE Member (inklusive private) zurück, die nur in dieser Klasse deklariert sind. getFields/getMethods geben nur public Member zurück, inkludieren aber vererbte. Um ein spezifisches Member zu finden, getDeclaredField(name) oder getDeclaredMethod(name, paramTypes...) verwenden — Parametertypen werden benötigt, um Überladungen zu disambiguieren. Reflection umgeht die Zugriffskontrolle, außer man ruft setAccessible(true) auf.

java
import java.lang.reflect.*;
import java.util.*;

class Sample {
    public String name;
    private int count;
    public Sample() {}
    public Sample(String n) { name = n; }
    private void secret() {}
    public int compute(int x) { return x * 2; }
}

Class<?> c = Sample.class;

// Fields: getDeclaredFields includes private; getFields only public
for (Field f : c.getDeclaredFields()) {
    System.out.println(f.getName() + " : " + f.getType().getSimpleName()
        + " (" + Modifier.toString(f.getModifiers()) + ")");
}

// Methods
for (Method m : c.getDeclaredMethods()) {
    System.out.println(m.getName()
        + " params=" + Arrays.toString(m.getParameterTypes())
        + " returns=" + m.getReturnType().getSimpleName());
}

// Constructors
for (Constructor<?> ctor : c.getConstructors()) {
    System.out.println("ctor params=" + Arrays.toString(ctor.getParameterTypes()));
}

// Lookup specific member
Field nameField = c.getDeclaredField("name");
Method compute = c.getDeclaredMethod("compute", int.class);
Constructor<?> ctor = c.getDeclaredConstructor(String.class);

Methoden aufrufen & Instanzen erstellen

Method.invoke(obj, args...) ruft eine Methode reflektiv auf — gibt immer Object zurück, also das Ergebnis casten. setAccessible(true) umgeht Java-Zugriffschecks (private Member werden erreichbar; kann --add-opens bei Modulen erfordern). Constructor.newInstance() erstellt Objekte — das reflective Äquivalent von new. Array.newInstance erstellt Arrays eines zur Laufzeit bekannten Komponententyps. Reflection ist langsamer als direkte Aufrufe und umgeht Compile-Zeit-Sicherheit.

java
import java.lang.reflect.*;

class Greeter {
    public String greet(String name) { return "Hello, " + name; }
    private String secret() { return "hidden"; }
}

Object obj = new Greeter();
Class<?> c = obj.getClass();

// Invoke public method
Method greet = c.getMethod("greet", String.class);
String result = (String) greet.invoke(obj, "Alice");  // "Hello, Alice"

// Invoke private method
Method sec = c.getDeclaredMethod("secret");
sec.setAccessible(true);  // bypass access check
String s = (String) sec.invoke(obj);  // "hidden"

// Create instances via constructor
Constructor<?> noArg = c.getConstructor();
Object o1 = noArg.newInstance();

// With args
class Person {
    String name;
    public Person(String n) { name = n; }
    public String toString() { return "Person(" + name + ")"; }
}
Constructor<?> ctor = Person.class.getConstructor(String.class);
Object p = ctor.newInstance("Bob");
System.out.println(p);  // "Person(Bob)"

// Array creation via reflection
Object strArray = Array.newInstance(String.class, 5);
Array.set(strArray, 0, "first");
String v = (String) Array.get(strArray, 0);

Felder lesen & modifizieren

Field.get(instance) liest den Wert eines Fields; Field.set(instance, value) schreibt ihn. Für Primitives typspezifische Accessoren verwenden (getInt/setInt), um Boxing zu vermeiden. Statische Fields nehmen null als Instanz-Argument. setAccessible(true) ist für private Fields erforderlich. Reflection-Feldzugriff ist, wie Serialisierungs-Bibliotheken (Jackson, Gson) und ORM-Frameworks Objektzustand generisch lesen/schreiben.

java
import java.lang.reflect.*;

class Config {
    public String env = "dev";
    private int retries = 3;
    public static String VERSION = "1.0";
}

Config cfg = new Config();
Class<?> c = cfg.getClass();

// Read public field
Field env = c.getField("env");
String e = (String) env.get(cfg);  // "dev"

// Read private field
Field retries = c.getDeclaredField("retries");
retries.setAccessible(true);
int r = retries.getInt(cfg);  // 3
// For objects: Object val = field.get(instance);

// Modify fields
env.set(cfg, "prod");
retries.setInt(cfg, 5);
System.out.println(cfg.env);     // "prod"

// Static fields: pass null as the instance
Field version = c.getField("VERSION");
String v = (String) version.get(null);  // "1.0"
version.set(null, "2.0");

// Type-specific getters/setters avoid boxing
// getInt/setInt, getLong/setLong, getBoolean/setBoolean, etc.
// For reference types, use get()/set()

Dynamische Proxies & Anwendungsfälle

java.lang.reflect.Proxy erstellt dynamische Proxies, die Interfaces zur Laufzeit implementieren — ein InvocationHandler fängt jeden Aufruf ab. So funktionieren Spring AOP, Hibernate Lazy Loading und Mockito Mocks. Reflection treibt die meisten Java-Frameworks (DI, ORM, Serialisierung, Testing), hat aber Kosten: langsamer als direkte Aufrufe, schwächere Typsicherheit und Modulsystem-Einschränkungen. Verwenden, wenn Runtime-Flexibilität benötigt wird, nicht für normalen Code.

java
import java.lang.reflect.*;
import java.util.*;

// JDK dynamic proxy: implements interfaces at runtime
interface UserService {
    String getUser(long id);
    void deleteUser(long id);
}

// InvocationHandler intercepts every method call
class LoggingHandler implements InvocationHandler {
    private final Object target;
    LoggingHandler(Object t) { target = t; }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Calling " + method.getName() + "(" + Arrays.toString(args) + ")");
        long start = System.nanoTime();
        Object result = method.invoke(target, args);
        System.out.println("  -> " + result + " (" + (System.nanoTime() - start) + "ns)");
        return result;
    }
}

UserService real = id -> "user-" + id;
UserService proxied = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class<?>[]{UserService.class},
    new LoggingHandler(real)
);
proxied.getUser(42);  // logs the call and result

// Common reflection use cases:
// - Frameworks: Spring DI, JPA entities, Jackson serialization
// - AOP/proxies: transactions, logging, security
// - Annotation processing at runtime
// - Test frameworks: JUnit discovers @Test methods
// - Code generation tools

// Caution: reflection is slower, bypasses compile-time checks,
// and can break under modules (--add-opens). Prefer alternatives when possible.
15

JDBC & Datenbankzugriff

Connection & DriverManager

DriverManager.getConnection() öffnet eine Datenbankverbindung — immer in try-with-resources wickeln, um Lecks zu vermeiden. Seit JDBC 4 registrieren sich Treiber automatisch via ServiceLoader, sodass Class.forName() selten benötigt wird. Das URL-Format variiert je nach Vendor. Properties für Verbindungsoptionen verwenden (SSL, Timeouts). In Produktion einen Connection-Pool (HikariCP) gegenüber direkten DriverManager-Aufrufen bevorzugen.

java
import java.sql.*;

// Basic connection (try-with-resources auto-closes)
String url = "jdbc:postgresql://localhost:5432/mydb";
try (Connection conn = DriverManager.getConnection(url, "user", "pass")) {
    System.out.println("Connected: " + conn.getSchema());
    // ... use connection
}

// Modern: no need for Class.forName() with JDBC 4+ (auto-discovery)
// Legacy: Class.forName("org.postgresql.Driver");

// Common URL patterns:
// jdbc:postgresql://host:5432/db
// jdbc:mysql://host:3306/db
// jdbc:oracle:thin:@host:1521:db
// jdbc:sqlite:/path/to/db.sqlite
// jdbc:h2:mem:test  (in-memory H2)

// Connection properties
import java.util.Properties;
Properties props = new Properties();
props.setProperty("user", "user");
props.setProperty("password", "pass");
props.setProperty("ssl", "true");
try (Connection c = DriverManager.getConnection(url, props)) {
    // ...
}

Statement vs. PreparedStatement

Immer PreparedStatement gegenüber Statement für jede Query mit Parametern verwenden — es verhindert SQL-Injection, indem es SQL-Struktur von Daten trennt. Parameter werden mit 1-basiertem Index mit typspezifischen Settern gesetzt. PreparedStatements können wiederverwendet werden (Parameter neu setzen und erneut ausführen) und unterstützen Batching (addBatch/executeBatch) für Bulk-Operationen. Statement ist nur in Ordnung für statisches, vertrauenswürdiges SQL wie DDL.

java
import java.sql.*;

// Statement: plain SQL (vulnerable to injection — avoid for user input)
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     Statement st = c.createStatement()) {

    st.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))");
    st.execute("INSERT INTO users VALUES (1, 'Alice')");

    // NEVER do this with user input — SQL injection!
    // st.execute("SELECT * FROM users WHERE name = '" + userInput + "'");
}

// PreparedStatement: parameterized, safe from injection, can be reused
String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     PreparedStatement ps = c.prepareStatement(sql)) {

    // Set parameters by index (1-based!)
    ps.setInt(1, 1);
    ps.setString(2, "Alice");
    ps.executeUpdate();

    // Reuse with different params
    ps.setInt(1, 2);
    ps.setString(2, "Bob");
    ps.executeUpdate();

    // Batch inserts
    for (int i = 3; i <= 100; i++) {
        ps.setInt(1, i);
        ps.setString(2, "user" + i);
        ps.addBatch();
    }
    ps.executeBatch();
}

ResultSet & Abfragen

ResultSet ist ein Cursor über Query-Zeilen — next() aufrufen, um fortzuschreiten (gibt false am Ende zurück). Spalten nach Name (lesbar) oder 1-basiertem Index lesen. wasNull() unterscheidet SQL NULL von einem primitiven Default (z.B. getInt gibt 0 für NULL zurück). Default-ResultSets sind nur vorwärts; TYPE_SCROLL_INSENSITIVE + CONCUR_UPDATABLE ermöglicht Random-Access und In-Place-Updates, was in modernen Apps selten verwendet wird.

java
import java.sql.*;
import java.util.*;

record User(int id, String name, String email) {}

// Execute query and map rows
String sql = "SELECT id, name, email FROM users WHERE active = ?";
List<User> users = new ArrayList<>();
try (Connection c = getConnection();
     PreparedStatement ps = c.prepareStatement(sql)) {
    ps.setBoolean(1, true);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {  // advance to next row
            users.add(new User(
                rs.getInt("id"),       // by column name
                rs.getString("name"),
                rs.getString("email")  // by column name (preferred)
            ));
        }
    }
}

// Column access by index (1-based) or name (more readable)
// rs.getInt(1), rs.getString(2), rs.getBoolean("active")

// Handle NULLs
String nick = rs.getString("nickname");
if (rs.wasNull()) nick = "anonymous";  // distinguish NULL from real null

// Scrollable/updatable ResultSet (needs specific flags)
Statement st = c.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE,
    ResultSet.CONCUR_UPDATABLE
);
ResultSet rs = st.executeQuery("SELECT * FROM users");
rs.absolute(5);  // jump to row 5
rs.updateString("name", "newname");
rs.updateRow();  // persist change

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Transaktionen & Batch

JDBC auto-committet standardmäßig jede Anweisung — autoCommit(false) setzen, um Anweisungen zu einer Transaktion zu gruppieren. Bei Erfolg committen, bei Fehlern zurückrollen. Savepoints erlauben partielles Rollback innerhalb einer Transaktion. Isolation-Levels steuern die Sichtbarkeit gleichzeitiger Änderungen (READ_COMMITTED ist der übliche Default; SERIALIZABLE ist am sichersten, aber am langsamsten). Immer autoCommit wiederherstellen oder die Verbindung schließen, um Transaction-Zustand nicht zu leaken.

java
import java.sql.*;

// Transactions: disabled by default (auto-commit = true)
try (Connection c = getConnection()) {
    c.setAutoCommit(false);  // start transaction
    try (PreparedStatement ps = c.prepareStatement(
            "UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
        ps.setInt(1, 100); ps.setInt(2, 1); ps.executeUpdate();  // debit
        ps.setInt(1, -100); ps.setInt(2, 2); ps.executeUpdate();  // credit
    }
    c.commit();  // commit both

    // If any step fails, rollback
} catch (SQLException e) {
    // connection auto-closed; rollback happens implicitly on close if not committed
}

// Explicit rollback pattern
try (Connection c = getConnection()) {
    c.setAutoCommit(false);
    try {
        // ... multiple statements
        c.commit();
    } catch (SQLException e) {
        c.rollback();  // undo all changes in this transaction
        throw e;
    }
}

// Savepoints: partial rollback
Statement st = c.createStatement();
st.execute("INSERT INTO log VALUES (1)");
Savepoint sp = c.setSavepoint("before-risky");
st.execute("INSERT INTO log VALUES (2)");
c.rollback(sp);  // undo only after savepoint
c.commit();

// Transaction isolation levels
c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Levels: NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Connection Pooling (HikariCP)

Connection-Pools (HikariCP ist der De-facto-Standard) halten Verbindungen warm und wiederverwenden sie, was die 10-100ms-Kosten vermeidet, pro Anfrage eine neue TCP+Auth-Verbindung zu öffnen. Max Pool-Größe konfigurieren (begrenzt durch DB-Kapazität), Timeouts und Lifetime. Mit getConnection() ausleihen, durch Schließen zurückgeben (es geht zurück zum Pool, wird nicht geschlossen). DataSource beim Shutdown immer schließen. In Spring Boot wird HikariCP automatisch konfiguriert.

java
import com.zaxxer.hikari.*;
import java.sql.*;
import java.util.*;

// HikariCP: high-performance JDBC connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30_000);   // ms to wait for a connection
config.setIdleTimeout(600_000);        // ms before idle connections close
config.setMaxLifetime(1_800_000);      // ms max connection lifetime
config.setPoolName("app-pool");

HikariDataSource ds = new HikariDataSource(config);

// Borrow a connection, use it, return it (auto via try-with-resources)
try (Connection c = ds.getConnection();
     PreparedStatement ps = c.prepareStatement("SELECT * FROM users")) {
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) System.out.println(rs.getString("name"));
    }
}  // connection returns to pool here

// Shutdown pool when app stops
ds.close();

// Why pool? Opening a TCP+auth connection is expensive (~10-100ms).
// A pool keeps connections warm and reuses them across requests,
// dramatically reducing latency under load.

// Alternative pools: Apache DBCP, c3p0, Tomcat JDBC, Agroal
16

I/O & NIO Deep Dive

InputStream & OutputStream (Bytes)

InputStream/OutputStream verarbeiten rohe Bytes. Immer in Buffered*-Varianten wickeln — ungepuffertes I/O macht einen System-Call pro Byte, was katastrophal für die Performance ist. read() gibt -1 am Stream-Ende zurück. transferTo() (Java 9+) macht einen effizienten Bulk-Copy. readAllBytes() ist praktisch, lädt aber den gesamten Stream in den Speicher — nur für kleine Dateien. Streams immer schließen (try-with-resources).

java
import java.io.*;

// Read bytes from a file
try (InputStream in = new FileInputStream("input.bin")) {
    int b;
    while ((b = in.read()) != -1) {  // -1 = end of stream
        // process byte
    }
}

// Buffered for performance (8KB default buffer)
try (InputStream in = new BufferedInputStream(new FileInputStream("big.bin"))) {
    byte[] buffer = new byte[8192];
    int read;
    while ((read = in.read(buffer)) != -1) {
        // process buffer[0..read]
    }
}

// readAllBytes (small files only — loads everything into memory)
byte[] all = new FileInputStream("small.bin").readAllBytes();

// Write bytes
try (OutputStream out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
    out.write(65);  // single byte
    out.write(new byte[]{66, 67, 68});
    out.flush();  // force buffered data to disk
}

// Copy streams (Java 9+)
try (InputStream in = new FileInputStream("src.bin");
     OutputStream out = new FileOutputStream("dst.bin")) {
    in.transferTo(out);  // efficient bulk copy
}

// Standard streams
System.in.read();   // stdin (InputStream)
System.out.write(65);  // stdout (PrintStream)

Channels & Buffers (NIO)

NIO Channels + ByteBuffers sind die High-Performance-Alternative zu Streams. Buffer haben position/limit/capacity; flip() schaltet von Schreib- zu Lesemodus, clear() setzt zum Schreiben zurück, compact() bewahrt ungelesene Daten. Direct Buffer (allocateDirect) leben außerhalb des JVM-Heaps und vermeiden einen Copy-Schritt für großes I/O. transferTo zwischen Channels kann Zero-Copy auf unterstützten Betriebssystemen verwenden. NIO verwenden, wenn Streaming-Performance wichtig ist.

java
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.*;

// Channel: high-performance, block-oriented I/O
try (FileChannel ch = FileChannel.open(Path.of("data.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {

    // ByteBuffer: fixed-capacity block of bytes
    ByteBuffer buf = ByteBuffer.allocate(1024);
    int read = ch.read(buf);  // fill buffer from channel
    buf.flip();  // switch from write mode to read mode

    while (buf.hasRemaining()) {
        byte b = buf.get();
    }
    buf.clear();  // reset for next read (or compact() to preserve unread)
}

// Direct buffer: outside JVM heap, faster for large I/O (no copy)
ByteBuffer direct = ByteBuffer.allocateDirect(64 * 1024);

// Scatter/gather: read into multiple buffers / write from multiple
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ch.read(new ByteBuffer[]{header, body});  // scatter

// Transfer between channels (zero-copy on some OSes)
try (FileChannel src = FileChannel.open(Path.of("a.bin"));
     FileChannel dst = FileChannel.open(Path.of("b.bin"), StandardOpenOption.WRITE)) {
    src.transferTo(0, src.size(), dst);
}

// ByteOrder
buf.order(ByteOrder.LITTLE_ENDIAN);
int value = buf.getInt();

Path-Operationen (NIO.2)

Path (NIO.2) ersetzt die alte File-Klasse. Path-Operationen (getFileName, getParent, resolve, normalize, relativize) sind reine String-Mathematik — sie berühren nicht die Festplatte. Files.*-Methoden interagieren mit dem Dateisystem: Größe, Zeitstempel, Berechtigungen, Symlinks. normalize() bereinigt . und ..-Segmente. resolveSibling ist praktisch zum Umbenennen (gleiches Verzeichnis, anderer Name). Path gegenüber File in modernem Code bevorzugen.

java
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;

// Creating Path objects
Path p1 = Path.of("a/b/c.txt");          // relative
Path p2 = Path.of("/usr/local/bin");     // absolute
Path p3 = Paths.get("config.json");      // older API, same thing

// Path manipulation (doesn't touch filesystem)
System.out.println(p1.getFileName());    // "c.txt"
System.out.println(p1.getParent());      // "a/b"
System.out.println(p1.getRoot());        // "" (relative)
System.out.println(p2.getRoot());        // "/"
System.out.println(p1.toAbsolutePath()); // "/cwd/a/b/c.txt"
System.out.println(p1.normalize());      // removes . and ..
System.out.println(p1.resolve("d.txt")); // "a/b/c.txt/d.txt"
System.out.println(p1.resolveSibling("x.txt")); // "a/b/x.txt"
System.out.println(p1.relativize(Path.of("a/b")));  // "../.."

// File metadata
Path file = Path.of("notes.txt");
System.out.println(Files.size(file));            // bytes
System.out.println(Files.getLastModifiedTime(file));
System.out.println(Files.isReadable(file));
System.out.println(Files.isWritable(file));

// PosixFilePermissions (Linux/macOS)
String perms = "rwxr-xr--";
Set<PosixFilePermission> set = PosixFilePermissions.fromString(perms);
Files.setPosixFilePermissions(file, set);

// Symbolic links
Path link = Path.of("link.txt");
Files.createSymbolicLink(link, Path.of("target.txt"));
Path target = Files.readSymbolicLink(link);

Verzeichnis-Durchlauf & Dateibäume

Files.list() listet eine Ebene; Files.walk() durchläuft rekursiv den Baum (gibt einen Stream zurück, muss geschlossen werden). Files.find() filtert nach Pfad und Attributen während des Durchlaufs. Für volle Kontrolle ermöglicht walkFileTree mit einem FileVisitor, Subtrees zu überspringen, Fehler zu behandeln und vor/nach dem Besuchen von Verzeichnissen zu handeln. Alle geben Streams zurück, die File-Handles halten — immer try-with-resources verwenden. max depth verwenden, um teure Durchläufe zu begrenzen.

java
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;

// List directory entries (one level)
try (Stream<Path> entries = Files.list(Path.of("."))) {
    entries.filter(Files::isRegularFile)
           .map(Path::getFileName)
           .forEach(System.out::println);
}

// Walk file tree recursively (depth-first)
try (Stream<Path> walk = Files.walk(Path.of("/project"))) {
    walk.filter(Files::isRegularFile)
        .filter(p -> p.toString().endsWith(".java"))
        .forEach(System.out::println);
}

// Walk with depth limit
try (Stream<Path> walk = Files.walk(Path.of("/project"), 3)) {
    walk.forEach(System.out::println);
}

// Find with BiPredicate (path + attributes)
try (Stream<Path> found = Files.find(Path.of("/logs"), 10,
        (path, attrs) -> attrs.isRegularFile()
            && attrs.size() > 1_000_000
            && path.toString().endsWith(".log"))) {
    found.forEach(p -> System.out.println("Large log: " + p));
}

// FileVisitor for full control (pre/post visit, skip subtrees)
Files.walkFileTree(Path.of("/project"), new SimpleFileVisitor<>() {
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("Visiting " + file);
        return FileVisitResult.CONTINUE;
    }
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        if (dir.getFileName().toString().equals("target")) {
            return FileVisitResult.SKIP_SUBTREE;  // skip target dirs
        }
        return FileVisitResult.CONTINUE;
    }
});

import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributes;

WatchService & Datei-Events

WatchService empfängt Dateisystem-Events (create, modify, delete) für registrierte Verzeichnisse. take() blockiert, bis ein Event ankommt; pollEvents() entleert sie. Das Eltern-Verzeichnis beobachten und nach context() filtern, um eine spezifische Datei zu verfolgen. Events können coalesced oder verloren gehen (OVERFLOW). Rekursive Beobachtung erfordert das Registrieren jedes Unterverzeichnisses. WatchService ist OS-nativ (inotify auf Linux, FSEvents auf macOS), aber die API ist Low-Level — Bibliotheken für komplexe Bedürfnisse in Betracht ziehen.

java
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;

// Watch a directory for changes
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
    Path dir = Path.of("/var/log");
    dir.register(watcher,
        ENTRY_CREATE,
        ENTRY_DELETE,
        ENTRY_MODIFY);

    System.out.println("Watching " + dir + "...");
    while (true) {
        WatchKey key = watcher.take();  // blocks until event
        for (WatchEvent<?> event : key.pollEvents()) {
            Path changed = dir.resolve((Path) event.context());
            System.out.println(event.kind() + " -> " + changed);

            if (event.kind() == OVERFLOW) continue;  // events lost

            // React to change
            if (event.kind() == ENTRY_CREATE) {
                System.out.println("New file: " + changed);
            }
        }
        if (!key.reset()) break;  // key no longer valid (dir deleted)
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// Note: WatchService watches directories, not individual files.
// To watch a file, watch its parent dir and filter by context().
// Events may be coalesced (rapid modifications may yield one event).
// For recursive watching, register each subdirectory manually
// or use a library like Apache Commons IO's FileAlterationMonitor.
17

Records & Pattern Matching

Records-Grundlagen

Records (Java 16+) sind transparente unveränderliche Daten-Carrier. Der Header `record Name(Type1 f1, Type2 f2)` generiert einen Konstruktor, Accessoren (f1(), f2() — nicht getF1()), equals, hashCode und toString. Sie sind ideal für DTOs, Value Objects und Funktionsergebnisse. Der Compact-Constructor (nur `{ ... }`) validiert oder normalisiert, ohne Fields neu zuzuweisen. Records können Interfaces implementieren, aber keine Klassen erweitern.

java
// Record: concise immutable data carrier (Java 16+)
public record Point(int x, int y) {}

// Equivalent to writing:
// - final class with private final int x, y
// - constructor Point(int x, int y)
// - accessors x(), y()
// - equals, hashCode, toString (all auto-generated)

Point p = new Point(3, 4);
System.out.println(p.x());          // 3 (accessor, not getX())
System.out.println(p.y());          // 4
System.out.println(p);              // "Point[x=3, y=4]"
System.out.println(p.equals(new Point(3, 4)));  // true

// Immutable: no setters
// p.setX(5);  // no such method

// Custom record with validation
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age: " + value);
        }
    }
}

// Record with multiple components
public record Employee(String name, int id, double salary, String dept) {}

// Records implement interfaces
public interface Named { String name(); }
public record Customer(String name, long id) implements Named {}

Compact Constructors & benutzerdefinierte Methoden

Der Compact-Constructor (`public Name { ... }`) läuft vor der Field-Zuweisung — dem Parameter zuweisen, um zu normalisieren, und der Compiler weist es dem Field zu. Records können zusätzliche Methoden und statische Factories haben, aber keine Instanz-Felder über den Header hinaus. Nicht-kanonische Konstruktoren müssen an den kanonischen via this(...) delegieren. Statische Factories für klarere Konstruktion (Point.origin()) und zum Cachen häufiger Instanzen verwenden.

java
// Compact constructor: validation without reassigning
public record Email(String address) {
    public Email {
        if (!address.contains("@")) {
            throw new IllegalArgumentException("Bad email: " + address);
        }
        address = address.toLowerCase().strip();  // normalize
    }
}

// Add custom methods (but not mutable fields)
public record Money(double amount, String currency) {
    public Money plus(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(amount + other.amount, currency);
    }
    public Money times(int factor) {
        return new Money(amount * factor, currency);
    }
    public static Money usd(double amt) { return new Money(amt, "USD"); }
}

// Canonical + non-canonical constructors
public record Range(int start, int end) {
    // Compact canonical (validation)
    public Range {
        if (start > end) throw new IllegalArgumentException();
    }
    // Non-canonical convenience constructor
    public Range(int end) {
        this(0, end);
    }
}

// Static factories are common on records
public record Point(int x, int y) {
    public static Point origin() { return new Point(0, 0); }
    public static Point of(int x, int y) { return new Point(x, y); }
}

Sealed Classes

Sealed Classes/Interfaces (Java 17+) beschränken, welche Typen sie erweitern können, via der `permits`-Klausel. Jeder erlaubte Subtyp muss final, sealed oder non-sealed sein. Kombiniert mit Records bilden sie algebraische Datentypen (geschlossene Vererbung + unveränderliche Daten). Der Schlüsselvorteil: der Compiler kennt alle Subtypen, sodass Switch-Expressions ohne default-Branch erschöpfend sein können — fügt man einen neuen Subtyp hinzu, markiert der Compiler jeden Switch, der aktualisiert werden muss.

java
// Sealed class: restricts which classes can extend it (Java 17+)
public sealed interface Shape
    permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}

// Every permitted subtype must be final, sealed, or non-sealed
public non-sealed class WeirdShape implements Shape {}  // open again

// Why sealed? Enables exhaustive pattern matching
public double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square sq -> sq.side() * sq.side();
        case Triangle t -> 0.5 * t.base() * t.height();
        // No default needed — compiler knows all subtypes
    };
}

// Sealed class hierarchy with records = algebraic data types
// Combines: closed inheritance (sealed) + immutable data (record)

// Sealed classes also work with classes (not just interfaces)
public sealed abstract class Result permits Success, Failure {}
public final class Success<T>(T value) extends Result {}
public final class Failure(String error) extends Result {}

Pattern Matching für instanceof

Pattern Matching für instanceof (Java 16+) deklariert eine Variable, die nur gebunden wird, wenn der Test erfolgreich ist, was den expliziten Cast eliminiert. Der Scope der Variable fließt aus der Wahrheit des Patterns — verwendbar in &&-Fortsetzungen und nach Early Returns. Java 21 fügte Pattern Matching in switch hinzu (case Type var when guard), was typbasiertes Dispatch mit Guards ermöglicht. Das macht Typ-Checking-Code deutlich prägnanter und weniger fehleranfällig.

java
// Old way: cast after instanceof
Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;  // redundant cast
    System.out.println(s.length());
}

// Java 16+: pattern variable
if (obj instanceof String s) {
    System.out.println(s.length());  // s is in scope, no cast
}

// The variable is scoped by the pattern's truth
if (obj instanceof String s && s.length() > 3) {
    System.out.println("Long string: " + s);
}
// s is NOT in scope here if the && short-circuited

// Negation scoping
if (!(obj instanceof String s)) {
    // s NOT in scope here
    return;
}
// s IS in scope here (the instanceof must have been true)

// Combining with other patterns
Object data = 42;
String desc = switch (data) {
    case Integer i when i > 0 -> "positive int: " + i;
    case Integer i -> "non-positive int: " + i;
    case String s -> "string: " + s;
    case null -> "null";
    default -> "other";
};

Switch Pattern Matching (Java 21)

Switch Pattern Matching (Java 21, final) erlaubt das Switchen auf Typen, das Destrukturieren von Records und das Hinzufügen von Guards (when). Kombiniert mit Sealed Types verifiziert der Compiler Erschöpflichkeit — kein default nötig, wenn alle Subtypen abgedeckt sind. Record Patterns (case Point(int x, int y)) destrukturieren in einem Schritt. null-case ist explizit (keine NPE). Das bringt Java nah an ML/Scalas Pattern Matching für deklaratives Modellieren von Domains.

java
// Java 21: switch pattern matching with type patterns and guards
sealed interface Event permits Login, Logout, Message, Error {}
record Login(String user) implements Event {}
record Logout(String user) implements Event {}
record Message(String from, String text) implements Event {}
record Error(int code, String detail) implements Event {}

String describe(Event e) {
    return switch (e) {
        case Login l -> l.user() + " logged in";
        case Logout l -> l.user() + " logged out";
        case Message m when m.from().equals("system") -> "[system] " + m.text();
        case Message m -> m.from() + ": " + m.text();
        case Error(int code, String detail) when code >= 500 -> "SERVER ERROR " + code;
        case Error(int code, String detail) -> "error " + code + ": " + detail;
        case null -> "no event";  // explicit null handling
    };
}

// Record patterns: destructure records in one go
record Point(int x, int y) {}
String classify(Object o) {
    return switch (o) {
        case Point(int x, int y) when x == y -> "diagonal";
        case Point(int x, int y) -> "point at (" + x + "," + y + ")";
        default -> "not a point";
    };
}

// Nested patterns
record Box(Point p) {}
String describe2(Object o) {
    return switch (o) {
        case Box(Point(int x, int y)) -> "box at " + x + "," + y;
        default -> "unknown";
    };
}
18

Module (JPMS)

module-info.java Grundlagen

module-info.java deklariert ein Modul (Java 9+ JPMS). requires fügt eine Abhängigkeit hinzu; exports macht Packages zugänglich; opens erlaubt Reflective Access (benötigt von Serialisierungs-/DI-Frameworks); uses/provides verdrahten ServiceLoader. Ohne diese Datei lebt Code auf dem Classpath als 'unnamed module' mit Legacy-Verhalten. Module bieten starke Kapselung (nur exportierte Packages sind public) und zuverlässige Konfiguration (explizite Abhängigkeiten).

java
// File: src/com.example.app/module-info.java
module com.example.app {
    requires java.sql;           // depends on java.sql module
    requires transitive com.example.lib;  // re-export (consumers see it too)
    requires static java.annotation;  // compile-time only (optional at runtime)

    exports com.example.app.api;       // public API visible to all
    exports com.example.app.internal to com.example.test;  // qualified export

    opens com.example.app.model to com.fasterxml.jackson.databind;  // reflection only
    opens com.example.app.dynamic;  // open to all for reflection

    uses com.example.app.spi.Plugin;  // service consumer
    provides com.example.app.spi.Plugin with com.example.app.plugins.DefaultPlugin;  // service provider
}

// Key directives:
// requires: depends on another module
// exports: makes packages public to other modules
// opens: allows deep reflection (for frameworks like Jackson, Hibernate)
// uses/provides: service loader integration

// A module without module-info.java is an "unnamed module" (classpath behavior)

requires, exports, opens

requires deklariert eine Abhängigkeit; requires transitive propagiert sie (verwenden, wenn die öffentliche API Typen aus diesem Modul exponiert). exports macht Packages public; exports to beschränkt auf benannte Module (qualified exports). opens gewährt Reflective Access (Deep Reflection) — essenziell für Frameworks, die setAccessible(true) aufrufen. Die Unterscheidung zwischen exports (öffentliche API) und opens (Reflection) ist Schlüssel: starke Kapselung ist der Default, und man opt-in pro Package.

java
// Module A: com.example.library
module com.example.library {
    // Public API anyone can use
    exports com.example.library.api;

    // Internal package: only visible to specific modules
    exports com.example.library.internal to com.example.app;

    // Allow reflection for frameworks (Jackson, JPA)
    opens com.example.library.model;

    // Only specific module can reflect
    opens com.example.library.config to com.example.app;

    // Dependencies
    requires java.logging;
    requires transitive java.sql;  // consumers of A also get java.sql
}

// Module B: com.example.app (consumer)
module com.example.app {
    requires com.example.library;  // use A's exported packages
    // Note: transitive means java.sql is also available here

    requires com.fasterxml.jackson.databind;
}

// 'requires transitive X' means: any module requiring this one
// also reads X. Use when your exported API exposes X's types.

// 'opens' vs 'exports':
// exports: compile-time + runtime access to public members
// opens: runtime reflective access to ALL members (including private)

// Reflective access without 'opens' fails with InaccessibleObjectException
// in Java 16+ (strong encapsulation enforced by default).

ServiceLoader & Services

ServiceLoader implementiert das SPI-Pattern: ein Interface in einem Modul, Implementierungen zur Laufzeit entdeckt. Das API-Modul exportiert das Interface; Provider-Module deklarieren `provides X with Y`; Konsumenten-Module deklarieren `uses X`. ServiceLoader.load(X.class) findet alle Provider auf dem Module Path. Das entkoppelt Interface von Implementierung — JDBC-Treiber, Logging-Backends (SLF4J) und Charset-Provider funktionieren alle so. Keine Compile-Zeit-Abhängigkeit von der Implementierung.

java
// SPI (Service Provider Interface) pattern with modules

// 1. Define the service interface in an API module
module com.example.spi {
    exports com.example.spi;
}
package com.example.spi;
public interface Plugin {
    String name();
    void run();
}

// 2. Implement in a provider module
module com.example.plugin.impl {
    requires com.example.spi;
    provides com.example.spi.Plugin with com.example.plugin.impl.DefaultPlugin;
}
package com.example.plugin.impl;
import com.example.spi.Plugin;
public class DefaultPlugin implements Plugin {
    public String name() { return "default"; }
    public void run() { System.out.println("running"); }
}

// 3. Consume in an app module
module com.example.app {
    requires com.example.spi;
    uses com.example.spi.Plugin;  // declares intent to load services
}

// Loading services at runtime
import java.util.ServiceLoader;
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) {
    System.out.println("Found: " + p.name());
    p.run();
}

// ServiceLoader is how JDBC drivers, SLF4J backends, and many
// plugin systems are discovered without compile-time dependencies.

Module Path vs. Classpath

Der Module Path (--module-path) hält modulare JARs mit starker Kapselung; der Classpath (-cp) hält Legacy-JARs als unnamed Modules ohne Kapselung. Modulare JARs funktionieren auf beiden. Automatic Modules sind JARs ohne module-info auf dem Module Path — ihr Name stammt vom Dateinamen oder dem Automatic-Module-Name-Manifest-Attribut. --add-opens ist ein Escape-Hatch für Legacy-Reflection, die unter starker Kapselung bricht.

java
// Compile a module
//   javac -d out/com.example.app \
//         --module-source-path src \
//         --module com.example.app

// Run a modular app
//   java --module-path out --module com.example.app/com.example.app.Main

// Package as a modular JAR (includes module-info.class)
//   jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app .

// Module path vs classpath:
// --module-path: modules with module-info, strong encapsulation enforced
// --class-path (or -cp): legacy "unnamed module", everything public, no encapsulation

// Mixing: modular JARs can be used on the classpath too (automatic module)
//   java -cp lib/app.jar:lib/dep.jar com.example.app.Main
// An automatic module: a JAR without module-info on the module path.
// Its name is derived from the JAR filename (Automatic-Module-Name in MANIFEST.MF
// gives an explicit name).

// Inspect a module JAR
//   jar --describe-module --file app.jar

// List observable modules
//   java --list-modules
//   java --describe-module java.sql

// Add opens at runtime for legacy reflection (escape hatch)
//   java --add-opens com.example.app/com.example.app.internal=ALL-UNNAMED

jdeps & jlink (Custom Runtimes)

jdeps analysiert Bytecode, um Modulabhängigkeiten aufzulisten — nützlich für die Migration zu Modulen und das Finden ungenutzter Abhängigkeiten. jlink erstellt eine benutzerdefinierte JRE, die nur die Module enthält, die die App benötigt, und erzeugt eine eigenständige, kleinere, schneller startende Runtime. Ideal für Docker-Images und Installer: die App plus eine 30-50MB JRE ausliefern statt eine 300MB JDK-Installation vorauszusetzen. Zusammen ermöglichen jdeps + jlink schlanke, eigenständige Java-Deployments.

java
# jdeps: analyze dependencies (find unused, list required modules)

# List dependencies of a JAR
jdeps --module-path lib app.jar

# Generate module-info.java for an existing JAR
jdeps --generate-module-info ./out app.jar

# Show which JDK modules a JAR uses
jdeps --print-module-deps --ignore-missing-deps app.jar
# Output: java.base,java.logging,java.sql

# jlink: create a custom stripped-down JRE containing only needed modules
jlink \
  --module-path "$JAVA_HOME/jmods:./out" \
  --add-modules com.example.app \
  --output ./custom-jre \
  --strip-debug \
  --compress=zip-6 \
  --no-header-files \
  --no-man-pages \
  --launcher app=com.example.app/com.example.app.Main

# The custom JRE is self-contained:
#   ./custom-jre/bin/app   # launches the app
#   ./custom-jre/bin/java  # the stripped JVM

# Benefits of jlink:
# - Smaller distribution (only needed modules)
# - Faster startup (less to load)
# - No need to install Java on target machine
# - Can cross-target (different OS/arch) with matching jmods

# Common workflow:
# 1. jdeps to find required modules
# 2. jlink to build a custom runtime
# 3. Package app + custom JRE together (Docker image, installer)
19

Concurrency Deep Dive

Locks: ReentrantLock & ReadWriteLock

ReentrantLock bietet mehr Kontrolle als synchronized: tryLock (nicht-blockierend/zeitlich), Fairness, Unterbrechbarkeit und Lock-Zustands-Inspektion. Immer in finally entsperren. ReadWriteLock erlaubt viele gleichzeitige Leser, aber exklusive Schreiber — großartig für Lese-intensive Caches. StampedLock (Java 8+) fügt optimistische Reads für noch besseren Read-Throughput hinzu. synchronized für einfache Fälle bevorzugen; Lock verwenden, wenn dessen erweiterte Features benötigt werden.

java
import java.util.concurrent.locks.*;

// ReentrantLock: more flexible than synchronized
class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // MUST be in finally
        }
    }

    public int get() {
        lock.lock();
        try { return count; }
        finally { lock.unlock(); }
    }
}

// tryLock with timeout (avoids deadlock-induced hangs)
if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
    try { /* work */ } finally { lock.unlock(); }
}

// Fair lock (FIFO ordering, slower)
ReentrantLock fair = new ReentrantLock(true);

// ReadWriteLock: many readers OR one writer
class Cache {
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private final Lock read = rw.readLock();
    private final Lock write = rw.writeLock();
    private java.util.Map<String, String> data = new java.util.HashMap<>();

    public String get(String key) {
        read.lock();
        try { return data.get(key); }
        finally { read.unlock(); }
    }
    public void put(String key, String val) {
        write.lock();
        try { data.put(key, val); }
        finally { write.unlock(); }
    }
}

Concurrent Collections

ConcurrentHashMap ist die Arbeitstier-Thread-sichere Map — compute/merge für atomare Updates verwenden statt Check-then-Act. CopyOnWriteArrayList ist am besten für Lese-intensive, Selten-schreibende Listen (Event-Listener) — Writes kopieren das Array. BlockingQueue ist das Rückgrat von Producer-Consumer-Pipelines (put blockiert, wenn voll, take blockiert, wenn leer). ConcurrentLinkedQueue ist nicht-blockierend und unbegrenzt. Diese ersetzen synchronized-Wrapper (Collections.synchronizedX), die grobkörnig und langsamer sind.

java
import java.util.concurrent.*;
import java.util.*;

// ConcurrentHashMap: thread-safe HashMap (no null keys/values)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.putIfAbsent("a", 2);  // only if absent
map.compute("a", (k, v) -> v == null ? 1 : v + 1);  // atomic update
map.merge("a", 1, Integer::sum);  // add 1 atomically
Integer val = map.getOrDefault("a", 0);

// CopyOnWriteArrayList: snapshot semantics, fast reads, slow writes
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("x");  // copies entire array
for (String l : listeners) { /* safe iteration, no ConcurrentModificationException */ }

// BlockingQueue: producer-consumer pattern
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put("task");        // blocks if full
boolean added = queue.offer("task", 1, TimeUnit.SECONDS);  // timed
// Consumer
String task = queue.take();  // blocks if empty
String polled = queue.poll(1, TimeUnit.SECONDS);

// ConcurrentLinkedQueue: non-blocking, unbounded
Queue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a");

// SkipListMap / SkipListSet: concurrent sorted collections
ConcurrentNavigableMap<Integer, String> sorted = new ConcurrentSkipListMap<>();

import java.util.concurrent.TimeUnit;

CountDownLatch & CyclicBarrier

CountDownLatch ist ein Einweg-Gate — N Threads zählen herunter, andere warten; nicht zurücksetzbar. CyclicBarrier ist wiederverwendbar — Threads warten aufeinander an einem Rendezvous-Punkt, mit einer optionalen Aktion, wenn alle ankommen. Phaser ist am flexibelsten: variable Parties, mehrere Phasen, Baumstruktur. Latch für Startup-Koordination, Barrier für parallele mehrstufige Algorithmen, Phaser für dynamische Teilnehmeranzahlen verwenden.

java
import java.util.concurrent.*;
import java.util.*;

// CountDownLatch: one-shot gate, N threads must arrive before proceeding
CountDownLatch ready = new CountDownLatch(3);
List<String> results = Collections.synchronizedList(new ArrayList<>());

for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try { Thread.sleep(id * 100); } catch (InterruptedException e) {}
        results.add("worker-" + id);
        ready.countDown();  // signal done
    }).start();
}
ready.await();  // main thread blocks until count reaches 0
System.out.println("All done: " + results);

// CyclicBarrier: reusable barrier, threads wait for each other
CyclicBarrier barrier = new CyclicBarrier(3, () ->
    System.out.println("--- phase complete ---"));

Runnable worker = () -> {
    try {
        System.out.println(Thread.currentThread().getName() + " phase 1");
        barrier.await();  // wait for all 3
        System.out.println(Thread.currentThread().getName() + " phase 2");
        barrier.await();  // reusable for next phase
    } catch (Exception e) {}
};
for (int i = 0; i < 3; i++) new Thread(worker).start();

// Phaser: more flexible (variable parties, multiple phases)
Phaser phaser = new Phaser(3);
phaser.register();  // dynamically add a party
phaser.arriveAndAwaitAdvance();

Semaphore & Exchanger

Semaphore steuert den Zugriff auf N Erlaubnisse — acquire blockiert, bis eines verfügbar ist, release gibt es zurück. Für Rate-Limiting, Connection-Pools oder jedes Szenario mit begrenzten Ressourcen verwenden. tryAcquire bietet nicht-blockierende und zeitliche Varianten. Exchanger lässt zwei Threads Werte an einem Rendezvous-Punkt tauschen — nischig, aber elegant für Pipeline-Designs, wo zwei Threads Buffer tauschen. Beide sind in java.util.concurrent und Low-Level-er als Locks für einige Koordinations-Patterns.

java
import java.util.concurrent.*;

// Semaphore: limit concurrent access to N permits
Semaphore pool = new Semaphore(5);  // 5 concurrent allowed

pool.acquire();  // blocks until a permit is available
try {
    // critical section (at most 5 threads here at once)
    System.out.println("Working, permits left: " + pool.availablePermits());
} finally {
    pool.release();  // return the permit
}

// Try-acquire (non-blocking)
if (pool.tryAcquire()) {
    try { /* work */ } finally { pool.release(); }
} else {
    System.out.println("Too busy, try later");
}

// Timed acquire
if (pool.tryAcquire(1, TimeUnit.SECONDS)) {
    try { /* work */ } finally { pool.release(); }
}

// Use case: rate limiting, connection pools, parking lots

// Exchanger: two threads swap values
Exchanger<String> ex = new Exchanger<>();
new Thread(() -> {
    try {
        String got = ex.exchange("from-A");  // gives "from-A", receives "from-B"
        System.out.println("A got: " + got);
    } catch (InterruptedException e) {}
}).start();
String got = ex.exchange("from-B");  // gives "from-B", receives "from-A"
System.out.println("B got: " + got);

import java.util.concurrent.TimeUnit;

CompletableFuture Advanced

CompletableFuture ist Javas Promise — asynchrone Arbeit komponieren mit thenApply (map), thenCompose (flatMap), thenCombine (zip von zwei). allOf wartet auf alle, anyOf auf das erste. exceptionally erholt sich von Fehlern; handle deckt beide ab. orTimeout (Java 9+) bricht ab, wenn es zu lange dauert. Immer einen expliziten Executor übergeben — der Default-commonPool kann unter blockierender Arbeit hungern. Dies ist die Grundlage von reaktivem asynchronem Code in Java.

java
import java.util.concurrent.*;
import java.util.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

// Async composition (like JS Promises)
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "hello", pool)            // async supplier
    .thenApply(String::toUpperCase)              // transform
    .thenCompose(s -> CompletableFuture.supplyAsync(  // flatMap
        () -> s + " world"))
    .thenApply(s -> s + "!");

System.out.println(future.join());  // "HELLO world!"

// Combine two independent futures
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> sum = a.thenCombine(b, Integer::sum);
System.out.println(sum.join());  // 30

// Run multiple and wait for all (or any)
List<CompletableFuture<Integer>> futures = List.of(
    CompletableFuture.supplyAsync(() -> 1, pool),
    CompletableFuture.supplyAsync(() -> 2, pool),
    CompletableFuture.supplyAsync(() -> 3, pool)
);
CompletableFuture<Void> all = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0]));
all.join();
List<Integer> results = futures.stream().map(CompletableFuture::join).toList();

// Error handling
CompletableFuture<String> safe = CompletableFuture
    .supplyAsync(() -> { throw new RuntimeException("boom"); })
    .exceptionally(ex -> "fallback: " + ex.getMessage());
System.out.println(safe.join());  // "fallback: boom"

// handle: both success and failure
CompletableFuture<String> handled = CompletableFuture
    .supplyAsync(() -> "ok")
    .handle((val, ex) -> ex == null ? val : "error");

// Timeout (Java 9+)
CompletableFuture<String> timed = CompletableFuture
    .supplyAsync(() -> { Thread.sleep(5000); return "slow"; })
    .orTimeout(1, TimeUnit.SECONDS)
    .exceptionally(ex -> "timed out");

Virtual Threads (Java 21)

Virtual Threads (Java 21) sind Lightweight-Threads, die von der JVM auf einem kleinen Pool von Carrier-(Platform-)Threads geplant werden. Wenn ein Virtual Thread auf I/O blockiert, wird er suspendiert und der Carrier führt einen anderen aus — sodass man Millionen gleichzeitiger blockierender Operationen haben kann. Dies lässt dich einfachen blockierenden Code statt komplexer reaktiver/async-Chains schreiben. Für I/O-gebundene Workloads (HTTP-Handler, DB-Aufrufe) verwenden; für CPU-gebundene Arbeit sind Platform Threads oder parallelStream noch angemessen.

java
import java.util.concurrent.*;
import java.util.*;

// Virtual threads: lightweight, cheap, millions possible
// Java 21 LTS feature — "Project Loom"

// Start a virtual thread
Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Running on: " + Thread);
});

// Builder pattern
Thread vt2 = Thread.ofVirtual().name("worker-1").start(() -> {
    // blocking I/O here is fine — virtual thread yields, not the OS thread
});

// Per-thread factory
ThreadFactory factory = Thread.ofVirtual().factory();

// ExecutorService for virtual threads (Java 21+)
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
    // Submit a million tasks — each gets its own virtual thread
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 1_000_000; i++) {
        final int id = i;
        futures.add(es.submit(() -> {
            Thread.sleep(100);  // blocking call is cheap on virtual threads
            return "done-" + id;
        }));
    }
    // virtual threads yield when blocking, so 1M concurrent is feasible
}

// Why virtual threads?
// - Platform threads (OS threads) are heavy (~1MB stack, kernel scheduling)
// - Virtual threads are user-mode, ~KB, scheduled by JVM on a small carrier pool
// - Blocking I/O on a virtual thread doesn't block a platform thread
// - Lets you write straightforward blocking code at scale (no reactive complexity)

// Best practice: use virtual threads for I/O-bound work,
// NOT for CPU-bound work (use platform threads / parallelStream).
20

Collections Framework Deep Dive

Comparators & Sortierung

Comparator.comparing(keyExtractor) baut Comparatoren aus einer Schlüssel-Funktion — deutlich sauberer als das Schreiben roher Compare-Logik. thenComparing verkettet sekundäre Sortierschlüssel. nullsFirst/nullsLast behandelt nulls sicher. comparingInt/comparingLong/comparingDouble verwenden, um Autoboxing zu vermeiden. List.sort() sortiert in-place (nur veränderliche Listen); Stream.sorted() gibt einen neuen sortierten Stream zurück. Comparators treiben Sortierung, TreeSet/TreeMap-Reihenfolge und Stream-Operationen an.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Carol", 30),
    new Person("Dave", 25)
);

// Comparator.comparing (key extractor)
people.stream().sorted(Comparator.comparing(Person::name)).toList();
people.stream().sorted(Comparator.comparingInt(Person::age)).toList();

// Reversed
people.stream().sorted(Comparator.comparing(Person::age).reversed()).toList();

// Chained (thenComparing): sort by age, then by name
List<Person> sorted = people.stream().sorted(
    Comparator.comparingInt(Person::age)
              .thenComparing(Person::name)
).toList();
// [Bob(25), Dave(25), Alice(30), Carol(30)]

// Nulls handling
Comparator<String> cmp = Comparator.nullsFirst(Comparator.naturalOrder());
List.of("b", null, "a").stream().sorted(cmp).toList();  // [null, a, b]

// Custom comparator
Comparator<Person> byNameLen = (a, b) -> a.name().length() - b.name().length();

// Mutable list sort
List<String> names = new ArrayList<>(List.of("charlie", "alice", "bob"));
names.sort(Comparator.naturalOrder());
// names = [alice, bob, charlie]

// Comparing with primitive specializations avoids boxing
Comparator<Person> byAge = Comparator.comparingInt(Person::age);

Unmodifiable & Immutable Collections

List.of/Set.of/Map.of (Java 9+) erstellen wirklich unveränderliche Collections — keine nulls, keine Mutationen. Collections.unmodifiableX erstellt eine nur-Lese-Ansicht, die weiterhin Änderungen der zugrunde liegenden Collection reflektiert. List.copyOf (Java 10+) macht eine unabhängige unveränderliche Kopie. Arrays.asList ist eine größenfixierte Ansicht eines Arrays (set funktioniert, add/remove nicht). Je nach Bedarf wählen: Immutable Factories für Konstanten, Unmodifiable-Ansichten, um Interna sicher zu exponieren, copyOf für defensive Kopien.

java
import java.util.*;

// Java 9+ immutable factories (List.of, Set.of, Map.of)
List<String> immutable = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("x", 1), Map.entry("y", 2), Map.entry("z", 3)
);
// immutable.add("d");  // throws UnsupportedOperationException
// immutable.set(0, "z");  // throws
// nulls not allowed in these immutable collections

// Unmodifiable view (wraps an existing collection)
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(mutable);
// view.add("c");  // throws
mutable.add("c");  // but changes to backing list ARE visible in view
System.out.println(view);  // [a, b, c]

// CopyOf (Java 10+): creates immutable copy
List<String> copy = List.copyOf(mutable);  // independent immutable copy

// Arrays.asList: fixed-size view of an array
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "x");  // OK (writes through to array)
// fixed.add("c");  // throws (size fixed)

// To make a truly mutable copy:
List<String> mut = new ArrayList<>(Arrays.asList(arr));

Queue & Deque Implementierungen

ArrayDeque ist die bevorzugte Stack- und Queue-Implementierung — schneller als die veraltete Stack-Klasse (die synchronisiert ist) und LinkedList. PriorityQueue ordnet Elemente nach einem Comparator (standardmäßig Min-Heap) — für Scheduling, Top-K-Probleme verwenden. Deque unterstützt beide Enden; addFirst/removeFirst für Stack-Semantik, addLast/removeFirst für Queue-Semantik verwenden. Für gleichzeitige Queues die java.util.concurrent-Implementierungen verwenden (LinkedBlockingQueue usw.).

java
import java.util.*;

// Queue: FIFO (offer/poll/peek)
Queue<String> queue = new LinkedList<>();
queue.offer("a"); queue.offer("b");
System.out.println(queue.peek());  // "a" (head)
System.out.println(queue.poll());  // "a" (remove head)

// Deque: double-ended (add/remove at both ends)
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("a"); deque.addLast("b");
System.out.println(deque.peekFirst());  // "a"
System.out.println(deque.peekLast());   // "b"
deque.removeFirst(); deque.removeLast();

// ArrayDeque as a stack (push/pop/peek)
Deque<String> stack = new ArrayDeque<>();
stack.push("first");  // addFirst
stack.push("second");
System.out.println(stack.pop());  // "second" (LIFO)
System.out.println(stack.peek()); // "first"

// PriorityQueue: orders by Comparator (not insertion order)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
System.out.println(pq.poll());  // 1 (smallest first)
System.out.println(pq.poll());  // 3

// With custom comparator
PriorityQueue<String> byLen = new PriorityQueue<>(Comparator.comparingInt(String::length));
byLen.offer("aaa"); byLen.offer("a"); byLen.offer("aa");
System.out.println(byLen.poll());  // "a"

// BlockingQueue implementations (concurrency): see concurrency-deep section
// ArrayDeque is faster than Stack/LinkedList for stack/queue use

Map merge, compute, getOrDefault

Diese Map-Methoden machen häufige Patterns atomar und prägnant. merge ist das Wortzähl-Idiom — bestehenden und neuen Wert kombinieren, den Eintrag entfernen, wenn die Funktion null zurückgibt. computeIfAbsent ist das Lazy-Cache-Pattern (Memoisierung). getOrDefault vermeidet null-Checks. replaceAll transformiert alle Werte. Diese sind deutlich sauberer als der Check-then-Act get/put-Tanz, und sie sind die Bausteine für ConcurrentHashMaps atomare Updates in nebenläufigem Code.

java
import java.util.*;

Map<String, Integer> counts = new HashMap<>();

// getOrDefault: safe read with default
int n = counts.getOrDefault("missing", 0);  // 0, no null

// putIfAbsent: only set if not present
counts.putIfAbsent("a", 1);  // sets to 1
counts.putIfAbsent("a", 2);  // no change (already present)

// compute: recompute value for a key
counts.compute("a", (k, v) -> v == null ? 1 : v + 1);  // increment

// computeIfAbsent: lazy initialization (cache pattern)
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("key", k -> new ArrayList<>()).add("item");
// creates list if absent, then adds — atomic, no race in single thread

// computeIfPresent: update only if present
counts.computeIfPresent("a", (k, v) -> v > 0 ? v - 1 : null);  // decrement, remove at 0

// merge: combine existing and new value (great for counting)
counts.merge("word", 1, Integer::sum);  // word count pattern
counts.merge("word", 1, Integer::sum);  // now 2
counts.merge("word", 1, Integer::sum);  // now 3

// merge with removal: function returns null -> entry removed
counts.merge("word", 1, (old, v) -> old > 1 ? old - 1 : null);

// replaceAll: transform all values
Map<String, Integer> doubled = new HashMap<>(counts);
doubled.replaceAll((k, v) -> v * 2);

// Word frequency counter (idiomatic)
String text = "the cat the dog the bird";
Map<String, Integer> freq = new HashMap<>();
for (String w : text.split(" ")) {
    freq.merge(w, 1, Integer::sum);
}

Collections Utility-Methoden

Collections.* bietet klassische Utilities: sort, binarySearch (erfordert sortierte Eingabe), shuffle, reverse, frequency, min/max. singleton/empty-Factories geben unveränderliche Single-Element- oder leere Collections zurück — emptyList() gegenüber null zurückgeben bevorzugen. nCopies ist speichereffizient (ein Element geteilt). Synchronized-Wrapper existieren für Legacy-Code, aber java.util.concurrent-Collections bevorzugen. Checked-Wrapper fangen Generic-Typ-Verletzungen zur Laufzeit ab, nützlich für Interop mit Raw Types.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));

// Sorting
Collections.sort(nums);                    // in-place, natural order
Collections.sort(nums, Comparator.reverseOrder());
nums.sort(Comparator.naturalOrder());      // List.sort (preferred)

// Searching (list must be sorted first)
int idx = Collections.binarySearch(nums, 4);  // index, or negative if absent

// Shuffling & reversing
Collections.shuffle(nums);
Collections.reverse(nums);

// Frequency & disjoint
int freq = Collections.frequency(nums, 1);  // count occurrences
boolean dis = Collections.disjoint(List.of(1, 2), List.of(3, 4));  // true

// Min/max
int min = Collections.min(nums);
int max = Collections.max(nums, Comparator.reverseOrder());

// Singleton collections (immutable, single element)
Set<String> one = Collections.singleton("only");
List<Integer> oneList = Collections.singletonList(42);
Map<String, Integer> oneMap = Collections.singletonMap("k", 1);

// Empty collections (prefer over returning null)
List<Object> empty = Collections.emptyList();
Set<Object> emptySet = Collections.emptySet();

// nCopies (immutable list of n copies)
List<String> padding = Collections.nCopies(5, "x");  // [x,x,x,x,x]

// Synchronized wrappers (legacy — prefer concurrent collections)
List<String> sync = Collections.synchronizedList(new ArrayList<>());

// Checked wrappers (catch heap pollution at runtime)
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
// checked.add(123);  // throws ClassCastException at the add site
21

Testing (JUnit 5 & Mockito)

JUnit 5 Grundlagen

JUnit 5 (Jupiter)-Annotationen: @Test markiert einen Test; @BeforeEach/@AfterEach laufen um jeden Test; @BeforeAll/@AfterAll laufen einmal für die Klasse (müssen static sein). @DisplayName passt Testnamen an. @Disabled überspringt Tests. assertThrows verifiziert Exceptions. Tests sollten unabhängig sein — @BeforeEach zum Zurücksetzen des Zustands verwenden, nicht statische Fields. JUnit 5 lebt in org.junit.jupiter.api (unterschiedlich von JUnit 4s org.junit).

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    private Calculator calc;

    @BeforeAll
    static void setUpOnce() {
        // runs once before all tests (must be static)
        System.out.println("Starting CalculatorTest");
    }

    @BeforeEach
    void setUp() {
        // runs before each test
        calc = new Calculator();
    }

    @Test
    @DisplayName("2 + 2 should equal 4")
    void addsTwoNumbers() {
        assertEquals(4, calc.add(2, 2));
    }

    @Test
    void dividesByZeroThrows() {
        ArithmeticException ex = assertThrows(
            ArithmeticException.class,
            () -> calc.divide(1, 0)
        );
        assertEquals("Division by zero", ex.getMessage());
    }

    @AfterEach
    void tearDown() {
        // runs after each test (cleanup)
        calc = null;
    }

    @AfterAll
    static void tearDownOnce() {
        // runs once after all tests
    }

    @Disabled("until bug #42 is fixed")
    @Test
    void skippedTest() {}
}

class Calculator {
    int add(int a, int b) { return a + b; }
    int divide(int a, int b) {
        if (b == 0) throw new ArithmeticException("Division by zero");
        return a / b;
    }
}

Assertions

JUnit 5-Assertions: assertEquals/assertNotEquals, assertTrue/False, assertNull/NotNull, assertSame (Identität). assertAll gruppiert Checks, sodass alle laufen, auch wenn einige fehlschlagen. assertTimeout lässt langsame Tests fehlschlagen. Messages können Strings oder Suppliers sein (lazy — nur bei Fehlern gebaut, vermeidet String-Konkatenation, wenn Tests bestehen). assertIterableEquals vergleicht geordnete Collections. Diese stammen aus org.junit.jupiter.api.Assertions.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsTest {

    @Test
    void equality() {
        assertEquals(4, 2 + 2);
        assertEquals(4, 2 + 2, "math is broken");
        assertNotEquals(5, 2 + 2);
    }

    @Test
    void booleans() {
        assertTrue(5 > 3);
        assertFalse(5 < 3, "5 should not be less than 3");
    }

    @Test
    void nullness() {
        assertNull(null);
        assertNotNull(new Object());
    }

    @Test
    void sameInstance() {
        String a = "x";
        assertSame(a, a);          // ==
        assertNotSame(a, new String("x"));
    }

    @Test
    void collections() {
        assertIterableEquals(List.of(1, 2, 3), List.of(1, 2, 3));
        assertLinesMatch(List.of("a.*", "b"), List.of("abc", "b"));
    }

    @Test
    void groupedAssertions() {
        // All run even if one fails — reports all failures
        assertAll("person",
            () -> assertEquals("Alice", "Alice"),
            () -> assertEquals(30, 30),
            () -> assertNotNull("x")
        );
    }

    @Test
    void timeout() {
        // Fails if it takes longer than 100ms
        assertTimeout(Duration.ofMillis(100), () -> {
            Thread.sleep(10);
        });
        // assertTimeoutPreemptively: stops the task early (in another thread)
    }

    @Test
    void customMessage() {
        int result = 5;
        assertEquals(4, result, () -> "expected 4 but got " + result);
        // Supplier<String> — message built lazily only on failure
    }

    import java.time.Duration;
    import java.util.List;
}

Parametrisierte Tests

Parametrisierte Tests führen dieselbe Test-Logik mit mehreren Eingaben aus. @ValueSource stellt ein einzelnes Argument-Array bereit. @CsvSource mappt CSV-Zeilen auf mehrere Parameter. @MethodSource (am flexibelsten) verwendet einen statischen Stream<Arguments>. @EnumSource iteriert Enum-Werte. @NullAndEmptySource fügt null/leer-Fälle hinzu. Dies eliminiert Copy-Paste-Testmethoden und macht datengetriebenes Testing sauber. Display-Namen inkludieren automatisch die Parameter für einfache Diagnose.

java
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class ParameterizedTests {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void positiveNumbersArePositive(int n) {
        assertTrue(n > 0);
    }

    @ParameterizedTest
    @ValueSource(strings = {"", "  ", "\t"})
    void blankStrings(String s) {
        assertTrue(s.isBlank());
    }

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {"  ", "\t"})
    void blankOrNull(String s) {
        assertTrue(s == null || s.isBlank());
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, -5, 5"
    })
    void addition(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/testdata.csv", numLinesToSkip = 1)
    void fromCsv(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @MethodSource("additionProvider")
    void fromMethod(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }
    static java.util.stream.Stream<Arguments> additionProvider() {
        return java.util.stream.Stream.of(
            Arguments.of(1, 1, 2),
            Arguments.of(2, 3, 5)
        );
    }

    @ParameterizedTest
    @EnumSource(TimeUnit.class)
    void allEnums(TimeUnit unit) {
        assertNotNull(unit);
    }
}

Lifecycle, Nested & Conditional

@Nested erstellt innere Testklassen, die Lifecycle teilen — großartig für BDD-artige 'when X then Y'-Strukturen, wo äußeres Setup auf innere Tests zutrifft. Conditional-Annotationen (@EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable) überspringen Tests basierend auf der Umgebung. @Tag gruppiert Tests für selektive Ausführung (z.B. schnell vs. langsam, Unit vs. Integration). Nested-Klassen können kein @BeforeAll haben (sie sind nicht-statisch). Diese Features machen Test-Organisation ausdrucksstark.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.condition.*;

class LifecycleTest {

    @Test
    void topLevel() {}

    @Nested
    @DisplayName("when stack is empty")
    class WhenEmpty {
        @BeforeEach
        void setUp() { /* stack is empty here */ }

        @Test
        void throwsOnPop() {
            assertThrows(Exception.class, () -> {});
        }

        @Nested
        @DisplayName("after pushing one element")
        class AfterPush {
            @BeforeEach
            void push() { /* push one */ }

            @Test
            void popReturnsElement() {
                // ...
            }
        }
    }

    // Conditional execution
    @Test
    @EnabledOnOs(OS.LINUX)
    void onlyOnLinux() {}

    @Test
    @EnabledIfSystemProperty(named = "env", matches = "ci")
    void onlyInCi() {}

    @Test
    @EnabledIfEnvironmentVariable(named = "DATABASE", matches = "postgres")
    void onlyWithPostgres() {}

    @Test
    @DisabledIf("customCondition")
    void conditional() {}
    static boolean customCondition() { return java.time.LocalTime.now().getHour() < 9; }

    // Tagging for selective runs
    @Test
    @Tag("slow")
    void slowIntegrationTest() {}

    @Test
    @Tag("fast")
    void fastUnitTest() {}

    // Run only fast: mvn test -Dgroups=fast
    import org.junit.jupiter.api.condition.OS;
    import java.util.concurrent.TimeUnit;

Mockito Mocking

Mockito erstellt Test-Doubles für Abhängigkeiten. @Mock erstellt einen Mock; @InjectMocks baut ein echtes Objekt mit injizierten Mocks. when(...).thenReturn(...) stubbt Rückgabewerte; verify(...) prüft Interaktionen. Argument-Matcher (any(), eq(), argThat()) matchen Aufrufe flexibel. Spies wickeln echte Objekte (partielles Mocking). Das Arrange-Act-Assert-Pattern hält Tests lesbar. Mocking isoliert die zu testende Einheit von ihren Abhängigkeiten (Datenbank, Netzwerk, Zeit).

java
import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    UserRepository repo;  // mock dependency

    @InjectMocks
    UserService service;  // real service with mocks injected

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void returnsUserWhenFound() {
        // Arrange: stub the mock
        User alice = new User(1, "Alice");
        when(repo.findById(1)).thenReturn(alice);

        // Act
        String name = service.getUserName(1);

        // Assert
        assertEquals("Alice", name);
        verify(repo).findById(1);       // called once
        verify(repo, never()).findById(2);
    }

    @Test
    void throwsWhenNotFound() {
        when(repo.findById(99)).thenReturn(null);
        // or: when(repo.findById(99)).thenThrow(new RuntimeException());

        assertThrows(RuntimeException.class, () -> service.getUserName(99));
    }

    @Test
    void argumentMatchers() {
        when(repo.findById(anyInt())).thenReturn(new User(0, "default"));
        // matchers: eq(), any(), anyInt(), contains(), argThat()

        service.getUserName(42);
        verify(repo).findById(intThat(n -> n > 0));
    }

    @Test
    void verifyInteractionDetails() {
        service.getUserName(1);
        verify(repo, times(1)).findById(1);
        verify(repo, atLeastOnce()).findById(anyInt());
        verifyNoMoreInteractions(repo);
    }

    @Test
    void spy_partialMock() {
        List<String> spy = spy(new ArrayList<>());
        spy.add("real");
        when(spy.size()).thenReturn(100);  // stub one method
        assertEquals(100, spy.size());     // stubbed
        assertEquals(1, spy.size());       // wait, this would be 100 too
    }
}

interface UserRepository { User findById(int id); }
record User(int id, String name) {}
class UserService {
    UserRepository repo;
    UserService(UserRepository r) { repo = r; }
    String getUserName(int id) {
        User u = repo.findById(id);
        if (u == null) throw new RuntimeException("not found");
        return u.name();
    }
}
22

JUnit Testing

Basistest

JUnit 5 verwendet @Test aus org.junit.jupiter.api. assertEquals verifiziert, dass erwartet gleich tatsächlich ist. Andere Assertions: assertTrue, assertThrows, assertAll.

java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
    @Test
    void testAdd() {
        assertEquals(5, calc.add(2, 3));
    }
}

Parametrisierte Tests

Parametrisierte Tests führen denselben Test mit unterschiedlichen Eingaben aus. @ValueSource stellt einzelne Argumente bereit. @CsvSource stellt mehrere Argumente bereit. Reduziert Test-Duplikation.

java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
void testPositive(int n) { assertTrue(n > 0); }

@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9"})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, calc.add(a, b));
}

Lifecycle-Methoden

@BeforeAll/@AfterAll laufen einmal pro Klasse (müssen static sein). @BeforeEach/@AfterEach laufen um jeden Test. Für Datenbankverbindungen und Mock-Setup verwenden.

java
class DbTest {
    @BeforeAll static void setupAll() { /* once before all */ }
    @AfterAll static void tearDownAll() { /* once after all */ }
    @BeforeEach void setup() { /* before each test */ }
    @AfterEach void tearDown() { /* after each test */ }
}

Assertions

assertAll führt alle Assertions aus, auch wenn einige fehlschlagen. assertThrows verifiziert, dass Code eine spezifische Exception wirft. assertTimeout für zeitbegrenzte Tests verwenden.

java
@Test
void testAll() {
    assertAll("person",
        () -> assertEquals("Alice", p.getName()),
        () -> assertEquals(30, p.getAge())
    );
}
@Test
void testException() {
    assertThrows(IllegalArgumentException.class, () -> service.process(-1));
}

Mockito

@Mock erstellt Mock-Objekte, @InjectMocks injiziert sie. when().thenReturn() stubbt Aufrufe. verify() prüft, ob eine Methode aufgerufen wurde. Mockito ist das Standard-Mocking-Framework.

java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserRepository repo;
    @InjectMocks UserService service;
    @Test
    void testFind() {
        when(repo.findById(1)).thenReturn(Optional.of(new User("Alice")));
        assertEquals("Alice", service.findUser(1).getName());
        verify(repo).findById(1);
    }
}
23

Maven/Gradle

Maven POM

Maven verwendet pom.xml. groupId/artifactId/version identifizieren das Projekt. Abhängigkeiten haben Scope (compile, test, provided). Maven erzwingt eine Standard-Verzeichnisstruktur.

java
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>1.0.0</version>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.10.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Maven-Befehle

Maven-Lifecycle: clean, compile, test, package, install, deploy. Jede Phase führt vorherige Phasen aus. -DskipTests verwenden, um Tests zu überspringen.

java
mvn clean          # Remove target/
mvn compile        # Compile sources
mvn test           # Run tests
mvn package        # Build JAR
mvn install        # Install to local repo
mvn dependency:tree  # Show dependency tree

Gradle Build

Gradle verwendet build.gradle (Groovy) oder build.gradle.kts (Kotlin). implementation für Compile-Abhängigkeiten, testImplementation für Tests. Gradle ist schneller als Maven.

java
plugins { id 'java'; id 'application' }
repositories { mavenCentral() }
dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
application { mainClass = 'com.example.App' }

Gradle-Befehle

Gradle-Tasks: build, test, run, clean. Der Wrapper (gradlew) stellt konsistente Versionen sicher. --parallel für parallele Modul-Builds verwenden.

java
gradle clean       # Clean build
gradle build       # Build + test
gradle test        # Run tests
gradle run         # Run application
gradle bootRun     # Run Spring Boot
gradle dependencies  # Show dependency tree

Multi-Modul-Projekt

Multi-Modul-Projekte splitten große Apps. settings.gradle listet Module. project(:core) erstellt Inter-Modul-Abhängigkeiten. Jedes Modul hat sein eigenes build.gradle.

java
// settings.gradle
include 'core', 'web', 'api'
// build.gradle (root)
subprojects {
    apply plugin: 'java'
    repositories { mavenCentral() }
}
// In web/build.gradle
dependencies { implementation project(':core') }
24

Spring Basics

Spring Boot App

@SpringBootApplication aktiviert Auto-Konfiguration, Component-Scanning und Konfiguration. SpringApplication.run startet den eingebetteten Server. Eliminiert XML-Konfiguration.

java
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

REST Controller

@RestController kombiniert @Controller und @ResponseBody. @GetMapping, @PostMapping sind Abkürzungen. @PathVariable extrahiert URL-Parameter, @RequestBody bindet JSON.

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    @PostMapping
    public User create(@RequestBody User user) {
        return userService.save(user);
    }
}

Dependency Injection

@Autowired injiziert Abhängigkeiten. Konstruktor-Injection wird empfohlen (testbar, unveränderlich). @Service, @Repository, @Component sind Stereotype für Injection.

java
@Service
public class UserService {
    private final UserRepository repo;
    @Autowired  // Constructor injection (recommended)
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

Konfiguration

@Configuration markiert Konfigurationsklassen. @Bean deklariert von Spring verwaltete Beans. @Primary macht eine Bean bevorzugt. Für Drittanbieter-Klassen verwenden.

java
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    @Bean @Primary
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

Application Properties

application.properties konfiguriert Spring Boot. Profiles aktivieren umgebungsspezifische Konfiguration. Mit spring.profiles.active=dev aktivieren. @Value oder @ConfigurationProperties verwenden.

java
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost/mydb
spring.jpa.hibernate.ddl-auto=update
# Profile-specific
# application-dev.properties
server.port=9090
25

JDBC Deep Dive

Connection & Statement

DriverManager.getConnection stellt eine Verbindung her. Statement führt statisches SQL aus. ResultSet iteriert Ergebnisse. Ressourcen immer schließen oder try-with-resources verwenden.

java
Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
    System.out.println(rs.getString("name"));
}
rs.close(); stmt.close(); conn.close();

PreparedStatement

PreparedStatement verhindert SQL-Injection durch Parametrisierung von Queries. Werte nach Index setzen (1-basiert). try-with-resources schließt automatisch. Verbessert Performance durch Pre-Kompilierung.

java
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, "Alice");
    ps.setString(2, "[email protected]");
    ps.executeUpdate();
}

Transaction-Management

setAutoCommit(false) startet eine Transaktion. commit persistiert, rollback macht rückgängig. Wenn eine Anweisung fehlschlägt, rollback zur Wahrung der Integrität. @Transactional handhabt dies in Spring.

java
conn.setAutoCommit(false);
try {
    stmt.executeUpdate("UPDATE accounts SET bal = bal - 100 WHERE id = 1");
    stmt.executeUpdate("UPDATE accounts SET bal = bal + 100 WHERE id = 2");
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

Connection Pooling

Connection Pooling wiederverwendet Verbindungen. HikariCP ist der schnellste Pool. maximumPoolSize begrenzt gleichzeitige Verbindungen. Immer schließen (kehrt zum Pool zurück). Spring Boot konfiguriert HikariCP automatisch.

java
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();

ResultSet Metadata

ResultSetMetaData beschreibt die Ergebnisstruktur: Spaltennamen, Typen, Eigenschaften. Nützlich für generischen Datenzugriff. Spalten-Indizes sind 1-basiert.

java
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++)
    System.out.println(meta.getColumnName(i) + ": " + meta.getColumnTypeName(i));
26

Concurrency-Utilities

ExecutorService

ExecutorService verwaltet Thread-Pools. submit gibt ein Future für asynchrone Ergebnisse zurück. get blockiert bis fertig (mit Timeout). Executor immer shutdownen.

java
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Result";
});
String result = future.get(5, TimeUnit.SECONDS);
executor.shutdown();

CompletableFuture

CompletableFuture ermöglicht funktionale asynchrone Programmierung. supplyAsync läuft im ForkJoinPool. thenApply transformiert, thenAccept konsumiert, exceptionally behandelt Fehler.

java
CompletableFuture.supplyAsync(() -> fetchData())
    .thenApply(data -> process(data))
    .thenAccept(result -> System.out.println(result))
    .exceptionally(ex -> { ex.printStackTrace(); return null; });

Concurrent Collections

ConcurrentHashMap ist Thread-sicher ohne vollständiges Locking. CopyOnWriteArrayList kopiert beim Schreiben (Lese-intensiv). BlockingQueue unterstützt Producer-Consumer-Patterns.

java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("b", k -> k.length());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);

CountDownLatch & CyclicBarrier

CountDownLatch wartet auf N Threads (Einweg). CyclicBarrier wartet auf N Threads und setzt dann zurück (wiederverwendbar). Latch für Startup, Barrier für mehrstufige Berechnung verwenden.

java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++)
    new Thread(() -> { work(); latch.countDown(); }).start();
latch.await();  // Wait for all

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All ready"));

Atomic-Variablen

Atomic-Variablen bieten lock-freie Thread-sichere Operationen. compareAndSet ermöglicht optimistisches Locking. LongAdder ist schneller als AtomicLong für hoch-umstrittene Zähler.

java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
LongAdder adder = new LongAdder();
adder.increment();
27

JVM Internals

Speicherbereiche

JVM-Speicher: Heap (Objekte, GC-verwaltet), Stack (Methodenaufrufe, pro Thread), Metaspace (Klassen-Metadaten). Young Gen verwendet Copying-GC, Old Gen verwendet Mark-Sweep-Compact.

java
// Heap: objects and arrays (shared)
// - Young Gen: Eden, S0, S1
// - Old Gen: long-lived objects
// Stack: method frames (per thread)
// Metaspace: class metadata (Java 8+)
// JVM flags: -Xms512m -Xmx2g

Class Loading

Class Loading ist lazy. Bootstrap lädt Core-Java, Extension lädt Erweiterungen, Application lädt Classpath. Statische Initialisierer laufen einmal. Custom Classloader ermöglichen Hot-Reload.

java
// Bootstrap -> Extension -> Application classloaders
class MyClass {
    static { System.out.println("Static init"); }
}
// Class.forName("MyClass") triggers loading
// -verbose:class shows class loading

Bytecode

Java kompiliert zu Bytecode (Stack-basiert). javap -c disassembliert Class-Dateien. Jede Instruktion pusht/popt den Operanden-Stack. Java-Agents können Bytecode zur Ladezeit modifizieren.

java
// javap -c MyClass.class
// Method int add(int, int):
//   iload_1      // Load local var 1
//   iload_2      // Load local var 2
//   iadd         // Add
//   ireturn      // Return int

JIT-Kompilierung

JIT kompiliert häufig ausgeführten Bytecode zu nativem Code. Tiered Compilation balanciert Startup und Peak-Performance. Hot Methods werden inlined und optimiert.

java
// JIT compiles hot methods to native code
// -XX:+PrintCompilation  // Show JIT activity
// -XX:CompileThreshold=10000  // Method call count
// Tiered: Interpreter -> C1 -> C2

Thread Dump

Thread Dumps zeigen alle Thread-Zustände und Stack-Traces. Essenziell zum Debuggen von Deadlocks und Hängern. jstack ist das Kommandozeilen-Tool. Nach BLOCKED- und WAITING-Threads suchen.

java
// Get thread dump
jstack <pid>
// Or: kill -3 <pid>
// Deadlock detection
jstack -l <pid> | grep -A 20 "Found deadlock"
28

Garbage Collection

GC-Algorithmen

Serial GC für kleine Apps. Parallel GC maximiert Throughput. G1 GC balanciert Throughput und Latenz (Default). ZGC bietet Sub-Millisekunden-Pausen für große Heaps.

java
# Serial GC (single-threaded)
-XX:+UseSerialGC
# Parallel GC (throughput)
-XX:+UseParallelGC
# G1 GC (balanced, default in Java 9+)
-XX:+UseG1GC
# ZGC (low-latency)
-XX:+UseZGC

G1 GC Tuning

G1 teilt den Heap in Regionen. MaxGCPauseMillis setzt ein weiches Pausen-Ziel. G1 priorisiert Regionen mit dem meisten Garbage. GCViewer oder GCEasy zum Analysieren der Logs verwenden.

java
# Set heap
-Xms4g -Xmx4g
# Max GC pause target
-XX:MaxGCPauseMillis=200
# Region size (1-32MB)
-XX:G1HeapRegionSize=16m
# Enable GC logging
-Xlog:gc*:file=gc.log:time,uptime

Memory Leaks

Memory Leaks werden durch unbeabsichtigte Objekt-Retention verursacht. Statische Collections, ungeschlossene Ressourcen und Listener-Registrierungen sind häufig. jmap zeigt Objekt-Anzahlen. hprof mit MAT analysieren.

java
// Common leak: static collections
static Map<String, Object> cache = new HashMap<>();
// Objects never removed -> leak
// Detect with:
jmap -histo <pid> | head -20
// Heap dump:
jmap -dump:format=b,file=heap.hprof <pid>

Weak References

WeakReference erlaubt GC, wenn keine starken Referenzen existieren. SoftReference überlebt bis Memory Pressure. WeakHashMap-Schlüssel sind weak. Für Caches verwenden, die GC nicht verhindern sollen.

java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024]);
WeakHashMap<Object, String> map = new WeakHashMap<>();
map.put(key, "value");  // Entry removed when key is GC'd

Finalization

finalize() ist veraltet (unvorhersehbar, langsam). Cleaner API (Java 9+) bietet besseres Cleanup. try-with-resources wird für deterministisches Cleanup bevorzugt.

java
// Cleaner API (Java 9+)
class Resource implements AutoCloseable {
    private final Cleaner.Cleanable cleanable;
    Resource() {
        cleanable = Cleaner.create().register(this, () -> cleanup());
    }
    public void close() { cleanable.clean(); }
}
29

Stream Collectors

Grouping By

groupingBy partitioniert Elemente nach einem Klassifizierer. Das zweite Argument ist ein Downstream-Collector für Aggregation. counting, averaging, summing sind häufige Downstream-Collectoren.

java
Map<String, List<Person>> byCity =
    people.stream().collect(Collectors.groupingBy(Person::getCity));
Map<String, Long> countByCity =
    people.stream().collect(Collectors.groupingBy(
        Person::getCity, Collectors.counting()));

Partitioning

partitioningBy teilt in zwei Gruppen (true/false). Effizienter als groupingBy für Boolean-Schlüssel. Ergebnis hat immer beide Schlüssel. Downstream-Collectoren aggregieren jede Partition.

java
Map<Boolean, List<Person>> partition =
    people.stream().collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
// {false=[minors], true=[adults]}
Map<Boolean, Long> count =
    people.stream().collect(Collectors.partitioningBy(
        p -> p.getAge() >= 18, Collectors.counting()));

Joining

joining verkettet Strings mit optionalem Trennzeichen, Präfix und Suffix. Elemente müssen Strings sein; zuerst map verwenden. Verwendet intern StringBuilder.

java
String names = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String csv = people.stream()
    .map(p -> p.getName() + "=" + p.getAge())
    .collect(Collectors.joining("\n", "[", "]"));

Reducing

reducing führt eine Fold-Operation aus. Die Drei-Argument-Version nimmt Identity, Mapper, Reducer. Die Zwei-Argument-Version gibt Optional zurück. Verwenden, wenn Standard-Collectoren unzureichend sind.

java
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));
Optional<Person> oldest = people.stream()
    .collect(Collectors.reducing((p1, p2) ->
        p1.getAge() > p2.getAge() ? p1 : p2));

Custom Collector

Collector.of erstellt benutzerdefinierte Collectoren: Supplier, Accumulator, Combiner, Finisher. Der Combiner führt partielle Ergebnisse für parallele Streams zusammen. Nützlich für spezialisierte Ausgabeformate.

java
Collector<Person, ?, String> toJson = Collector.of(
    StringBuilder::new,
    (sb, p) -> sb.append(`{"name":"${p.getName()}"}`),
    StringBuilder::append,
    StringBuilder::toString
);
30

Häufige Fallstricke

Integer Caching

Java speichert Integer-Werte von -128 bis 127. == vergleicht Referenzen, nicht Werte. Für Integer außerhalb des Cache-Bereichs gibt == false zurück. Immer .equals() für Integer verwenden.

java
Integer a = 127; Integer b = 127;
System.out.println(a == b);  // true (cached)
Integer c = 128; Integer d = 128;
System.out.println(c == d);  // false (not cached)
System.out.println(c.equals(d));  // true

String-Immutabilität

Strings sind unveränderlich: Methoden wie concat geben neue Strings zurück. Vergessen, das Ergebnis zuzuweisen, ist ein häufiger Bug. StringBuilder für wiederholte Konkatenation verwenden.

java
String s = "Hello";
s.concat(" World");  // Returns new string, s unchanged
System.out.println(s);  // "Hello"
// Use StringBuilder for mutation
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");  // Mutates in place

Equals & HashCode

Gleiche Objekte müssen gleiche Hash-Codes haben. HashMap und HashSet verwenden hashCode für Bucketing und equals für Vergleich. Wenn equals überschrieben wird, MUSS hashCode überschrieben werden.

java
class Person {
    String name;
    public boolean equals(Object o) {
        if (!(o instanceof Person)) return false;
        return name.equals(((Person)o).name);
    }
    public int hashCode() { return name.hashCode(); }
}

Checked vs. Unchecked

Checked Exceptions müssen deklariert oder gefangen werden (IOException, SQLException). Unchecked (RuntimeException) erfordern keine Behandlung. Vermeiden, breite Exception zu fangen.

java
// Checked: must catch or declare
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* handle */ }
// Unchecked: RuntimeException
throw new IllegalArgumentException("bad input");

Ressourcen-Lecks

Ressourcen müssen geschlossen werden. try-with-resources (Java 7+) schließt AutoCloseable automatisch. Ohne dies verursachen Exceptions Lecks. Niemals eine Ressource ungeschlossen lassen.

java
// BAD: resource leak
FileInputStream fis = new FileInputStream("file.txt");
// If exception here, fis never closed
// GOOD: try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // Use resource
}  // Auto-closed even on exception
31

Design Patterns

Singleton

Double-checked Locking mit volatile stellt Thread-sichere Lazy-Initialisierung sicher. volatile verhindert Instruction-Reordering. Enum-Singleton ist einfacher: public enum Singleton { INSTANCE; }.

java
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
}

Builder

Builder-Pattern behandelt Objekte mit vielen optionalen Parametern. Vermeidet teleskopierende Konstruktoren. Der innere Builder sammelt Parameter fluently. build() erstellt das unveränderliche Objekt.

java
public class Pizza {
    private final String size;
    private final boolean cheese;
    private Pizza(Builder b) { size = b.size; cheese = b.cheese; }
    public static class Builder {
        private String size; private boolean cheese;
        public Builder size(String s) { size = s; return this; }
        public Builder cheese(boolean c) { cheese = c; return this; }
        public Pizza build() { return new Pizza(this); }
    }
}

Strategy

Strategy-Pattern kapselt austauschbare Algorithmen. Der Kontext delegiert an das Strategy-Interface. Vermeidet große if-else-Ketten. Folgt dem Open-Closed-Prinzip.

java
interface PaymentStrategy { void pay(double amount); }
class CreditCard implements PaymentStrategy {
    public void pay(double amount) { System.out.println("Card: " + amount); }
}
class Cart { private PaymentStrategy strategy; void checkout() { strategy.pay(100); } }

Observer

Observer-Pattern definiert eine Eins-zu-viele-Abhängigkeit. Wenn sich das Subject ändert, werden alle Observer benachrichtigt. Verwendet in MVC, Event-Systemen und reaktiver Programmierung.

java
interface Observer { void update(String event); }
class Subject {
    private List<Observer> observers = new ArrayList<>();
    void subscribe(Observer o) { observers.add(o); }
    void notify(String event) { observers.forEach(o -> o.update(event)); }
}

Factory Method

Factory Method definiert ein Interface zum Erstellen von Objekten, lässt aber Subklassen entscheiden, welche Klasse instanziiert wird. Entkoppelt Client-Code von konkreten Klassen.

java
abstract class Document { abstract void open(); }
class PDF extends Document { void open() { /* ... */ } }
abstract class DocFactory { abstract Document create(); }
class PDFFactory extends DocFactory { Document create() { return new PDF(); } }

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.