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.
git clone https://github.com/majiayu000/claude-skill-registry
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"
skills/data/convert-java-rust/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 |
|---|---|---|
| / | Owned vs borrowed |
| | 32-bit signed integer |
| | 64-bit signed integer |
| | 32-bit float |
| | 64-bit float |
| | Direct mapping |
| | Growable array |
| | Hash table |
| | Unique collection |
| | Nullable values |
| in | Explicit nullability |
| | Type-safe errors |
| | Behavioral contracts |
| + | Data + behavior |
| No annotation needed | Traits enforce signature |
| / | Explicit locking |
| / | OS threads / async tasks |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Preserve semantics over syntax similarity
- Adopt target idioms - don't write "Java code in Rust syntax"
- Handle edge cases - null checks, error paths, resource cleanup
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Java | Rust | Notes |
|---|---|---|
| | Direct mapping |
| | 8-bit signed integer |
| | 16-bit signed integer |
| | 32-bit signed integer (most common) |
| | 64-bit signed integer |
| | 32-bit floating point |
| | 64-bit floating point (most common) |
| | Unicode scalar value (4 bytes in Rust) |
| | Unit type |
Note: Java
char is 16-bit UTF-16, Rust char is 32-bit Unicode scalar.
Boxed Primitives
| Java | Rust | Notes |
|---|---|---|
| | Primitives don't need boxing in Rust |
| | No autoboxing/unboxing |
| | Direct primitive usage |
| | No wrapper types needed |
| | Direct usage |
String Types
| Java | Rust | Notes |
|---|---|---|
| | Owned, heap-allocated UTF-8 |
(param) | | Borrowed string slice for parameters |
| | Use with , |
| | Character array |
| | Byte array |
Collection Types
| Java | Rust | Notes |
|---|---|---|
| | Growable array |
| | Doubly-linked list (rarely used) |
| | Hash table, K must be Hash + Eq |
| | Ordered map, K must be Ord |
| | Unique collection |
| | Ordered unique collection |
| | Double-ended queue |
| | Max-heap by default |
| | Dynamic array |
(fixed) | | Fixed-size array |
Nullable Types
| Java | Rust | Notes |
|---|---|---|
| | Explicit nullability |
| | Non-null by default in Rust |
| | Direct mapping |
| | Null variant |
Error Types
| Java | Rust | Notes |
|---|---|---|
| | Type-safe error handling |
| or | Pattern matching or propagation |
| trait | Error interface |
| / | Unrecoverable vs recoverable |
Composite Types
| Java | Rust | Notes |
|---|---|---|
| + | Data + behavior separation |
| | Behavioral contract |
| | Algebraic data types in Rust |
| | Immutable by default in Rust |
| | Tuple |
Generic Types
| Java | Rust | Notes |
|---|---|---|
| | Type parameter |
| | Bounded type parameter |
| No direct equivalent | Use trait objects |
| (type inference) | Wildcard |
| | Bounded wildcard |
| | 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:
makes nullability explicit in the type systemOption<T>- No null pointer exceptions possible at runtime
- Combinators like
andand_then
are idiomaticunwrap_or - 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:
encodes success/failure in the type systemResult<T, E>- 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
to RustOptionalOption - Rust's
methods are similar to Java'sOption - Pattern matching can destructure nested
sOption - 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
,collectsum - Rust iterators are lazy (like Java streams)
- No need for specialized primitive streams (
, etc.)mapToInt - More explicit borrowing with
vsiter()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
consumption for method chainingself - No need for nested
class (separate struct)Builder
accepts bothimpl Into<String>
andString&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 (
) make mutation explicit&mut - The
operator replaces?
declarationsthrows - No
annotation needed (enforced by trait)@Override
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
keyword needed (nostatic
parameter)self - Factory methods return
for validationResult - Named constructors are idiomatic (
,new
, etc.)with_capacity - Private constructors not needed (use
selectively)pub
Paradigm Translation: OOP → Systems Programming
Mental Model Shift: Object-Oriented → Ownership-Based
| Java Concept | Rust Approach | Key Insight |
|---|---|---|
| Class with state | + blocks | Data and behavior separated but associated |
| Inheritance | Composition + traits | Favor composition over deep hierarchies |
| Polymorphism (subtyping) | Trait objects () or generics | Static dispatch (generics) vs dynamic (trait objects) |
| Encapsulation | Module visibility + | Privacy at module level, not class level |
| Constructor | Associated function | No special constructor syntax |
| Garbage collection | Ownership + borrowing | Compiler-enforced memory safety |
| Null references | | Null safety in type system |
| Exceptions | | Errors as values |
Memory Management Mental Model
| Java Model | Rust Model | Conceptual Translation |
|---|---|---|
| Heap allocation automatic | Explicit (, , ) | Ownership makes allocation visible |
| GC reclaims memory | Automatic via RAII () | Deterministic cleanup at scope end |
| References everywhere | Borrows (, ) | Explicit lifetime tracking |
| No manual cleanup | No manual cleanup | Same safety, different mechanism |
| Shared mutable state | , , interior mutability | Mutation rules enforced by compiler |
Concurrency Mental Model
| Java Model | Rust Model | Conceptual Translation |
|---|---|---|
blocks | / | Lock protects data, not code |
| Thread-safe by convention | Thread-safe by type system (, ) | Compiler prevents data races |
| / | Async runtime for task scheduling |
(Java 8+) | trait + / | 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 | | Recoverable errors (expected) |
| Unchecked exceptions | or | Recoverable or programmer errors |
clause | Return type | Signature shows fallibility |
| or | Explicit error handling |
(propagate) | operator | Early return on error |
| RAII / trait | Automatic cleanup |
| or | 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
trait enables automatic conversion withFrom?- Pattern matching handles different error cases
crate provides ergonomic error handling for applicationsanyhow
crate simplifies custom error typesthiserror
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:
combines reference counting (Arc) with mutual exclusion (Mutex)Arc<Mutex<T>>- Mutex protects the data, not the code block
- Type system prevents data races at compile time
- Explicit cloning makes shared ownership visible
closure captures ownershipmove
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
syntax is more ergonomic than futuresawait- Tasks are lightweight (like virtual threads in Java 21)
- No need for explicit thread pool management
- Type-safe async with
traitFuture
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
is more readable than chaining futuresawait
operator propagates errors through async chain?- No need for explicit
handlersexceptionally - 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
- Each value has a single owner (unlike Java where references are shared freely)
- When the owner goes out of scope, the value is dropped (like Java finalization, but deterministic)
- 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:
(single-threaded) orRc<T>
(thread-safe) for shared ownershipArc<T>- Explicit cloning makes reference counting visible
- No cycles by default (use
for weak references)Weak<T> - 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
is the standard serialization frameworkserde
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 (
) for runtime polymorphismdyn Trait - 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 |
|---|---|---|
/ | | Compile |
/ | | Run tests |
/ | | Build release |
/ | | Install binary |
/ | | Clean build |
/ | | 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
modules#[cfg(test)] - 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 | Unit testing |
| Mockito | | Mocking |
| Javadoc | (rustdoc) | Documentation generation |
| IntelliJ IDEA | VS Code + rust-analyzer | IDE |
| Eclipse | RustRover (JetBrains) | IDE |
| Checkstyle / PMD | | Linting |
| Google Java Format | (rustfmt) | Code formatting |
| JaCoCo | / | Code coverage |
| VisualVM | / / | 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
syntax is more readableawait- 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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar GC → ownership translationconvert-golang-rust
- Dynamic → static typing translationconvert-python-rust
- Java development patternslang-java-dev
- Rust development patternslang-rust-dev
Cross-cutting pattern skills:
- Async, threads, channels across languagespatterns-concurrency-dev
- JSON, validation, annotations across languagespatterns-serialization-dev
- Annotations, macros, reflection across languagespatterns-metaprogramming-dev