Agent Skills: Convert Java to Rust

Convert Java code to idiomatic Rust. Use when migrating Java projects to Rust, translating Java patterns to idiomatic Rust, or refactoring Java codebases. Extends meta-convert-dev with Java-to-Rust specific patterns.

UncategorizedID: arustydev/ai/convert-java-rust

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/convert-java-rust

Skill Files

Browse the full folder contents for convert-java-rust.

Download Skill

Loading file tree…

content/skills/convert-java-rust/SKILL.md

Skill Metadata

Name
convert-java-rust
Description
Convert Java code to idiomatic Rust. Use when migrating Java projects to Rust, translating Java patterns to idiomatic Rust, or refactoring Java codebases. Extends meta-convert-dev with Java-to-Rust specific patterns.

Convert Java to Rust

Convert Java code to idiomatic Rust. This skill extends meta-convert-dev with Java-to-Rust specific type mappings, idiom translations, and tooling.

This Skill Extends

  • meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: Java types → Rust types
  • Idiom translations: Java patterns → idiomatic Rust
  • Error handling: Java exceptions → Rust Result<T, E>
  • Concurrency: Java threads/ExecutorService → Rust async/await
  • Memory/Ownership: Garbage collection → ownership/borrowing
  • OOP patterns: Java classes/inheritance → Rust structs/traits
  • Null safety: null references → Option<T>
  • Metaprogramming: Java annotations/reflection → Rust macros/traits

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Java language fundamentals - see lang-java-dev
  • Rust language fundamentals - see lang-rust-dev
  • Reverse conversion (Rust → Java) - see convert-rust-java

Quick Reference

| Java | Rust | Notes | |----------|----------|-------| | String | String / &str | Owned vs borrowed | | int | i32 | 32-bit signed integer | | long | i64 | 64-bit signed integer | | float | f32 | 32-bit float | | double | f64 | 64-bit float | | boolean | bool | Direct mapping | | List<T> | Vec<T> | Growable array | | Map<K, V> | HashMap<K, V> | Hash table | | Set<T> | HashSet<T> | Unique collection | | Optional<T> | Option<T> | Nullable values | | null | None in Option<T> | Explicit nullability | | throws Exception | Result<T, E> | Type-safe errors | | interface | trait | Behavioral contracts | | class | struct + impl | Data + behavior | | @Override | No annotation needed | Traits enforce signature | | synchronized | Mutex<T> / RwLock<T> | Explicit locking | | Thread | std::thread / tokio::task | OS threads / async tasks |

When Converting Code

  1. Analyze source thoroughly before writing target
  2. Map types first - create type equivalence table
  3. Preserve semantics over syntax similarity
  4. Adopt target idioms - don't write "Java code in Rust syntax"
  5. Handle edge cases - null checks, error paths, resource cleanup
  6. Test equivalence - same inputs → same outputs

Type System Mapping

Primitive Types

| Java | Rust | Notes | |----------|----------|-------| | boolean | bool | Direct mapping | | byte | i8 | 8-bit signed integer | | short | i16 | 16-bit signed integer | | int | i32 | 32-bit signed integer (most common) | | long | i64 | 64-bit signed integer | | float | f32 | 32-bit floating point | | double | f64 | 64-bit floating point (most common) | | char | char | Unicode scalar value (4 bytes in Rust) | | void | () | Unit type |

Note: Java char is 16-bit UTF-16, Rust char is 32-bit Unicode scalar.

Boxed Primitives

| Java | Rust | Notes | |----------|----------|-------| | Integer | i32 | Primitives don't need boxing in Rust | | Long | i64 | No autoboxing/unboxing | | Double | f64 | Direct primitive usage | | Boolean | bool | No wrapper types needed | | Character | char | Direct usage |

String Types

| Java | Rust | Notes | |----------|----------|-------| | String | String | Owned, heap-allocated UTF-8 | | String (param) | &str | Borrowed string slice for parameters | | StringBuilder | String | Use String with push_str, push | | char[] | Vec<char> | Character array | | byte[] | Vec<u8> | Byte array |

Collection Types

| Java | Rust | Notes | |----------|----------|-------| | ArrayList<T> | Vec<T> | Growable array | | LinkedList<T> | std::collections::LinkedList<T> | Doubly-linked list (rarely used) | | HashMap<K, V> | HashMap<K, V> | Hash table, K must be Hash + Eq | | TreeMap<K, V> | BTreeMap<K, V> | Ordered map, K must be Ord | | HashSet<T> | HashSet<T> | Unique collection | | TreeSet<T> | BTreeSet<T> | Ordered unique collection | | ArrayDeque<T> | VecDeque<T> | Double-ended queue | | PriorityQueue<T> | BinaryHeap<T> | Max-heap by default | | T[] | Vec<T> | Dynamic array | | T[] (fixed) | [T; N] | Fixed-size array |

Nullable Types

| Java | Rust | Notes | |----------|----------|-------| | @Nullable T | Option<T> | Explicit nullability | | @NonNull T | T | Non-null by default in Rust | | Optional<T> | Option<T> | Direct mapping | | null | None | Null variant |

Error Types

| Java | Rust | Notes | |----------|----------|-------| | throws Exception | Result<T, Error> | Type-safe error handling | | try/catch | match result or ? | Pattern matching or propagation | | Throwable | Error trait | Error interface | | RuntimeException | panic! / Result | Unrecoverable vs recoverable |

Composite Types

| Java | Rust | Notes | |----------|----------|-------| | class Foo { } | struct Foo { } + impl Foo { } | Data + behavior separation | | interface Bar { } | trait Bar { } | Behavioral contract | | enum Status { } | enum Status { } | Algebraic data types in Rust | | record Point(int x, int y) | struct Point { x: i32, y: i32 } | Immutable by default in Rust | | Pair<K, V> | (K, V) | Tuple |

Generic Types

| Java | Rust | Notes | |----------|----------|-------| | <T> | <T> | Type parameter | | <T extends Foo> | <T: Foo> | Bounded type parameter | | <T super Foo> | No direct equivalent | Use trait objects | | <?> | _ (type inference) | Wildcard | | List<? extends T> | Vec<impl Trait> | Bounded wildcard | | Class<T> | PhantomData<T> | Type token |


Idiom Translation

Pattern 1: Null Checking

Java:

public String getUserName(User user) {
    if (user == null) {
        return "Anonymous";
    }
    if (user.getName() == null || user.getName().isEmpty()) {
        return "Anonymous";
    }
    return user.getName();
}

Rust:

fn get_user_name(user: Option<&User>) -> &str {
    user.and_then(|u| {
        if u.name.is_empty() {
            None
        } else {
            Some(u.name.as_str())
        }
    })
    .unwrap_or("Anonymous")
}

// Or more idiomatically with pattern matching:
fn get_user_name(user: Option<&User>) -> &str {
    match user {
        Some(u) if !u.name.is_empty() => &u.name,
        _ => "Anonymous",
    }
}

Why this translation:

  • Option<T> makes nullability explicit in the type system
  • No null pointer exceptions possible at runtime
  • Combinators like and_then and unwrap_or are idiomatic
  • Pattern matching with guards is more expressive
  • Borrowed references avoid unnecessary cloning

Pattern 2: Exception Handling

Java:

public Config readConfig(String path) throws IOException {
    String content = Files.readString(Path.of(path));
    return parseConfig(content);
}

public void processConfig(String path) {
    try {
        Config config = readConfig(path);
        apply(config);
    } catch (IOException e) {
        System.err.println("Failed to read config: " + e.getMessage());
    }
}

Rust:

use std::fs;
use std::path::Path;

fn read_config(path: &Path) -> Result<Config, std::io::Error> {
    let content = fs::read_to_string(path)?;
    parse_config(&content)
}

fn process_config(path: &Path) {
    match read_config(path) {
        Ok(config) => apply(config),
        Err(e) => eprintln!("Failed to read config: {}", e),
    }
}

// Or with the ? operator in a Result-returning function:
fn process_config(path: &Path) -> Result<(), std::io::Error> {
    let config = read_config(path)?;
    apply(config);
    Ok(())
}

Why this translation:

  • Result<T, E> encodes success/failure in the type system
  • The ? operator propagates errors ergonomically (like Java throws)
  • Pattern matching makes error handling explicit
  • No hidden control flow (exceptions jumping up the stack)
  • Errors are values, not exceptional control flow

Pattern 3: Optional Chaining

Java:

public String getCityName(User user) {
    return Optional.ofNullable(user)
        .map(User::getAddress)
        .map(Address::getCity)
        .map(City::getName)
        .orElse("Unknown");
}

Rust:

fn get_city_name(user: Option<&User>) -> &str {
    user.and_then(|u| u.address.as_ref())
        .and_then(|a| a.city.as_ref())
        .map(|c| c.name.as_str())
        .unwrap_or("Unknown")
}

// Or with pattern matching:
fn get_city_name(user: Option<&User>) -> &str {
    match user {
        Some(User {
            address: Some(Address {
                city: Some(City { name, .. }),
                ..
            }),
            ..
        }) => name,
        _ => "Unknown",
    }
}

Why this translation:

  • Direct mapping from Java Optional to Rust Option
  • Rust's Option methods are similar to Java's
  • Pattern matching can destructure nested Options
  • Borrowed references avoid cloning

Pattern 4: Stream/Iterator Operations

Java:

List<String> names = users.stream()
    .filter(user -> user.getAge() > 18)
    .map(User::getName)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

int totalAge = users.stream()
    .mapToInt(User::getAge)
    .sum();

Rust:

let names: Vec<String> = users
    .iter()
    .filter(|user| user.age > 18)
    .map(|user| user.name.to_uppercase())
    .collect();

let total_age: i32 = users
    .iter()
    .map(|user| user.age)
    .sum();

Why this translation:

  • Rust iterators are zero-cost abstractions (like Java streams)
  • Similar combinator API: filter, map, collect, sum
  • Rust iterators are lazy (like Java streams)
  • No need for specialized primitive streams (mapToInt, etc.)
  • More explicit borrowing with iter() vs into_iter()

Pattern 5: Builder Pattern

Java:

public class Request {
    private final String url;
    private final String method;
    private final Map<String, String> headers;

    private Request(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = builder.headers;
    }

    public static class Builder {
        private String url;
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();

        public Builder url(String url) {
            this.url = url;
            return this;
        }

        public Builder method(String method) {
            this.method = method;
            return this;
        }

        public Builder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

        public Request build() {
            return new Request(this);
        }
    }
}

// Usage
Request request = new Request.Builder()
    .url("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .build();

Rust:

use std::collections::HashMap;

struct Request {
    url: String,
    method: String,
    headers: HashMap<String, String>,
}

struct RequestBuilder {
    url: String,
    method: String,
    headers: HashMap<String, String>,
}

impl RequestBuilder {
    fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            method: String::from("GET"),
            headers: HashMap::new(),
        }
    }

    fn method(mut self, method: impl Into<String>) -> Self {
        self.method = method.into();
        self
    }

    fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.insert(key.into(), value.into());
        self
    }

    fn build(self) -> Request {
        Request {
            url: self.url,
            method: self.method,
            headers: self.headers,
        }
    }
}

// Usage
let request = RequestBuilder::new("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .build();

Why this translation:

  • Similar builder pattern structure
  • Rust uses self consumption for method chaining
  • No need for nested Builder class (separate struct)
  • impl Into<String> accepts both String and &str
  • More ergonomic with fewer allocations

Pattern 6: Interface Implementation

Java:

interface Reader {
    int read(byte[] buffer) throws IOException;
}

class FileReader implements Reader {
    private String path;

    @Override
    public int read(byte[] buffer) throws IOException {
        // Implementation
        return buffer.length;
    }
}

void processReader(Reader reader) throws IOException {
    byte[] buffer = new byte[1024];
    int bytesRead = reader.read(buffer);
    // Process buffer
}

Rust:

use std::io;

trait Reader {
    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize>;
}

struct FileReader {
    path: String,
}

impl Reader for FileReader {
    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
        // Implementation
        Ok(buffer.len())
    }
}

fn process_reader<R: Reader>(reader: &mut R) -> io::Result<()> {
    let mut buffer = vec![0u8; 1024];
    let bytes_read = reader.read(&mut buffer)?;
    // Process buffer
    Ok(())
}

Why this translation:

  • Rust traits are explicitly implemented with impl Trait for Type
  • Generic functions use trait bounds (<R: Reader>)
  • Mutable borrows (&mut) make mutation explicit
  • The ? operator replaces throws declarations
  • No @Override annotation needed (enforced by trait)

Pattern 7: Inheritance vs Composition

Java:

abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public abstract void makeSound();

    public void sleep() {
        System.out.println(name + " is sleeping");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

Rust:

// Use traits instead of abstract classes
trait Animal {
    fn name(&self) -> &str;
    fn make_sound(&self);

    // Default implementation (like concrete methods in abstract class)
    fn sleep(&self) {
        println!("{} is sleeping", self.name());
    }
}

struct Dog {
    name: String,
}

impl Animal for Dog {
    fn name(&self) -> &str {
        &self.name
    }

    fn make_sound(&self) {
        println!("Woof!");
    }
}

// Alternative: Composition with delegation
struct AnimalData {
    name: String,
}

struct Dog {
    data: AnimalData,
}

impl Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }

    fn sleep(&self) {
        println!("{} is sleeping", self.data.name);
    }
}

Why this translation:

  • Rust favors composition over inheritance
  • Traits define shared behavior without state
  • No virtual method dispatch overhead by default
  • More flexible than rigid class hierarchies
  • Prefer trait bounds over inheritance for polymorphism

Pattern 8: Static Methods and Factory Patterns

Java:

class User {
    private String name;
    private int age;

    private User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static User create(String name, int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        return new User(name, age);
    }

    public static User createAnonymous() {
        return new User("Anonymous", 0);
    }
}

Rust:

struct User {
    name: String,
    age: u32,
}

impl User {
    // Associated function (like static method)
    fn new(name: impl Into<String>, age: u32) -> Self {
        Self {
            name: name.into(),
            age,
        }
    }

    // Factory method with validation
    fn create(name: impl Into<String>, age: i32) -> Result<Self, &'static str> {
        if age < 0 {
            return Err("Age cannot be negative");
        }
        Ok(Self {
            name: name.into(),
            age: age as u32,
        })
    }

    // Named constructor
    fn anonymous() -> Self {
        Self {
            name: String::from("Anonymous"),
            age: 0,
        }
    }
}

// Usage
let user = User::new("Alice", 30);
let user2 = User::create("Bob", 25)?;
let anon = User::anonymous();

Why this translation:

  • Rust uses associated functions instead of static methods
  • No static keyword needed (no self parameter)
  • Factory methods return Result for validation
  • Named constructors are idiomatic (new, with_capacity, etc.)
  • Private constructors not needed (use pub selectively)

Paradigm Translation: OOP → Systems Programming

Mental Model Shift: Object-Oriented → Ownership-Based

| Java Concept | Rust Approach | Key Insight | |------------------|-------------------|-------------| | Class with state | struct + impl blocks | Data and behavior separated but associated | | Inheritance | Composition + traits | Favor composition over deep hierarchies | | Polymorphism (subtyping) | Trait objects (dyn Trait) or generics | Static dispatch (generics) vs dynamic (trait objects) | | Encapsulation | Module visibility + pub | Privacy at module level, not class level | | Constructor | Associated function new() | No special constructor syntax | | Garbage collection | Ownership + borrowing | Compiler-enforced memory safety | | Null references | Option<T> | Null safety in type system | | Exceptions | Result<T, E> | Errors as values |

Memory Management Mental Model

| Java Model | Rust Model | Conceptual Translation | |----------------|----------------|------------------------| | Heap allocation automatic | Explicit (Box, Vec, String) | Ownership makes allocation visible | | GC reclaims memory | Automatic via RAII (Drop) | Deterministic cleanup at scope end | | References everywhere | Borrows (&, &mut) | Explicit lifetime tracking | | No manual cleanup | No manual cleanup | Same safety, different mechanism | | Shared mutable state | Mutex, RefCell, interior mutability | Mutation rules enforced by compiler |

Concurrency Mental Model

| Java Model | Rust Model | Conceptual Translation | |----------------|----------------|------------------------| | synchronized blocks | Mutex<T> / RwLock<T> | Lock protects data, not code | | Thread-safe by convention | Thread-safe by type system (Send, Sync) | Compiler prevents data races | | ExecutorService | tokio::Runtime / async-std | Async runtime for task scheduling | | Future<T> (Java 8+) | Future trait + async/await | First-class async support | | Heavyweight threads | OS threads or lightweight async tasks | Choose cost based on use case |


Error Handling

Java Exception Model → Rust Result Model

Java uses exceptions for both expected and unexpected errors. Rust distinguishes between recoverable errors (Result) and unrecoverable errors (panic!).

Mapping:

| Java | Rust | Use Case | |------|------|----------| | Checked exceptions | Result<T, E> | Recoverable errors (expected) | | Unchecked exceptions | Result<T, E> or panic! | Recoverable or programmer errors | | throws clause | Return type Result<T, E> | Signature shows fallibility | | try/catch | match result or if let Err | Explicit error handling | | try/catch (propagate) | ? operator | Early return on error | | finally | RAII / Drop trait | Automatic cleanup | | throw new Exception() | Err(...) or panic!() | Return error or abort |

Pattern: Multiple Exception Types

Java:

public Data processFile(String path) throws IOException, ParseException {
    String content = Files.readString(Path.of(path));
    return parseData(content);
}

try {
    Data data = processFile("config.json");
} catch (IOException e) {
    System.err.println("IO error: " + e.getMessage());
} catch (ParseException e) {
    System.err.println("Parse error: " + e.getMessage());
}

Rust:

use std::fs;
use std::path::Path;

// Define error enum to combine multiple error types
#[derive(Debug)]
enum ProcessError {
    Io(std::io::Error),
    Parse(String),
}

impl From<std::io::Error> for ProcessError {
    fn from(e: std::io::Error) -> Self {
        ProcessError::Io(e)
    }
}

fn process_file(path: &Path) -> Result<Data, ProcessError> {
    let content = fs::read_to_string(path)?;  // Auto-converts via From
    parse_data(&content).map_err(ProcessError::Parse)
}

match process_file(Path::new("config.json")) {
    Ok(data) => { /* use data */ },
    Err(ProcessError::Io(e)) => eprintln!("IO error: {}", e),
    Err(ProcessError::Parse(e)) => eprintln!("Parse error: {}", e),
}

// Or use anyhow/error-stack for simplified error handling:
use anyhow::Result;

fn process_file(path: &Path) -> Result<Data> {
    let content = fs::read_to_string(path)?;
    let data = parse_data(&content)?;
    Ok(data)
}

Why this translation:

  • Custom error enums replace multiple exception types
  • From trait enables automatic conversion with ?
  • Pattern matching handles different error cases
  • anyhow crate provides ergonomic error handling for applications
  • thiserror crate simplifies custom error types

Pattern: Try-with-Resources

Java:

try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    String line = reader.readLine();
    // Process line
} catch (IOException e) {
    System.err.println("Error: " + e.getMessage());
}

Rust:

use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;

fn read_first_line(path: &Path) -> io::Result<String> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let mut line = String::new();
    reader.lines().next()
        .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "Empty file"))?
}

// File is automatically closed when it goes out of scope (Drop trait)
match read_first_line(Path::new("file.txt")) {
    Ok(line) => println!("{}", line),
    Err(e) => eprintln!("Error: {}", e),
}

Why this translation:

  • Rust's RAII (Drop trait) automatically cleans up resources
  • No need for explicit try-with-resources syntax
  • Scope-based cleanup is deterministic
  • More type-safe than runtime resource management

Concurrency Patterns

Java Concurrency → Rust Concurrency

Rust provides both traditional OS threads and lightweight async/await concurrency.

Pattern 1: Basic Threading

Java:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Counter counter = new Counter();

List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    Thread t = new Thread(() -> {
        for (int j = 0; j < 1000; j++) {
            counter.increment();
        }
    });
    threads.add(t);
    t.start();
}

for (Thread t : threads) {
    t.join();
}

System.out.println("Count: " + counter.getCount());

Rust:

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        for _ in 0..1000 {
            let mut num = counter.lock().unwrap();
            *num += 1;
        }
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

println!("Count: {}", *counter.lock().unwrap());

Why this translation:

  • Arc<Mutex<T>> combines reference counting (Arc) with mutual exclusion (Mutex)
  • Mutex protects the data, not the code block
  • Type system prevents data races at compile time
  • Explicit cloning makes shared ownership visible
  • move closure captures ownership

Pattern 2: ExecutorService → Tokio Async

Java:

ExecutorService executor = Executors.newFixedThreadPool(4);

List<Future<String>> futures = new ArrayList<>();
for (String url : urls) {
    Future<String> future = executor.submit(() -> fetchUrl(url));
    futures.add(future);
}

List<String> results = new ArrayList<>();
for (Future<String> future : futures) {
    try {
        results.add(future.get());
    } catch (InterruptedException | ExecutionException e) {
        System.err.println("Error: " + e.getMessage());
    }
}

executor.shutdown();

Rust:

use tokio;

#[tokio::main]
async fn main() {
    let urls = vec!["url1", "url2", "url3"];

    let tasks: Vec<_> = urls
        .into_iter()
        .map(|url| tokio::spawn(async move { fetch_url(url).await }))
        .collect();

    let mut results = Vec::new();
    for task in tasks {
        match task.await {
            Ok(result) => results.push(result),
            Err(e) => eprintln!("Error: {}", e),
        }
    }
}

async fn fetch_url(url: &str) -> String {
    // Async HTTP request
    String::from(url)
}

Why this translation:

  • Tokio provides async runtime (like ExecutorService)
  • async/await syntax is more ergonomic than futures
  • Tasks are lightweight (like virtual threads in Java 21)
  • No need for explicit thread pool management
  • Type-safe async with Future trait

Pattern 3: CompletableFuture → Async/Await

Java:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> fetchData());
CompletableFuture<Integer> future2 = future1.thenApply(data -> parseData(data));
CompletableFuture<Void> future3 = future2.thenAccept(value -> processValue(value));

future3.exceptionally(ex -> {
    System.err.println("Error: " + ex.getMessage());
    return null;
});

future3.join();

Rust:

use tokio;

#[tokio::main]
async fn main() {
    match fetch_and_process().await {
        Ok(()) => println!("Success"),
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn fetch_and_process() -> Result<(), Box<dyn std::error::Error>> {
    let data = fetch_data().await?;
    let value = parse_data(&data).await?;
    process_value(value).await?;
    Ok(())
}

async fn fetch_data() -> Result<String, std::io::Error> {
    // Async operation
    Ok(String::from("data"))
}

async fn parse_data(data: &str) -> Result<i32, std::num::ParseIntError> {
    data.parse()
}

async fn process_value(value: i32) -> Result<(), std::io::Error> {
    println!("Value: {}", value);
    Ok(())
}

Why this translation:

  • async/await is more readable than chaining futures
  • ? operator propagates errors through async chain
  • No need for explicit exceptionally handlers
  • Type-safe error handling with Result
  • Composable async functions

Memory & Ownership

Java GC → Rust Ownership

The biggest paradigm shift from Java to Rust is memory management. Java uses garbage collection; Rust uses compile-time ownership tracking.

Core Ownership Rules

  1. Each value has a single owner (unlike Java where references are shared freely)
  2. When the owner goes out of scope, the value is dropped (like Java finalization, but deterministic)
  3. Values can be borrowed immutably or mutably (unlike Java where everything is mutable unless final)

Pattern 1: Ownership Transfer

Java:

// Java freely shares references
List<String> list1 = new ArrayList<>();
list1.add("hello");
List<String> list2 = list1;  // Both point to same list
list2.add("world");
System.out.println(list1.size());  // 2

Rust:

// Rust transfers ownership by default
let mut list1 = vec![String::from("hello")];
let list2 = list1;  // Ownership transferred to list2
// println!("{:?}", list1);  // Compile error: list1 moved

list2.push(String::from("world"));
println!("{}", list2.len());  // 2

// To share, use borrowing:
let mut list1 = vec![String::from("hello")];
let list2 = &list1;  // Borrow immutably
println!("{:?}", list1);  // OK: list1 still owns the data
println!("{:?}", list2);  // OK: borrowing

Why this matters:

  • Rust prevents use-after-move bugs at compile time
  • No runtime overhead (no reference counting)
  • Clear ownership semantics

Pattern 2: Cloning vs Borrowing

Java:

void processData(List<String> data) {
    // Can mutate the list
    data.add("new item");
}

List<String> myData = new ArrayList<>();
processData(myData);  // myData is modified

Rust:

// Option 1: Borrow mutably
fn process_data(data: &mut Vec<String>) {
    data.push(String::from("new item"));
}

let mut my_data = vec![];
process_data(&mut my_data);  // my_data is modified

// Option 2: Borrow immutably (cannot modify)
fn read_data(data: &Vec<String>) {
    for item in data {
        println!("{}", item);
    }
    // data.push(...);  // Compile error: cannot mutate
}

read_data(&my_data);

// Option 3: Take ownership (consumes the value)
fn consume_data(data: Vec<String>) {
    // data is moved here, caller loses access
}

// my_data is gone after this
consume_data(my_data);
// println!("{:?}", my_data);  // Compile error

Why this translation:

  • Explicit borrowing prevents accidental mutation
  • Ownership transfer is visible in function signatures
  • Compiler enforces no data races or aliasing bugs

Pattern 3: Reference Counting (Rc/Arc)

Java:

// Java automatically manages shared references
class Node {
    int value;
    List<Node> children;
}

Node parent = new Node();
Node child1 = new Node();
Node child2 = new Node();
parent.children.add(child1);
parent.children.add(child2);
// All nodes share references, GC cleans up when unreachable

Rust:

use std::rc::Rc;

struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}

let child1 = Rc::new(Node {
    value: 1,
    children: vec![],
});

let child2 = Rc::new(Node {
    value: 2,
    children: vec![],
});

let parent = Node {
    value: 0,
    children: vec![Rc::clone(&child1), Rc::clone(&child2)],
};

// Rc provides shared ownership with reference counting
// Arc for thread-safe reference counting

Why this translation:

  • Rc<T> (single-threaded) or Arc<T> (thread-safe) for shared ownership
  • Explicit cloning makes reference counting visible
  • No cycles by default (use Weak<T> for weak references)
  • More predictable than GC

Metaprogramming

Java Annotations/Reflection → Rust Macros/Traits

Java uses runtime reflection and annotations. Rust uses compile-time macros and traits.

Pattern 1: Annotations → Derive Macros

Java:

@Data  // Lombok annotation
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(unique = true)
    private String email;
}

Rust:

use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Option<u64>,
    pub name: String,
    pub email: String,
}

// Or with a custom derive macro for ORM:
#[derive(Debug, Entity)]
#[table(name = "users")]
pub struct User {
    #[id]
    #[generated]
    pub id: Option<u64>,

    #[column(nullable = false)]
    pub name: String,

    #[column(unique = true)]
    pub email: String,
}

Why this translation:

  • Derive macros generate code at compile time (no runtime reflection)
  • Type-safe (errors caught during compilation)
  • Zero runtime overhead
  • serde is the standard serialization framework

Pattern 2: Reflection → Trait Objects

Java:

void processObject(Object obj) {
    if (obj instanceof String) {
        String s = (String) obj;
        System.out.println("String: " + s);
    } else if (obj instanceof Integer) {
        Integer i = (Integer) obj;
        System.out.println("Integer: " + i);
    }
}

// Or with reflection:
Class<?> clazz = obj.getClass();
Method method = clazz.getMethod("toString");
Object result = method.invoke(obj);

Rust:

// Prefer enums over runtime type checking
enum Value {
    String(String),
    Integer(i32),
}

fn process_value(value: Value) {
    match value {
        Value::String(s) => println!("String: {}", s),
        Value::Integer(i) => println!("Integer: {}", i),
    }
}

// Or use trait objects for polymorphism:
trait Printable {
    fn print(&self);
}

impl Printable for String {
    fn print(&self) {
        println!("String: {}", self);
    }
}

impl Printable for i32 {
    fn print(&self) {
        println!("Integer: {}", self);
    }
}

fn process_printable(obj: &dyn Printable) {
    obj.print();
}

Why this translation:

  • Rust avoids runtime reflection (unsafe and slow)
  • Enums are type-safe alternatives to instanceof
  • Trait objects (dyn Trait) for runtime polymorphism
  • Most metaprogramming done at compile time with macros

Pattern 3: Custom Annotations → Attribute Macros

Java:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cached {
    int ttl() default 60;
}

public class Service {
    @Cached(ttl = 300)
    public Data fetchData(String key) {
        // Method implementation
    }
}

// Runtime processing with reflection
for (Method method : Service.class.getDeclaredMethods()) {
    if (method.isAnnotationPresent(Cached.class)) {
        Cached cached = method.getAnnotation(Cached.class);
        int ttl = cached.ttl();
        // Setup caching
    }
}

Rust:

// Define attribute macro (in a proc-macro crate)
#[proc_macro_attribute]
pub fn cached(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Parse ttl from attr
    // Generate wrapper code that caches results
    // Return modified function
}

// Usage
#[cached(ttl = 300)]
pub fn fetch_data(key: &str) -> Data {
    // Method implementation
}

// Macro expands at compile time to:
pub fn fetch_data(key: &str) -> Data {
    // Check cache
    // If miss, call original function and cache result
}

Why this translation:

  • Rust macros run at compile time
  • No runtime reflection overhead
  • Type-safe macro expansion
  • More powerful than annotations (can generate arbitrary code)

Serialization

Jackson → Serde

Java uses Jackson for JSON serialization. Rust uses Serde, which is more flexible and type-safe.

Pattern: JSON Serialization

Java:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.*;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Config {
    @JsonProperty("api_key")
    private String apiKey;

    private String endpoint;

    @JsonIgnore
    private String internalState;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private LocalDateTime createdAt;

    // Getters and setters
}

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(config);
Config parsed = mapper.readValue(json, Config.class);

Rust:

use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Config {
    #[serde(rename = "api_key")]
    api_key: String,

    endpoint: String,

    #[serde(skip)]
    internal_state: String,

    #[serde(skip_serializing)]
    created_at: DateTime<Utc>,
}

// Serialization
let json = serde_json::to_string(&config)?;
let pretty_json = serde_json::to_string_pretty(&config)?;

// Deserialization
let parsed: Config = serde_json::from_str(&json)?;

Why this translation:

  • Serde is compile-time type-safe
  • Zero runtime overhead
  • More flexible than Jackson (works with JSON, YAML, TOML, MessagePack, etc.)
  • Errors caught at compile time, not runtime

Build and Dependencies

Maven/Gradle → Cargo

Java uses Maven or Gradle. Rust uses Cargo, which is simpler and faster.

Pattern: Dependency Management

Java (Maven):

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.0</version>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Java (Gradle):

dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.15.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}

Rust (Cargo.toml):

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
criterion = "0.5"

Why this translation:

  • Cargo is simpler (one file vs XML/Groovy)
  • Built-in features system
  • Faster dependency resolution
  • Lock file (Cargo.lock) ensures reproducible builds

Common Commands

| Maven/Gradle | Cargo | Purpose | |--------------|-------|---------| | mvn compile / gradle build | cargo build | Compile | | mvn test / gradle test | cargo test | Run tests | | mvn package / gradle jar | cargo build --release | Build release | | mvn install / gradle publishToMavenLocal | cargo install | Install binary | | mvn clean / gradle clean | cargo clean | Clean build | | mvn dependency:tree / gradle dependencies | cargo tree | Show deps |


Testing

JUnit → Cargo Test

Java uses JUnit for testing. Rust has built-in testing support.

Pattern: Unit Tests

Java:

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

class CalculatorTest {
    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    void shouldAddTwoNumbers() {
        int result = calculator.add(2, 3);
        assertEquals(5, result);
    }

    @Test
    void shouldThrowOnDivisionByZero() {
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });
    }
}

Rust:

struct Calculator;

impl Calculator {
    fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }

    fn divide(&self, a: i32, b: i32) -> Result<i32, &'static str> {
        if b == 0 {
            Err("Division by zero")
        } else {
            Ok(a / b)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn should_add_two_numbers() {
        let calculator = Calculator;
        let result = calculator.add(2, 3);
        assert_eq!(result, 5);
    }

    #[test]
    fn should_return_error_on_division_by_zero() {
        let calculator = Calculator;
        let result = calculator.divide(10, 0);
        assert!(result.is_err());
    }
}

Why this translation:

  • Built-in test framework (no external dependency)
  • Tests live next to code in #[cfg(test)] modules
  • Assertions are macros: assert!, assert_eq!, assert_ne!
  • No need for setup/teardown (use RAII pattern)

Common Pitfalls

Pitfall 1: Assuming Null Everywhere

Problem: In Java, any reference can be null. In Rust, values are non-null by default.

Java:

String name = getName();  // Might be null
int length = name.length();  // NullPointerException!

Rust:

// Wrong: trying to use null
let name = get_name();  // Returns Option<String>
// let length = name.len();  // Compile error: Option has no len()

// Right: handle Option explicitly
let name = get_name();
let length = name.map(|s| s.len()).unwrap_or(0);

// Or with pattern matching:
match get_name() {
    Some(name) => println!("Length: {}", name.len()),
    None => println!("No name"),
}

Pitfall 2: Mutating Shared References

Problem: In Java, shared references can be mutated freely. Rust enforces exclusive mutation.

Java:

List<String> list = new ArrayList<>();
List<String> ref1 = list;
List<String> ref2 = list;
ref1.add("hello");  // OK
ref2.add("world");  // OK

Rust:

// Wrong: multiple mutable references
let mut list = vec![];
let ref1 = &mut list;
let ref2 = &mut list;  // Compile error: cannot borrow as mutable more than once
ref1.push("hello");
ref2.push("world");

// Right: use immutable borrows or take ownership
let mut list = vec![];
list.push("hello");
list.push("world");

// Or use interior mutability (RefCell, Mutex)
use std::cell::RefCell;
let list = RefCell::new(vec![]);
list.borrow_mut().push("hello");
list.borrow_mut().push("world");

Pitfall 3: Expecting Inheritance

Problem: Java relies heavily on class inheritance. Rust favors composition and traits.

Java:

class Animal { }
class Dog extends Animal { }

Animal animal = new Dog();  // Polymorphism via inheritance

Rust:

// Wrong: trying to use inheritance
// Rust has no inheritance!

// Right: use traits
trait Animal {
    fn make_sound(&self);
}

struct Dog;

impl Animal for Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }
}

fn process_animal(animal: &dyn Animal) {
    animal.make_sound();
}

let dog = Dog;
process_animal(&dog);

Pitfall 4: Checked Exceptions vs Result

Problem: Java uses checked exceptions that must be declared. Rust uses Result as a return type.

Java:

// Java: throws in signature
public Data readFile(String path) throws IOException {
    // ...
}

Rust:

// Wrong: trying to throw exceptions
// Rust has no exceptions!

// Right: return Result
fn read_file(path: &Path) -> Result<Data, std::io::Error> {
    // ...
}

// Or use the ? operator to propagate
fn process() -> Result<(), std::io::Error> {
    let data = read_file(Path::new("file.txt"))?;
    Ok(())
}

Pitfall 5: String Confusion

Problem: Java has one String type. Rust has String (owned) and &str (borrowed).

Java:

String s1 = "hello";
String s2 = new String("world");
void process(String s) { }

Rust:

// Wrong: using only String
fn process(s: String) { }  // Takes ownership!

let s1 = String::from("hello");
process(s1);
// println!("{}", s1);  // Compile error: s1 was moved

// Right: use &str for parameters
fn process(s: &str) { }

let s1 = String::from("hello");
process(&s1);  // Borrow
println!("{}", s1);  // OK: still own s1

// String literals are &str
let s2 = "world";  // Type: &str

Pitfall 6: Integer Overflow

Problem: Java silently wraps on integer overflow. Rust panics in debug mode.

Java:

int max = Integer.MAX_VALUE;
int overflow = max + 1;  // Wraps to Integer.MIN_VALUE

Rust:

// Debug mode: panics on overflow
let max: i32 = i32::MAX;
// let overflow = max + 1;  // Panic in debug, wraps in release

// Right: use checked/wrapping/saturating arithmetic
let overflow = max.checked_add(1);  // Returns None
let wrapping = max.wrapping_add(1);  // Always wraps
let saturating = max.saturating_add(1);  // Clamps to max

Pitfall 7: Cloning Performance

Problem: Java clones collections implicitly. Rust makes cloning explicit and visible.

Java:

List<String> list1 = Arrays.asList("a", "b", "c");
List<String> list2 = new ArrayList<>(list1);  // Clone

Rust:

// Explicit cloning
let list1 = vec!["a", "b", "c"];
let list2 = list1.clone();  // Explicit, visible

// Prefer borrowing when possible
let list1 = vec!["a", "b", "c"];
process_list(&list1);  // Borrow, no clone
println!("{:?}", list1);  // Still available

Tooling

| Java Tool | Rust Equivalent | Purpose | |-----------|----------------|---------| | Maven / Gradle | Cargo | Build system, dependency management | | JUnit | Built-in #[test] | Unit testing | | Mockito | mockall | Mocking | | Javadoc | cargo doc (rustdoc) | Documentation generation | | IntelliJ IDEA | VS Code + rust-analyzer | IDE | | Eclipse | RustRover (JetBrains) | IDE | | Checkstyle / PMD | cargo clippy | Linting | | Google Java Format | cargo fmt (rustfmt) | Code formatting | | JaCoCo | cargo-tarpaulin / cargo-llvm-cov | Code coverage | | VisualVM | perf / valgrind / flamegraph | Profiling |


Examples

Example 1: Simple - HTTP Client

Before (Java):

import java.net.http.*;
import java.net.URI;

public class HttpExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.example.com/data"))
            .build();

        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        System.out.println(response.body());
    }
}

After (Rust):

use reqwest;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let response = reqwest::get("https://api.example.com/data")
        .await?
        .text()
        .await?;

    println!("{}", response);
    Ok(())
}

Example 2: Medium - Data Processing Pipeline

Before (Java):

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

public class DataProcessor {
    public static class User {
        String name;
        int age;
        String city;

        public User(String name, int age, String city) {
            this.name = name;
            this.age = age;
            this.city = city;
        }
    }

    public static List<String> processUsers(List<User> users) {
        return users.stream()
            .filter(user -> user.age >= 18)
            .filter(user -> user.city.equals("NYC"))
            .map(user -> user.name.toUpperCase())
            .sorted()
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("Alice", 25, "NYC"),
            new User("Bob", 17, "LA"),
            new User("Charlie", 30, "NYC")
        );

        List<String> result = processUsers(users);
        System.out.println(result);
    }
}

After (Rust):

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    city: String,
}

fn process_users(users: &[User]) -> Vec<String> {
    users
        .iter()
        .filter(|user| user.age >= 18)
        .filter(|user| user.city == "NYC")
        .map(|user| user.name.to_uppercase())
        .collect::<Vec<_>>()
        .into_iter()
        .sorted()
        .collect()
}

fn main() {
    let users = vec![
        User {
            name: String::from("Alice"),
            age: 25,
            city: String::from("NYC"),
        },
        User {
            name: String::from("Bob"),
            age: 17,
            city: String::from("LA"),
        },
        User {
            name: String::from("Charlie"),
            age: 30,
            city: String::from("NYC"),
        },
    ];

    let result = process_users(&users);
    println!("{:?}", result);
}

Example 3: Complex - Concurrent Web Server

Before (Java):

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class SimpleServer {
    private static final ExecutorService executor =
        Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("Server started on port 8080");

        while (true) {
            Socket clientSocket = serverSocket.accept();
            executor.submit(() -> handleClient(clientSocket));
        }
    }

    private static void handleClient(Socket socket) {
        try (
            BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream())
            );
            PrintWriter out = new PrintWriter(
                socket.getOutputStream(), true
            )
        ) {
            String request = in.readLine();
            System.out.println("Request: " + request);

            String response = processRequest(request);
            out.println("HTTP/1.1 200 OK");
            out.println("Content-Type: text/plain");
            out.println();
            out.println(response);
        } catch (IOException e) {
            System.err.println("Error handling client: " + e.getMessage());
        }
    }

    private static String processRequest(String request) {
        // Simulate processing
        return "Processed: " + request;
    }
}

After (Rust):

use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Server started on port 8080");

    loop {
        let (socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            if let Err(e) = handle_client(socket).await {
                eprintln!("Error handling client: {}", e);
            }
        });
    }
}

async fn handle_client(socket: TcpStream) -> Result<(), Box<dyn std::error::Error>> {
    let mut reader = BufReader::new(socket);
    let mut request = String::new();

    reader.read_line(&mut request).await?;
    println!("Request: {}", request.trim());

    let response = process_request(&request);

    let mut socket = reader.into_inner();
    socket.write_all(b"HTTP/1.1 200 OK\r\n").await?;
    socket.write_all(b"Content-Type: text/plain\r\n").await?;
    socket.write_all(b"\r\n").await?;
    socket.write_all(response.as_bytes()).await?;

    Ok(())
}

fn process_request(request: &str) -> String {
    format!("Processed: {}", request.trim())
}

Why this translation:

  • Tokio provides async I/O (more efficient than thread pool)
  • async/await syntax is more readable
  • Type-safe error handling with Result
  • Automatic resource cleanup (no explicit try-with-resources)
  • Lightweight async tasks instead of OS threads

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-golang-rust - Similar GC → ownership translation
  • convert-python-rust - Dynamic → static typing translation
  • lang-java-dev - Java development patterns
  • lang-rust-dev - Rust development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, threads, channels across languages
  • patterns-serialization-dev - JSON, validation, annotations across languages
  • patterns-metaprogramming-dev - Annotations, macros, reflection across languages