Claude-skill-registry convert-java-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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-java-rust" ~/.claude/skills/majiayu000-claude-skill-registry-convert-java-rust && rm -rf "$T"
manifest: skills/data/convert-java-rust/SKILL.md
source content

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

JavaRustNotes
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 neededTraits 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

JavaRustNotes
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

JavaRustNotes
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

JavaRustNotes
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

JavaRustNotes
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

JavaRustNotes
@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

JavaRustNotes
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

JavaRustNotes
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

JavaRustNotes
<T>
<T>
Type parameter
<T extends Foo>
<T: Foo>
Bounded type parameter
<T super Foo>
No direct equivalentUse 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
    Option
    s
  • 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 ConceptRust ApproachKey Insight
Class with state
struct
+
impl
blocks
Data and behavior separated but associated
InheritanceComposition + traitsFavor composition over deep hierarchies
Polymorphism (subtyping)Trait objects (
dyn Trait
) or generics
Static dispatch (generics) vs dynamic (trait objects)
EncapsulationModule visibility +
pub
Privacy at module level, not class level
ConstructorAssociated function
new()
No special constructor syntax
Garbage collectionOwnership + borrowingCompiler-enforced memory safety
Null references
Option<T>
Null safety in type system
Exceptions
Result<T, E>
Errors as values

Memory Management Mental Model

Java ModelRust ModelConceptual Translation
Heap allocation automaticExplicit (
Box
,
Vec
,
String
)
Ownership makes allocation visible
GC reclaims memoryAutomatic via RAII (
Drop
)
Deterministic cleanup at scope end
References everywhereBorrows (
&
,
&mut
)
Explicit lifetime tracking
No manual cleanupNo manual cleanupSame safety, different mechanism
Shared mutable state
Mutex
,
RefCell
, interior mutability
Mutation rules enforced by compiler

Concurrency Mental Model

Java ModelRust ModelConceptual Translation
synchronized
blocks
Mutex<T>
/
RwLock<T>
Lock protects data, not code
Thread-safe by conventionThread-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 threadsOS threads or lightweight async tasksChoose 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:

JavaRustUse 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/GradleCargoPurpose
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 ToolRust EquivalentPurpose
Maven / GradleCargoBuild system, dependency management
JUnitBuilt-in
#[test]
Unit testing
Mockito
mockall
Mocking
Javadoc
cargo doc
(rustdoc)
Documentation generation
IntelliJ IDEAVS Code + rust-analyzerIDE
EclipseRustRover (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