Zum Inhalt springen

Rust für Java-Entwickler: ein praktischer Leitfaden

Wenn du jahrelang mit Java gearbeitet hast, bringst du fast alles mit, was du zum Rust-Lernen brauchst. Du denkst in Typen. Du kennst Generics, Interfaces, Lambdas und Stream-Pipelines. Rust hat all das: Traits sehen Interfaces mit Default-Methoden sehr ähnlich, Iteratoren lesen sich wie Streams, und Cargo erledigt die Aufgaben von Maven oder Gradle, nur mit viel weniger Konfiguration.

Neu ist alles, was bisher die JVM für dich erledigt hat. Rust hat keinen Garbage Collector und keine virtuelle Maschine. Dein Code wird zu einer nativen Binärdatei kompiliert, und der Compiler entscheidet, wann Speicher freigegeben wird, indem er Ownership (Eigentum) und Borrowing (Ausleihen) prüft, bevor das Programm läuft. Außerdem hat Rust kein null (du nutzt Option), keine Exceptions (du nutzt Result) und keine Klassenvererbung. In diese vier Änderungen fließt der Großteil deiner Lernzeit.

Warum greifen Java-Entwickler zu Rust? Meist aus einem dieser Gründe: Dienste, die wenig und gleichmäßig Speicher brauchen und keine GC-Pausen haben dürfen. Kommandozeilen-Tools und Serverless-Funktionen, die ohne JVM sofort starten sollen. Ein heißer Pfad in einer Java-Anwendung, den du in nativen Code verlagern willst (Rust lässt sich aus Java über JNI oder die neuere Foreign Function & Memory API aufrufen). Oder einfach Neugier, warum Rust in Umfragen immer wieder als „beliebteste Sprache“ ganz oben landet. Diese Seite übersetzt das, was du aus Java kennst, in Rust. So kannst du dich auf die Teile konzentrieren, die wirklich anders sind.

Einen allgemeinen Vergleich mit C++, Go und Python findest du unter Rust vs. C++, Go und Python. Wenn du auch .NET kennst, gibt es eine eigene Seite: Rust für C#-Entwickler.

Nutze diese Tabelle wie einen Sprachführer. Die letzte Spalte verlinkt die Lektion auf dieser Seite, die die Rust-Seite erklärt.

Java Rust Mehr dazu
class mit Feldern und Methoden struct für die Daten + ein impl-Block für die Methoden Structs
Konstruktoren und new Eine ganz normale Funktion, die per Konvention new heißt und Self zurückgibt Structs
interface trait Traits
default-Methoden in einem Interface Standardmethoden in einem Trait (sie funktionieren genauso) Traits
abstract class, extends, super Keine Vererbung. Nutze Traits mit Standardmethoden für gemeinsames Verhalten und Komposition (ein Struct in einem Struct) für gemeinsame Daten Traits, Entwurfsmuster
List<Shape> mit verschiedenen Klassen Vec<Box<dyn Shape>> (ein Trait-Objekt) Trait-Objekte
sealed interface + records + Pattern-switch (Java 17/21) Ein enum, dessen Varianten Daten tragen, plus match Enums, Match
enum mit Feldern und Konstruktoren enum, bei dem jede Variante andere Daten enthalten kann Enums
record Point(int x, int y) #[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } Structs, Wichtige Traits
toString(), equals(), hashCode(), compareTo() Display / Debug, PartialEq / Eq, Hash, PartialOrd / Ord (meist per derive) Wichtige Traits
== (dasselbe Objekt) vs. .equals() (derselbe Wert) == vergleicht immer Werte. Die Falle „Referenzgleichheit“ gibt es nicht Operatoren
null und Optional<T> Option<T>: entweder Some(value) oder None Option
orElse, orElseGet, orElseThrow, map, flatMap, ifPresent unwrap_or, unwrap_or_else, ok_or / expect, map, and_then, if let Some(...) Option, If Let / While Let
Checked Exceptions und throws Result<T, E> im Rückgabetyp, plus der Operator ? Result, Eigene Fehlertypen
Unchecked Exceptions für echte Bugs (IllegalStateException) panic! (beendet den Thread) Panic
Exception Chaining (new ConfigException(msg, cause)) map_err, oder #[source] aus thiserror und .context(...) aus anyhow anyhow und thiserror
try-with-resources und AutoCloseable Drop, das automatisch läuft, wenn der Besitzer seinen Gültigkeitsbereich verlässt Gültigkeitsbereich, Smart Pointer
Garbage Collector Ownership und Borrowing, geprüft beim Kompilieren Ownership, Borrowing
final lokale Variablen Einfaches let (standardmäßig unveränderbar); mit let mut erlaubst du Änderungen Variablen
static final-Konstanten const Variablen
Veränderbare static-Felder, Singletons static mit LazyLock oder OnceLock, in einem Mutex, wenn sich der Wert ändern muss Geteilter Zustand
Generics mit <T extends Comparable<T>> Generics mit Trait Bounds: T: Ord oder where T: Ord Generics
Type Erasure, List<Integer> statt List<int> Monomorphisierung: Generischer Code wird für jeden Typ einzeln kompiliert, und ein Vec<i32> speichert einfache Zahlen Generics, Fortgeschrittene Generics
Lambdas, Function<T, R>, Supplier<T>, Consumer<T> Closures, beschrieben durch die Traits Fn, FnMut, FnOnce Closures, Fn, FnMut und FnOnce
Streams: filter, map, reduce, collect Iterator-Adapter: filter, map, fold, sum, collect Iteratoren
Iterable<T> und Iterator<T> IntoIterator und Iterator (eine Methode: next, die eine Option zurückgibt) Eigene Iteratoren
parallelStream() par_iter() aus dem Crate rayon Nebenläufigkeit
Thread, ExecutorService std::thread::spawn und std::thread::scope; rayon oder tokio für Pools Nebenläufigkeit
synchronized, ReentrantLock Mutex<T>, der die Daten umschließt, die er schützt Geteilter Zustand
AtomicInteger, AtomicLong AtomicI32, AtomicI64, AtomicUsize Geteilter Zustand
BlockingQueue zwischen Threads Channels (std::sync::mpsc) Channels
CompletableFuture, virtuelle Threads async fn und .await, ausgeführt von einer Runtime wie tokio Async/Await
String (unveränderbar) und StringBuilder &str (eine geliehene Sicht auf Text) und String (besessen, wachsend) Strings
char (eine UTF-16-Einheit) char (ein beliebiges Unicode-Zeichen, 4 Bytes); Strings sind UTF-8 Strings
int, long, double und geboxtes Integer i32, i64, f64, dazu vorzeichenlose Typen von u8 bis u128. Nie geboxt Datentypen
Stiller Integer-Überlauf Eine Panic in Debug-Builds; mit checked_add, wrapping_add, saturating_add wählst du das Verhalten Integer-Überlauf
Casts wie (int) x as für Zahlen, From / TryFrom für sichere Umwandlungen Typumwandlung, Konvertierungen
BigDecimal, BigInteger Die Crates rust_decimal und num-bigint Datentypen
Packages und import Module (mod) und use Module
public, private, protected, package-private Standardmäßig privat; pub und pub(crate). Ein protected gibt es nicht Module
Ein JAR / Maven-Artefakt Ein Crate (eine Bibliothek oder ein Programm) Cargo und Crates
Annotationen (@Override, @Data aus Lombok) Attribute wie #[derive(Debug)] und #[test] Makros
Reflection, Verarbeitung von Annotationen zur Laufzeit Zur Laufzeit praktisch keine. Stattdessen erzeugen Makros Code beim Kompilieren Prozedurale Makros
Methodenüberladung Keine Überladung. Nutze verschiedene Namen oder einen generischen Parameter Funktionen
instanceof-Prüfungen und Downcasts match auf ein Enum (Downcasting mit Any gibt es, aber selten) Pattern Matching
JNI, Foreign Function & Memory API extern "C"-Funktionen FFI

Das Java Collections Framework passt sauber auf std::collections. Der größte Unterschied: Collections in Rust geben nie null zurück. Ein Nachschlagen liefert dir eine Option.

Java Rust Mehr dazu
ArrayList<T> Vec<T> Vektoren
int[], String[] [T; N], wenn die Länge beim Kompilieren bekannt ist, sonst Vec<T>. Beides übergibst du als Slice &[T] Arrays, Slices
List.of(...) (unveränderbar) Ein Array oder ein Vec, den du nur als &Vec<T> / &[T] herausgibst (über eine geteilte Referenz kann ihn niemand ändern) Borrowing
HashMap<K, V> HashMap<K, V> (get gibt Option<&V> zurück) HashMap
map.merge(word, 1, Integer::sum) *map.entry(word).or_insert(0) += 1 HashMap
TreeMap<K, V> BTreeMap<K, V> (Schlüssel bleiben sortiert) HashMap
LinkedHashMap<K, V> (Einfügereihenfolge) IndexMap aus dem Crate indexmap
HashSet<T>, TreeSet<T> HashSet<T>, BTreeSet<T> HashSet
ArrayDeque<T> VecDeque<T> VecDeque und BinaryHeap
Stack<T> oder Deque als Stack Vec<T> mit push / pop Vektoren
PriorityQueue<T> (kleinstes Element zuerst) BinaryHeap<T> (größtes Element zuerst; mit Reverse umschlossen das kleinste zuerst) VecDeque und BinaryHeap
LinkedList<T> LinkedList<T> gibt es, aber Vec oder VecDeque ist fast immer schneller VecDeque und BinaryHeap
ConcurrentHashMap<K, V> Mutex<HashMap<K, V>> oder das Crate dashmap Geteilter Zustand
list.sort(Comparator.comparing(...)) v.sort_by_key(...) oder v.sort_by(...) Sortieren und Suchen
list.removeIf(...) v.retain(...) (behält die Elemente, die passen) Vektoren

Wie die HashMap in Java hat auch eine HashMap in Rust keine feste Reihenfolge, und die Reihenfolge kann sich zwischen zwei Läufen ändern. Wenn du sortierte Ausgabe brauchst, nimm eine BTreeMap oder sortiere vorher die Schlüssel.

Die meisten Java-Entwickler verlieben sich schnell in Cargo. Es gibt ein Tool, ein Konfigurationsformat und kein Plugin-XML. Builds, Abhängigkeiten, Tests, Doku, Formatierung und Lints kommen alle aus derselben Quelle.

Java Rust Hinweise
Maven / Gradle cargo Baut, startet, testet, fügt Abhängigkeiten hinzu, veröffentlicht. Siehe Cargo und Crates
pom.xml / build.gradle.kts Cargo.toml Kurz, und für normale Projekte sind keine Build-Skripte nötig
Dependency Locking in Gradle Cargo.lock Wird für dich erzeugt und aktualisiert
Multi-Modul-Build Ein Cargo-Workspace Eine Cargo.toml ganz oben listet die zugehörigen Crates auf
Maven Central crates.io Die API-Doku jedes veröffentlichten Crates steht auf docs.rs
Eine <dependency> hinzufügen cargo add serde
mvn archetype:generate / gradle init cargo new my_app (cargo new --lib für eine Bibliothek)
mvn compile / mvn test / mvn package cargo build / cargo test / cargo build --release Debug-Builds laufen deutlich langsamer; Performance immer im Release-Modus messen
java -jar app.jar ./target/release/app Eine einzige native Binärdatei. Keine JVM auf dem Server nötig
JDK-Versionen, SDKMAN!, Toolchains rustup und eine Datei rust-toolchain.toml Siehe Rust-Werkzeuge und Einrichtung
javac rustc Cargo ruft es für dich auf; selbst startest du es selten
JShell Der Rust Playground im Browser oder evcxr_repl
Checkstyle, SpotBugs, PMD, Error Prone cargo clippy Hunderte offizielle Lints
google-java-format, Spotless cargo fmt Ein einheitlicher Stil; niemand streitet über Klammern
IntelliJ IDEA, Eclipse JetBrains RustRover oder VS Code + rust-analyzer rust-analyzer bietet Vervollständigung, Sprung zur Definition und Fehler direkt im Code
Javadoc ///-Doc-Kommentare in Markdown + cargo doc Code-Beispiele in der Doku laufen als Tests. Siehe Dokumentation
JUnit 5 Eingebautes #[test] und cargo test (cargo nextest ist ein beliebter, schnellerer Runner) Siehe Testen
@ParameterizedTest rstest
Mockito mockall oder handgeschriebene Fakes, die deinen Trait implementieren Siehe Testen für Fortgeschrittene
AssertJ / Hamcrest assert!, assert_eq! und pretty_assertions für gut lesbare Diffs
WireMock, Testcontainers wiremock, testcontainers Dieselben Ideen als Rust-Versionen
JaCoCo cargo llvm-cov oder cargo tarpaulin
JMH criterion Siehe Performance
JFR, async-profiler, VisualVM cargo flamegraph (installiert mit cargo install flamegraph), perf Kein GC und kein JIT, die man tunen müsste
GraalVM native-image Nicht nötig Jeder Rust-Build ist schon nativer Code

Hier ist eine kleine Cargo.toml. Vergleich sie mit der pom.xml, die du für dieselben drei Abhängigkeiten bräuchtest:

[package]
name = "orders-api"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Bibliotheken und Frameworks: „Was nehmen wir statt …?“

Abschnitt betitelt „Bibliotheken und Frameworks: „Was nehmen wir statt …?““

Das ist die Frage, die jedes Java-Team zuerst stellt: „Wir nutzen Spring Boot und Hibernate. Was gibt es dafür in Rust?“ Rust hat kein Spring, also kein einzelnes Framework, das alles mitbringt. Stattdessen kombinierst du ein paar spezialisierte Crates. Hier sind die nächsten Entsprechungen.

Java Rust Hinweise
Spring Boot (Spring MVC / WebFlux) axum (wir bauen eine API in Projekt: Web-API mit Axum), actix-web, Rocket axum und actix-web sind in Produktion weit verbreitet
@RestController + @GetMapping axum-Routen, die auf einfache async fn-Handler zeigen Keine Annotationen und kein Classpath-Scanning; du verdrahtest die Routen im Code
Servlet-Filter, Spring-Interceptors tower-Layer und das Crate tower-http (CORS, Kompression, Tracing, Timeouts) axum nutzt tower direkt
Dependency Injection in Spring (@Autowired) Geteilter State in axum, einfache Konstruktoren und Traits Siehe die DI-Zeile weiter unten
springdoc-openapi utoipa
Thymeleaf askama (Templates, die beim Kompilieren geprüft werden) oder tera
Spring WebSocket WebSockets in axum (das Feature ws) oder tokio-tungstenite
Spring for GraphQL async-graphql
gRPC Java tonic (mit prost für Protobuf)
java.net.http.HttpClient, OkHttp, WebClient reqwest Standardmäßig async, mit optionalem blockierendem Client
Netty tokio + hyper
Spring Security (JWT, Passwort-Hashing) jsonwebtoken, argon2 Kein Security-Framework, das alles abdeckt
Java Rust Hinweise
Hibernate / JPA / Spring Data JPA SeaORM: asynchrones ORM mit Entities, Relationen und Migrationen Fühlt sich am ehesten wie JPA an; baut auf sqlx auf
Hibernate, aber mit Fokus auf strenge Typen Diesel: ORM und Query Builder; deine Abfragen werden beim Kompilieren typgeprüft Ausgereift (2.x), standardmäßig synchron (diesel-async gibt es)
JDBC, JdbcTemplate, jOOQ, MyBatis sqlx: einfaches SQL, und das Makro query! prüft es beim Kompilieren gegen deine echte Datenbank Async, sehr beliebt, kein ORM
Flyway / Liquibase sqlx migrate (sqlx-cli), diesel migration oder refinery
Jedis / Lettuce (Redis) redis
Kafka-Clients, Spring Kafka rdkafka (baut auf librdkafka auf)
RabbitMQ Java Client lapin
AWS SDK for Java Das offizielle AWS SDK for Rust: aws-config, aws-sdk-s3, …

Wenn du JPA-Entities magst, probier zuerst SeaORM. Wenn du lieber SQL schreibst (die jOOQ- und MyBatis-Fraktion), probier sqlx. Wenn du die stärksten Garantien beim Kompilieren willst, probier Diesel. Datenbanken in Rust zeigt alle drei mit funktionierendem Code.

Java Rust Hinweise
Jackson / Gson serde + serde_json Der Standard; kann auch TOML, YAML und mehr. Siehe JSON mit Serde
JAXB, Jackson XML quick-xml (mit dem Feature serialize für serde)
SLF4J + Logback / Log4j 2 tracing (+ tracing-subscriber) oder log + env_logger Siehe Logging
Lombok Eingebaute Derives (Debug, Clone, PartialEq, Default, …); bon oder derive_builder für Builder Keine Bytecode-Tricks; alles ganz normale Makros
MapStruct From- / Into-Implementierungen Von Hand geschrieben, vom Compiler geprüft. Siehe Konvertierungen
IoC-Container von Spring Meist gar nichts: Abhängigkeiten an Konstruktoren übergeben, Traits für alles, was du in Tests austauschen willst Rust-Projekte nutzen selten einen DI-Container
Bean Validation (@NotNull, @Size) validator oder garde (Validierung per derive) @NotNull brauchst du nicht: Nutze Option nur dort, wo ein Wert fehlen darf
Resilience4j backon oder die Retry- und Timeout-Middleware von tower
Caffeine (Caching) moka
Guava, Apache Commons Meist die Standardbibliothek, dazu itertools
java.time chrono oder jiff Siehe Datum und Uhrzeit
picocli clap Siehe Projekt: CLI-Tool mit clap
@Scheduled, Quartz tokio-cron-scheduler
Micrometer metrics oder opentelemetry
Jakarta Mail lettre
Java Rust Hinweise
JavaFX / Swing Tauri (Web-UI + Rust-Backend), Slint, egui, Iced Tauri 2 und Slint 1.x sind am stabilsten. Siehe GUI-Apps
LibGDX, jMonkeyEngine Bevy, macroquad Bevy ist noch vor 1.0 und ändert seine API zwischen Releases. Siehe Spieleentwicklung
Code für Android-Apps Behalte Kotlin oder Java für die UI; pack gemeinsame Logik in eine Rust-Bibliothek, gebaut mit dem Android NDK uniffi von Mozilla erzeugt Kotlin-Bindings
Nativen Code aus Java aufrufen extern "C"-Funktionen bereitstellen und über die FFM API (ab JDK 22) aufrufen, oder JNI-Funktionen mit dem Crate jni schreiben Siehe FFI

Im direkten Vergleich: aus einer abstrakten Klasse wird ein Trait

Abschnitt betitelt „Im direkten Vergleich: aus einer abstrakten Klasse wird ein Trait“

Hier ist eine klassische Java-Hierarchie: eine abstrakte Basisklasse mit einer abstrakten und einer gemeinsamen Methode.

abstract class Employee {
protected final String name;
Employee(String name) { this.name = name; }
abstract int monthlyPay();
String payslip() { return name + " verdient " + monthlyPay() + " im Monat"; }
}
class Salaried extends Employee {
private final int yearly;
Salaried(String name, int yearly) { super(name); this.yearly = yearly; }
@Override int monthlyPay() { return yearly / 12; }
}
class Contractor extends Employee {
private final int hours, rate;
Contractor(String name, int hours, int rate) { super(name); this.hours = hours; this.rate = rate; }
@Override int monthlyPay() { return hours * rate; }
}
List<Employee> staff = List.of(new Salaried("Ada", 60000), new Contractor("Linus", 120, 50));
for (Employee e : staff) System.out.println(e.payslip());

Rust hat kein extends. Die gemeinsame Methode wandert als Standardmethode in einen Trait, und jedes Struct behält seine eigenen Daten:

trait Employee {
fn name(&self) -> &str;
fn monthly_pay(&self) -> u32;
// Eine Standardmethode: wie das konkrete payslip() in der abstrakten Klasse.
fn payslip(&self) -> String {
format!("{} verdient {} im Monat", self.name(), self.monthly_pay())
}
}
struct Salaried {
name: String,
yearly: u32,
}
struct Contractor {
name: String,
hours: u32,
rate: u32,
}
impl Employee for Salaried {
fn name(&self) -> &str {
&self.name
}
fn monthly_pay(&self) -> u32 {
self.yearly / 12
}
}
impl Employee for Contractor {
fn name(&self) -> &str {
&self.name
}
fn monthly_pay(&self) -> u32 {
self.hours * self.rate
}
}
fn main() {
// Box<dyn Employee> spielt die Rolle von „eine Referenz auf irgendeinen Employee“.
let staff: Vec<Box<dyn Employee>> = vec![
Box::new(Salaried { name: "Ada".to_string(), yearly: 60_000 }),
Box::new(Contractor { name: "Linus".to_string(), hours: 120, rate: 50 }),
];
for e in &staff {
println!("{}", e.payslip());
}
}

Ausgabe:

Ada verdient 5000 im Monat
Linus verdient 6000 im Monat
▶ Selbst ausprobieren

Ein Trait kann keine Felder enthalten. Deshalb wird aus dem protected name eine kleine Methode name(), die jedes Struct implementiert. Das fühlt sich anfangs nach Mehrarbeit an, beseitigt aber eine ganze Klasse von Problemen: keine fragilen Basisklassen, keine vergessenen super-Aufrufe und keine tiefen Hierarchien, durch die du scrollen musst. Wenn du die default-Methoden an Interfaces aus Java 8 kennst, weißt du schon, wie Rust Verhalten teilt. Und wenn die Menge der Typen feststeht (wie bei einem sealed interface in Java 17), ist ein Enum mit match oft einfacher als ein Trait.

Optional war in Java ein großer Schritt nach vorn. Es wird aber nur für Rückgabewerte genutzt, und map.get() oder jedes beliebige Feld kann trotzdem null sein:

static Optional<String> findEmail(int userId) {
return userId == 1 ? Optional.of("[email protected]") : Optional.empty();
}
for (int id : new int[] {1, 2}) {
System.out.println(findEmail(id).map(String::toUpperCase).orElse("keine E-Mail hinterlegt"));
}
findEmail(1).ifPresent(email -> System.out.println("sende an " + email));

Option in Rust hat fast dieselbe API, wird aber überall genutzt, wo ein Wert fehlen könnte, auch beim Nachschlagen in Collections. Und dahinter versteckt sich kein null:

fn find_email(user_id: u32) -> Option<String> {
if user_id == 1 {
Some("[email protected]".to_string())
} else {
None
}
}
fn main() {
for id in [1, 2] {
// map + orElseGet
let shown = find_email(id)
.map(|email| email.to_uppercase())
.unwrap_or_else(|| "keine E-Mail hinterlegt".to_string());
println!("{shown}");
}
// ifPresent
if let Some(email) = find_email(1) {
println!("sende an {email}");
}
// flatMap heißt and_then
let domain = find_email(1).and_then(|email| email.split_once('@').map(|(_, d)| d.to_string()));
println!("{domain:?}");
}

Ausgabe:

keine E-Mail hinterlegt
Some("example.com")
▶ Selbst ausprobieren

Der Compiler lässt dich .to_uppercase() erst auf einer Option<String> aufrufen, wenn du dich um None gekümmert hast. Eine NullPointerException gibt es deshalb einfach nicht. Option kostet außerdem nichts extra: Eine Option<&T> ist genauso groß wie eine einfache Referenz. Die Hilfsmethoden lernst du auf der Seite Option kennen.

Im direkten Vergleich: aus Checked Exceptions wird Result

Abschnitt betitelt „Im direkten Vergleich: aus Checked Exceptions wird Result“

Hinter Checked Exceptions steckte eine gute Idee: Die Signatur sagt dir, was schiefgehen kann. Hier ist eine typische Java-Version:

class InsufficientFundsException extends Exception {
InsufficientFundsException(int needed) { super("es fehlen noch " + needed); }
}
static int withdraw(int balance, int amount) throws InsufficientFundsException {
if (amount > balance) throw new InsufficientFundsException(amount - balance);
return balance - amount;
}
static int pay(int balance, int amount) throws InsufficientFundsException {
int left = withdraw(balance, amount);
System.out.println(amount + " bezahlt, " + left + " übrig");
return left;
}
try { pay(100, 80); pay(50, 80); }
catch (InsufficientFundsException e) { System.out.println("Fehler: " + e.getMessage()); }

Rust behält diese gute Idee und wirft den Rest weg. Der Fehler ist Teil des Rückgabetyps (Result<u32, BankError>), und ? tut, was throws tut: „Wenn das fehlgeschlagen ist, reich den Fehler an meinen Aufrufer weiter.“ Fehler sind meist ein enum, sodass ein einziger Typ jede Art auflisten kann, auf die eine Operation fehlschlagen kann:

use std::fmt;
#[derive(Debug)]
enum BankError {
InvalidAmount,
InsufficientFunds { needed: u32 },
}
// Display übernimmt die Rolle von getMessage().
impl fmt::Display for BankError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BankError::InvalidAmount => write!(f, "der Betrag muss größer als null sein"),
BankError::InsufficientFunds { needed } => write!(f, "es fehlen noch {needed}"),
}
}
}
fn withdraw(balance: u32, amount: u32) -> Result<u32, BankError> {
if amount == 0 {
return Err(BankError::InvalidAmount);
}
if amount > balance {
return Err(BankError::InsufficientFunds { needed: amount - balance });
}
Ok(balance - amount)
}
fn pay(balance: u32, amount: u32) -> Result<u32, BankError> {
let left = withdraw(balance, amount)?; // wie der Aufruf einer Methode, die „wirft“
println!("{amount} bezahlt, {left} übrig");
Ok(left)
}
fn main() {
for (balance, amount) in [(100, 80), (50, 80), (100, 0)] {
if let Err(e) = pay(balance, amount) {
println!("Fehler: {e}");
}
}
}

Ausgabe:

80 bezahlt, 20 übrig
Fehler: es fehlen noch 30
Fehler: der Betrag muss größer als null sein
▶ Selbst ausprobieren

Achte darauf, was alles fehlt: keine try-Blöcke, kein Stack Unwinding, keine Exceptions, die in eine RuntimeException verpackt werden, nur damit sie durch ein Lambda kommen. Fehler sind ganz normale Werte und funktionieren deshalb problemlos in Closures und Iterator-Ketten. Das ? markiert sichtbar jede Stelle, an der die Funktion vorzeitig zurückkehren kann. Display von Hand zu schreiben wird schnell lästig. Das Crate thiserror erzeugt es für dich, und anyhow gibt Anwendungen einen Fehlertyp für alles (siehe anyhow und thiserror). Für echte Bugs, die du in Java mit einer Unchecked Exception melden würdest, hat Rust panic!.

Im direkten Vergleich: aus Streams werden Iteratoren

Abschnitt betitelt „Im direkten Vergleich: aus Streams werden Iteratoren“

Eine Stream-Pipeline mit einem groupingBy-Collector:

record Order(String customer, String city, int total) {}
List<Order> orders = List.of(
new Order("Ada", "London", 120), new Order("Grace", "New York", 80),
new Order("Linus", "Helsinki", 200), new Order("Ken", "New York", 150));
List<String> big = orders.stream()
.filter(o -> o.total() > 100)
.sorted(Comparator.comparingInt(Order::total).reversed())
.map(Order::customer)
.toList();
System.out.println(String.join(", ", big));
Map<String, Integer> byCity = orders.stream()
.collect(Collectors.groupingBy(Order::city, TreeMap::new, Collectors.summingInt(Order::total)));
byCity.forEach((city, sum) -> System.out.println(city + ": " + sum));

In Rust übernimmt .iter() die Rolle von .stream(), und die Adapter haben vertraute Namen:

use std::cmp::Reverse;
use std::collections::BTreeMap;
struct Order {
customer: String,
city: String,
total: u32,
}
fn order(customer: &str, city: &str, total: u32) -> Order {
Order { customer: customer.to_string(), city: city.to_string(), total }
}
fn main() {
let orders = vec![
order("Ada", "London", 120),
order("Grace", "New York", 80),
order("Linus", "Helsinki", 200),
order("Ken", "New York", 150),
];
// filter + toList
let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect();
// sorted(...reversed()): Sortiert wird auf einem Vec, nicht im Iterator.
big.sort_by_key(|o| Reverse(o.total));
// map
let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect();
println!("{}", names.join(", "));
// groupingBy(city, TreeMap::new, summingInt(total))
let mut by_city: BTreeMap<&str, u32> = BTreeMap::new();
for o in &orders {
*by_city.entry(o.city.as_str()).or_insert(0) += o.total;
}
for (city, sum) in &by_city {
println!("{city}: {sum}");
}
}

Ausgabe:

Linus, Ken, Ada
Helsinki: 200
London: 120
New York: 230
▶ Selbst ausprobieren

Wie Streams sind Iteratoren in Rust lazy (sie arbeiten erst bei Bedarf) und laufen erst, wenn etwas sie verbraucht (collect, sum, eine for-Schleife). Anders als bei Streams gibt es keine Klasse Collectors: collect() schaut auf den Typ, den du anforderst (Vec, HashMap, String, …), und baut ihn. Zum Gruppieren ist eine einfache Schleife mit der entry-API die übliche Antwort; das Crate itertools ergänzt Helfer, falls dir groupingBy fehlt. Und Iterator-Ketten werden zu demselben Maschinencode kompiliert wie eine handgeschriebene Schleife, ohne dass unterwegs ints geboxt werden. Mehr dazu unter Iteratoren und Sortieren und Suchen.

Im direkten Vergleich: Type Erasure vs. Monomorphisierung

Abschnitt betitelt „Im direkten Vergleich: Type Erasure vs. Monomorphisierung“

Generics in Java gibt es nur beim Kompilieren. Zur Laufzeit ist T gelöscht (Type Erasure). Deshalb kannst du nicht new T() schreiben, und du kannst kein int ohne Boxing in eine List stecken:

static <T> List<T> filled(int n) {
List<T> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(new T()); // kompiliert nicht: T ist gelöscht
return list;
}
List<int> ids; // kompiliert auch nicht: du brauchst List<Integer>

Rust kompiliert für jeden Typ, mit dem du eine generische Funktion benutzt, eine eigene Kopie davon (das nennt sich Monomorphisierung). Die Funktion weiß also genau, was T ist, und kann T::default() aufrufen:

use std::mem::size_of;
// „T muss einen Standardwert haben und klonbar sein“
fn filled<T: Default + Clone>(n: usize) -> Vec<T> {
vec![T::default(); n]
}
fn main() {
let ids: Vec<i32> = filled(3);
let names: Vec<String> = filled(2);
let flags: Vec<bool> = filled(2);
println!("{ids:?} {names:?} {flags:?}");
// Kein Boxing: Ein Vec<i32> speichert einfache 4-Byte-Zahlen direkt nebeneinander.
println!("ein i32 belegt {} Bytes", size_of::<i32>());
println!("ein bool belegt {} Byte", size_of::<bool>());
}

Ausgabe:

[0, 0, 0] ["", ""] [false, false]
ein i32 belegt 4 Bytes
ein bool belegt 1 Byte
▶ Selbst ausprobieren

Der Kompromiss: Mehr Kopien des Codes bedeuten größere Binärdateien und längere Kompilierzeiten, dafür ist jeder Aufruf direkt und kann geinlinet werden. Wenn du eine gemeinsame Kopie willst (wie in Java), nimm ein Trait-Objekt wie Box<dyn Trait>. Die Bounds nach dem Doppelpunkt (T: Default + Clone) sind die Rust-Version von <T extends ...>. Siehe Generics und Fortgeschrittene Generics.

Im direkten Vergleich: aus synchronized werden Arc und Mutex

Abschnitt betitelt „Im direkten Vergleich: aus synchronized werden Arc und Mutex“

Vier Threads erhöhen einen gemeinsamen Zähler. In Java hängt der Lock am Objekt, und du musst selbst an synchronized denken:

class Counter {
private int count = 0;
synchronized void increment() { count++; }
synchronized int get() { return count; }
}
Counter counter = new Counter();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 4; i++) {
Thread t = new Thread(() -> { for (int j = 0; j < 1000; j++) counter.increment(); });
t.start();
threads.add(t);
}
for (Thread t : threads) t.join();
System.out.println("Zähler = " + counter.get());

In Rust besitzt der Mutex die Zahl. Es gibt keinen Weg an sie heran, ohne vorher zu sperren. Arc (ein atomar referenzgezählter Pointer) lässt mehrere Threads den Mutex teilen:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
// Jeder Thread bekommt sein eigenes Handle auf denselben Mutex.
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() gibt einen Guard zurück; der Lock wird freigegeben, wenn der Guard gedroppt wird.
*counter.lock().unwrap() += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Zähler = {}", *counter.lock().unwrap());
}

Ausgabe:

Zähler = 4000
▶ Selbst ausprobieren

Das ist der Teil, den Java-Entwickler lieben: Wenn du in Java synchronized vergisst, kompiliert der Code und liefert ab und zu falsche Zahlen. Wenn du in Rust versuchst, ein einfaches i32 oder einen Vec zwischen Threads zu teilen und zu verändern, kompiliert das Programm nicht. Data Races werden vom Typsystem abgefangen (über die Traits Send und Sync). Für einen einzelnen Zähler wäre ein AtomicUsize leichter als ein Mutex, genau wie AtomicInteger. Siehe Nebenläufigkeit und Geteilter Zustand.

Alle Java-Entwickler stolpern über dieselben Dinge. Wenn du sie vorher kennst, werden sie kleiner.

  • Einen Wert an eine Funktion zu übergeben, verschiebt ihn. Wenn du in Java ein Objekt übergibst, übergibst du eine Referenz, und du kannst deine Variable weiter benutzen. Wenn du in Rust einen String oder Vec als Wert übergibst, wird er in die Funktion verschoben (ein Move):

    fn greet(name: String) {
    println!("Hallo, {name}!");
    }
    fn main() {
    let name = String::from("Ada");
    greet(name);
    greet(name);
    }
    error[E0382]: use of moved value: `name`
    --> src/main.rs:8:11
    |
    6 | let name = String::from("Ada");
    | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
    7 | greet(name);
    | ---- value moved here
    8 | greet(name);
    | ^^^^ value used here after move

    Der Compiler sagt auf Englisch: name wird benutzt, nachdem sein Wert schon verschoben wurde. Die übliche Lösung ist, die Funktion ausleihen zu lassen: fn greet(name: &str) und der Aufruf greet(&name). Lies zuerst Ownership; dort wird diese Regel erklärt, und E0382 behandelt genau diesen Fehler im Detail.

  • Der Borrow Checker. Du kannst viele Leser (&T) oder genau einen Schreiber (&mut T) gleichzeitig haben, nie beides. Objektgraphen, die in Java ganz normal sind, etwa ein Kind, das auf sein Elternobjekt zurückzeigt, oder viele Objekte, die ein gemeinsames Objekt halten und verändern, brauchen Smart Pointer oder ein anderes Design. Die ersten Wochen können frustrierend sein. Es wird wirklich leichter.

  • Keine Vererbung, kein protected, kein super. Du wirst versucht sein, Klassenhierarchien mit Traits nachzubauen. Probier zuerst Enums und Komposition; die passen meist besser.

  • == vergleicht Werte. Ausnahmsweise eine gute Nachricht: a == b vergleicht bei zwei Strings den Text, der klassische Bug == vs. .equals() kann also nicht passieren. Bei deinen eigenen Structs funktioniert == erst, wenn du PartialEq per derive ableitest oder implementierst.

  • BinaryHeap gibt das größte Element zuerst heraus. Die PriorityQueue in Java liefert das kleinste zuerst. In Rust umschließt du die Elemente mit std::cmp::Reverse, um das Verhalten eines Min-Heaps zu bekommen. Siehe VecDeque und BinaryHeap.

  • Integer-Überlauf passiert nicht still. Integer.MAX_VALUE + 1 läuft in Java leise über. In einem Debug-Build von Rust gibt es eine Panic. Sag mit checked_add, wrapping_add oder saturating_add, was du meinst. Siehe Integer-Überlauf.

  • String vs. &str, und kein charAt. Strings in Rust sind UTF-8, deshalb kompiliert s[0] nicht. Mit s.chars() gehst du Zeichen für Zeichen durch. Funktionen nehmen meist &str entgegen, Structs speichern meist String. Siehe Strings.

  • Keine Methodenüberladung. Du kannst keine zwei print-Methoden mit verschiedenen Parametern haben. Nutze verschiedene Namen (print, print_with_color) oder einen generischen Parameter.

  • Keine Laufzeit-Magie. Kein Classpath-Scanning, kein @Autowired, keine Proxys, die Transaktionen um deine Methoden legen. Alles wird explizit im Code verdrahtet, und Makros laufen beim Kompilieren. Das ist mehr Tipparbeit, aber du siehst immer, was passiert.

  • Kein null und keine Exceptions. Am Anfang fühlen sich Option und Result überall nach Mehrarbeit an. Nach ein paar Wochen merkst du, dass du keine NullPointerException und keinen überraschenden Stack Trace mehr gesehen hast. .unwrap() aufzurufen ist beim Lernen in Ordnung. In echtem Code ist es aber die Rust-Version von „dann stürzt es halt ab“.

  • Kompilierzeiten. Ein kompletter Build lädt alle Abhängigkeiten und kompiliert sie aus dem Quellcode. Das dauert länger als javac. Inkrementelle Builds sind viel schneller, und beim Bearbeiten nutzt du am häufigsten cargo check (das die Codeerzeugung überspringt). Die gute Nachricht am anderen Ende: Das Programm startet sofort, ganz ohne JVM-Aufwärmphase.

  • Async braucht eine Runtime. Es gibt keine virtuellen Threads. Eine async fn tut nichts, bis ein Executor sie ausführt. Deshalb fügen Webprojekte tokio hinzu und schreiben #[tokio::main] über main. Siehe Async/Await.

Du musst diese Seite nicht der Reihe nach lesen. Auf diesem Weg überspringst du, was Java dir schon beigebracht hat, und nimmst dir Zeit für das, was in Rust anders ist:

  1. Erste Schritte mit Rust und Rust-Werkzeuge und Einrichtung: Rust installieren, einen Editor wählen, cargo new ausführen.
  2. Variablen und Datentypen: nur überfliegen, vor allem für let mut und die Zahlentypen.
  3. Ownership, dann Borrowing, dann Slices: die wichtigsten Seiten für dich. Lass dir Zeit.
  4. Strings: String vs. &str.
  5. Structs und Enums, dann Match und Pattern Matching.
  6. Option und Result, dann Eigene Fehlertypen und anyhow und thiserror.
  7. Traits, Generics, Wichtige Traits und Trait-Objekte: deine Interfaces und abstrakten Klassen.
  8. Closures und Iteratoren: deine Lambdas und Streams.
  9. Vektoren, HashMap und VecDeque und BinaryHeap.
  10. Module, Cargo und Crates und Testen.
  11. Lifetimes und Smart Pointer.
  12. Nebenläufigkeit, Geteilter Zustand, Channels und Async/Await.
  13. JSON mit Serde, Logging und Datenbanken in Rust, danach baust du das Web-API-Projekt.
  14. Du willst eine bestehende Java-Anwendung beschleunigen? Lies FFI, um zu sehen, wie Rust extern "C"-Funktionen bereitstellt.

Beide Übungen geben dir etwas Java-Code. Portiere ihn nach Rust.

1. Einen Record und eine Stream-Pipeline portieren

Abschnitt betitelt „1. Einen Record und eine Stream-Pipeline portieren“

Dieser Code gibt aus, welche Studierenden bestanden haben, ob jemand die volle Punktzahl erreicht hat und welches das beste Ergebnis ist. Tipp: Iteratoren haben any (wie anyMatch) und max (das eine Option zurückgibt, wie OptionalInt). Für Collectors.joining sammelst du in einen Vec<&str> und rufst .join(", ") auf.

record Student(String name, int score) {}
List<Student> students = List.of(
new Student("Ada", 91), new Student("Brian", 42),
new Student("Grace", 100), new Student("Ken", 67));
String passed = students.stream()
.filter(s -> s.score() >= 50)
.map(Student::name)
.collect(Collectors.joining(", "));
boolean perfect = students.stream().anyMatch(s -> s.score() == 100);
int best = students.stream().mapToInt(Student::score).max().orElse(0);
System.out.println("bestanden: " + passed);
System.out.println("volle Punktzahl: " + perfect);
System.out.println("Bestwert: " + best);
Lösung anzeigen
#[derive(Debug, Clone, PartialEq)]
struct Student {
name: String,
score: u32,
}
fn student(name: &str, score: u32) -> Student {
Student { name: name.to_string(), score }
}
fn main() {
let students = vec![
student("Ada", 91),
student("Brian", 42),
student("Grace", 100),
student("Ken", 67),
];
let passed: Vec<&str> = students
.iter()
.filter(|s| s.score >= 50)
.map(|s| s.name.as_str())
.collect();
let perfect = students.iter().any(|s| s.score == 100);
let best = students.iter().map(|s| s.score).max().unwrap_or(0);
println!("bestanden: {}", passed.join(", "));
println!("volle Punktzahl: {perfect}");
println!("Bestwert: {best}");
}

Ausgabe:

bestanden: Ada, Grace, Ken
volle Punktzahl: true
Bestwert: 100
▶ Selbst ausprobieren

Die Derives geben dir das, was ein Java-record gratis mitbringt: eine Debug-Darstellung ({:?}), Kopieren (.clone()) und Wertgleichheit (==). Die Hilfsfunktion student übernimmt die Rolle des Record-Konstruktors. Anders als einen Java-Stream kannst du students.iter() so oft aufrufen, wie du willst, weil jeder Aufruf einen neuen Iterator über denselben Vec erzeugt.

Diese Methode liest eine Zeile der Form port=<Zahl> aus einer Konfigurationsdatei. Sie wirft eine Checked Exception ConfigException und verpackt darin die NumberFormatException, die sie von Integer.parseInt bekommt. Portiere sie so, dass read_port ein Result<u16, String> zurückgibt. Tipp: line.split_once('=') gibt eine Option<(&str, &str)> zurück. Mach daraus mit .ok_or_else(...) ein Result. .parse::<u16>() gibt ein Result zurück, und .map_err(...) tauscht dessen Fehler gegen deine eigene Meldung aus – genau so, wie man eine Exception in eine andere verpackt.

class ConfigException extends Exception {
ConfigException(String message) { super(message); }
}
static int readPort(String line) throws ConfigException {
String[] parts = line.split("=", 2);
if (parts.length != 2 || !parts[0].trim().equals("port"))
throw new ConfigException("erwartet: port=<Zahl>, erhalten: '" + line + "'");
try {
return Integer.parseInt(parts[1].trim());
} catch (NumberFormatException e) {
throw new ConfigException("ungültige Portnummer: " + parts[1].trim());
}
}
for (String line : List.of("port = 8080", "port=achtzig", "host=localhost")) {
try { System.out.println("Port ist " + readPort(line)); }
catch (ConfigException e) { System.out.println("Konfigurationsfehler: " + e.getMessage()); }
}
Lösung anzeigen
fn read_port(line: &str) -> Result<u16, String> {
let expected = || format!("erwartet: port=<Zahl>, erhalten: '{line}'");
let (key, value) = line.split_once('=').ok_or_else(expected)?;
if key.trim() != "port" {
return Err(expected());
}
let value = value.trim();
value
.parse::<u16>()
.map_err(|_| format!("ungültige Portnummer: {value}"))
}
fn main() {
for line in ["port = 8080", "port=achtzig", "host=localhost"] {
match read_port(line) {
Ok(port) => println!("Port ist {port}"),
Err(e) => println!("Konfigurationsfehler: {e}"),
}
}
}

Ausgabe:

Port ist 8080
Konfigurationsfehler: ungültige Portnummer: achtzig
Konfigurationsfehler: erwartet: port=<Zahl>, erhalten: 'host=localhost'
▶ Selbst ausprobieren

Aus der Klausel throws ConfigException wurde der Rückgabetyp Result, aus try/catch um parseInt wurde ein einziges map_err, und aus throw wurde Err(...). Dass der Port ein u16 ist, ist ein kleiner Bonus: Eine Zahl über 65535 lehnt schon parse selbst ab, was ein int in Java nicht tun würde. Die kleine Closure expected baut die Fehlermeldung nur, wenn sie gebraucht wird, wie ein Supplier in orElseThrow(() -> ...).

  • Vieles aus Java nimmst du mit: Generics, Interfaces (als Traits), Lambdas (als Closures) und Stream-Pipelines (als Iteratoren).
  • Die großen Umstellungen sind Ownership statt Garbage Collector, Option statt null, Result und ? statt Exceptions sowie Traits plus Komposition statt Vererbung.
  • Generics werden für jeden Typ einzeln kompiliert (Monomorphisierung). Es gibt also keine Type Erasure und kein Boxing von Zahlen.
  • Daten, die sich Threads teilen, stecken in einem Mutex (umschlossen von einem Arc), und ein vergessener Lock ist ein Compilerfehler, kein Race.
  • Cargo ersetzt Maven oder Gradle; clippy und rustfmt ersetzen Checkstyle, SpotBugs und Formatter; Tests sind eingebaut.
  • Statt Spring Boot schau dir axum oder actix-web an, statt JPA SeaORM oder Diesel, statt JDBC und jOOQ sqlx, und statt Jackson nimmst du serde.

Kurzer Check

1. Eine abstrakte Java-Klasse hat eine abstrakte Methode und eine konkrete Methode, die sie aufruft. Was ist der übliche Ersatz in Rust?

2. Die PriorityQueue in Java gibt das kleinste Element zuerst zurück. Was gibt BinaryHeap::pop in Rust zurück?

3. Warum kann eine generische Funktion in Rust T::default() aufrufen, während man in Java nicht new T() schreiben kann?

4. Du teilst einen einfachen Vec zwischen Threads und versuchst, ohne Mutex etwas hinzuzufügen. Was passiert in Rust?

Punkte: 0 / 4

  • Die Programmiersprache Rust – Das Rust-Buch: die deutsche Übersetzung des offiziellen Buchs. Für Java-Entwickler sind die Kapitel 4 (Ownership), 9 (Fehlerbehandlung), 10 (Generics und Traits) und 16 (Nebenläufigkeit) am wichtigsten.
  • std::collections (auf Englisch): der eigene Leitfaden der Standardbibliothek zur Wahl zwischen Vec, VecDeque, HashMap, BTreeMap und den anderen.
  • JEP 454: Foreign Function & Memory API (auf Englisch): das Feature aus JDK 22, mit dem du native Bibliotheken – auch Rust-Bibliotheken – aus Java aufrufst, ohne JNI-Code zu schreiben.
  • Das Crate jni auf docs.rs (auf Englisch): Rust-Bindings für JNI, falls du Rust aus älteren JDKs aufrufen oder zurück nach Java rufen musst.
  • Are We Web Yet? (auf Englisch): eine Übersicht aus der Community über die Web-Frameworks, Datenbankbibliotheken und mehr in Rust – falls du deinen Ersatz für den Spring-Boot-Stack suchst.