| 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
- 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
- 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 |
|---|---|---|
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_thenandunwrap_orare 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 Javathrows) - 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
Optionalto RustOption - Rust's
Optionmethods 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()vsinto_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
selfconsumption for method chaining - No need for nested
Builderclass (separate struct) impl Into<String>accepts bothStringand&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 replacesthrowsdeclarations - No
@Overrideannotation 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
statickeyword needed (noselfparameter) - Factory methods return
Resultfor validation - Named constructors are idiomatic (
new,with_capacity, etc.) - Private constructors not needed (use
pubselectively)
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
Fromtrait enables automatic conversion with?- Pattern matching handles different error cases
anyhowcrate provides ergonomic error handling for applicationsthiserrorcrate 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
moveclosure 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/awaitsyntax 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
Futuretrait
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/awaitis more readable than chaining futures?operator propagates errors through async chain- No need for explicit
exceptionallyhandlers - 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:
Rc<T>(single-threaded) orArc<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
serdeis 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/awaitsyntax 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 examplesconvert-golang-rust- Similar GC → ownership translationconvert-python-rust- Dynamic → static typing translationlang-java-dev- Java development patternslang-rust-dev- Rust development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Async, threads, channels across languagespatterns-serialization-dev- JSON, validation, annotations across languagespatterns-metaprogramming-dev- Annotations, macros, reflection across languages