Rust for Java Developers
A comprehensive guide with side-by-side comparisons for Java engineers transitioning to Rust
Table of Contents
- Introduction
- Hello World
- Variables & Mutability
- Data Types
- String vs &str
- Stack vs Heap
- Functions
- Ownership
- Structs vs Classes
- Enums & Pattern Matching
- Traits vs Interfaces
- Error Handling
- Collections
- Package Management
- Common Patterns
- Generics
- Lifetimes
- Smart Pointers
- Memory Efficiency
- Concurrency
- Async/Await
- Debugging
- Testing
- Quick Reference
- Next Steps
Introduction
Rust is a systems programming language that guarantees memory safety without garbage collection. Coming from Java, the biggest shift is understanding ownership – Rust’s way of managing memory at compile-time instead of runtime.
Key Philosophy Differences
| Java | Rust |
|---|---|
| Garbage Collection | Ownership System (compile-time) |
| Runtime safety checks | Compile-time safety checks |
| Null everywhere | No null (uses Option) |
| Exceptions | Result types |
| Inheritance | Composition + Traits |
Hello World Comparison
Java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Rust
fn main() {
println!("Hello, World!");
}
- Rust does not require class definitions for functions
- Function declarations use the
fnkeyword println!is a macro, indicated by the!suffix
Variables and Mutability
Java
// Variables are mutable by default
int x = 5;
x = 6; // OK
// Constants
final int Y = 10;
Rust
// Variables are IMMUTABLE by default
let x = 5;
// x = 6; // ERROR
// Mutable variables
let mut y = 5;
y = 6; // OK
// Constants
const MAX: u32 = 100_000;
mut keyword.
Data Types
Primitive Types Comparison
| Java | Rust | Notes |
|---|---|---|
byte | i8 | 8-bit signed |
short | i16 | 16-bit signed |
int | i32 | 32-bit signed (default) |
long | i64 | 64-bit signed |
float | f32 | 32-bit float |
double | f64 | 64-bit float (default) |
boolean | bool | true/false |
String | String | Heap-allocated, growable |
| – | &str | String slice |
Stack vs Heap: Rust’s Memory Strategy
Understanding why Rust chooses stack over heap by default is essential to understanding how Rust achieves memory safety without garbage collection. This is a fundamental Rust innovation.
The Fundamental Difference
Java’s Approach
// Primitives -> Stack
int x = 42; // Stack (4 bytes)
double y = 3.14; // Stack (8 bytes)
// Objects -> Heap (Always)
String s = "hello"; // Reference on stack, data on heap
Person p = new Person(); // Reference on stack, object on heap
int[] arr = {1,2,3}; // Reference on stack, array on heap
// Everything is traced by GC
// GC runs periodically to clean up
Problem: GC must pause your program to scan and clean memory. This causes unpredictable latency spikes.
Rust’s Approach
// Stack by default (FAST.)
let x = 42; // Stack (4 bytes)
let y = 3.14; // Stack (8 bytes)
let point = Point { x: 1, y: 2 }; // Stack (8 bytes)
let arr = [1, 2, 3]; // Stack (12 bytes)
// Heap only when you explicitly ask
let s = String::from("hello"); // Heap
let v = Vec::new(); // Heap
let b = Box::new(42); // Heap
// Memory freed immediately when owner goes out of scope
// No garbage collection required.
Advantage: Memory freed at compile-time determined points. Zero garbage collection overhead.
Stack vs Heap: The Basics
| Feature | Stack | Heap |
|---|---|---|
| Speed | Fast – Just move a pointer | Slower – Search for space |
| Allocation Cost | ~1 CPU cycle | ~100+ CPU cycles |
| Deallocation | Automatic (just move pointer back) | Must be explicitly freed (or GC) |
| Size | Must be known at compile time | Can be dynamic/unknown |
| Lifetime | Tied to function scope | Can live beyond function |
| Cache Locality | Excellent (contiguous) | Poor (scattered) |
| Thread Safety | Each thread has own stack | Shared, needs synchronization |
How the Stack Works
Stack Memory Visualization:
Function calls build up the stack:
main() calls: Stack grows down ↓
┌──────────────┐
│ main's locals│ ← Stack pointer (top)
├──────────────┤
│ calculate() │ ← Previous frame
├──────────────┤
│ add() │ ← Even earlier frame
├──────────────┤
│ ... │
└──────────────┘
When function returns, stack pointer moves up ↑
Memory is "freed" instantly (just adjust pointer.)
Stack Allocation:
fn example() {
let x = 5; // Push 4 bytes
let y = 10; // Push 4 bytes
let z = x + y; // Push 4 bytes
} // Function ends, all 3 values "freed" instantly by moving stack pointer.
How the Heap Works
Heap Memory Visualization:
Heap is scattered memory:
┌─────────────────────────────────────┐
│ [free] [used] [free] [used] [free] │ Heap memory
└─────────────────────────────────────┘
↑ ↑
│ │
Finding free space takes time.
Fragmentation can occur
Heap Allocation:
fn example() {
let s = String::from("hello"); // 1. Search heap for space
// 2. Allocate memory
// 3. Copy "hello" to heap
// 4. Store pointer on stack
} // 5. Compiler inserts cleanup code to free heap memory
Why Rust Defaults to Stack
Performance Advantages:
- Speed: Stack allocation is ~100x faster than heap
- Cache Friendly: Stack data is contiguous, great for CPU cache
- No Fragmentation: Stack Never fragments
- Predictable: Allocation/deallocation cost is constant
- Zero Overhead: No allocator, no bookkeeping, no GC
When Rust Uses Heap
Rust only uses heap when you have one of these needs:
| Need | Solution | Example |
|---|---|---|
| Unknown size at compile time | Vec<T>, String |
Dynamic arrays, user input |
| Large data (avoid stack overflow) | Box<T> |
Large structs, recursive types |
| Data outlives function | Box<T>, Rc<T> |
Return owned data |
| Shared ownership | Rc<T>, Arc<T> |
Multiple owners of same data |
Concrete Examples
Example 1: Fixed Size -> Stack
struct Point {
x: i32, // 4 bytes
y: i32, // 4 bytes
} // Total: 8 bytes - known at compile time.
fn main() {
let p = Point { x: 10, y: 20 }; // Stack allocated.
// Fast, no heap allocation needed
} // Automatically freed when function ends
Example 2: Dynamic Size -> Heap
fn main() {
let mut v = Vec::new(); // Heap: size unknown at compile time
v.push(1);
v.push(2);
v.push(3);
// Vector grows dynamically - needs heap
} // Rust automatically calls Vec's destructor to free heap memory
Example 3: Large Data -> Heap
struct BigData {
data: [u8; 1_000_000], // 1 MB.
}
fn main() {
// let big = BigData { ... }; // Stack overflow risk.
let big = Box::new(BigData { data: [0; 1_000_000] }); // Heap
// Only stores pointer on stack (8 bytes)
} // Box's destructor automatically frees heap memory
The Magic: How Rust Avoids Garbage Collection
Java’s GC Approach
void process() {
String s1 = new String("hello");
String s2 = new String("world");
// ...
} // Memory still allocated.
// Later: GC runs
// 1. Stop the world (pause program)
// 2. Scan all live objects
// 3. Mark unreachable objects
// 4. Sweep (free) marked objects
// 5. Maybe compact heap
// 6. Resume program
// Problem: Unpredictable pauses.
Rust’s Ownership Approach
fn process() {
let s1 = String::from("hello");
let s2 = String::from("world");
// ...
} // Compiler inserts cleanup code HERE.
// s2.drop() called automatically
// s1.drop() called automatically
// Memory freed immediately
// No GC, no pause, no scanning
// Compiler knows exactly when to free.
// Zero runtime overhead.
Ownership Rules Enable Deterministic Cleanup
fn demonstrate_ownership() {
let s1 = String::from("hello"); // s1 owns the String
{
let s2 = s1; // Ownership moved to s2
println!("{}", s2);
} // s2 goes out of scope here, String freed HERE (deterministic.)
// println!("{}", s1); // ERROR: s1 no longer valid
}
// Compiler enforces: only ONE owner at a time
// When owner goes out of scope, value is dropped
// No need for GC to figure out when to free.
RAII: Resource Acquisition Is Initialization
The Pattern:
Resources are tied to object lifetime:
- Acquire resource in constructor
- Release resource in destructor
- Destructor called automatically when owner drops
use std::fs::File;
fn read_file() -> std::io::Result<String> {
let mut file = File::open("data.txt")?; // Resource acquired
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
} // file.drop() called automatically - file handle closed.
// No need for try-finally or explicit close()
// In Java you'd need:
// try (FileReader fr = new FileReader("data.txt")) {
// // use file
// } // finally block auto-generated
Performance Comparison
Memory Allocation Benchmark (typical):
| Operation | Java | Rust (Stack) | Rust (Heap) |
|---|---|---|---|
| Allocate small object | ~100 ns | ~1 ns | ~100 ns |
| Free small object | GC decides (ms later) | ~1 ns (instant) | ~50 ns (instant) |
| GC pause | 10-500 ms typical | 0 ms | 0 ms |
| Memory overhead | 12-16 bytes per object | 0 bytes | 0 bytes |
Real-World Impact
// Example: Processing 1 million records
// Java-style (heap everything)
fn process_java_style() {
let mut results = Vec::new();
for i in 0..1_000_000 {
let record = Box::new(Record { id: i, data: vec![0; 100] });
results.push(record); // 1M heap allocations.
}
// GC will need to scan and free 1M objects
// Can cause long pause.
}
// Rust-style (stack where possible)
fn process_rust_style() {
let mut results = Vec::with_capacity(1_000_000); // 1 heap allocation
for i in 0..1_000_000 {
let record = Record { id: i, data: [0; 100] }; // Stack.
results.push(record); // Copy to Vec's heap buffer
}
// All memory freed immediately when results drops
// No GC pause.
}
// Rust version is typically 2-10x faster.
Compiler-Determined Cleanup Points
fn example() {
let s1 = String::from("hello");
if some_condition {
let s2 = s1; // s1 moved to s2
println!("{}", s2);
} // s2 dropped here if condition was true
// Compiler inserts: if some_condition { drop(s2); }
let s3 = String::from("world");
} // Compiler inserts: drop(s3);
// No runtime decision making needed.
// All determined at compile time.
Why This is Revolutionary
Rust’s Innovation:
- Compile-Time Memory Management: Compiler determines when to free memory
- Zero Runtime Cost: No GC tracing, no reference counting checks
- Predictable Performance: No GC pauses, consistent latency
- Memory Safety: Impossible to use freed memory (compiler prevents it)
- No Leaks: Memory Always freed when owner drops (enforced by type system)
Key Principles Summary
How Rust Achieves Efficiency Without GC:
- Stack by Default: Most data lives on stack (fast, automatic cleanup)
- Explicit Heap: Only use heap when necessary (Vec, String, Box, etc.)
- Single Owner: Each value has exactly one owner
- Scope-Based Cleanup: Memory freed when owner goes out of scope
- Compiler Enforced: Ownership rules checked at compile time
- Zero-Cost Abstraction: No runtime overhead for safety
- RAII Pattern: Resources tied to object lifetime
- Move Semantics: Transfer ownership instead of copying
Comparison Chart
| Aspect | Java (GC) | Rust (Ownership) |
|---|---|---|
| Default allocation | Heap (objects) | Stack |
| Memory freed | By GC (eventually) | immediately at scope end |
| Overhead per object | 12-16 bytes header | 0 bytes |
| Runtime cost | GC scanning/marking/sweeping | Zero (compile-time determined) |
| Pause times | 10-500ms typical | 0ms (no pauses) |
| Memory usage | 2-5x needed (GC overhead) | 1x needed |
| Predictability | Low (GC unpredictable) | High (deterministic) |
| Developer mental model | Simple (just allocate) | More complex (ownership) |
The Trade-off:
Java: Easy to use, but unpredictable performance and higher memory usage
Rust: Steeper learning curve (ownership), but predictable performance and optimal memory usage
Rust makes you think more upfront, but gives you performance guarantees Java cannot match.
Final Example: Complete Picture
struct Report {
id: u32, // Stack (4 bytes)
name: String, // Stack (24 bytes) -> Heap (actual string)
data: Vec<f64>, // Stack (24 bytes) -> Heap (array)
}
fn process_reports() {
// Stack frame for this function
let count = 0; // Stack: 4 bytes
let threshold = 100.0; // Stack: 8 bytes
let report = Report {
id: 1, // Stack: 4 bytes
name: String::from("Q1"), // Stack: 24 bytes, Heap: ~10 bytes
data: vec![1.0, 2.0, 3.0], // Stack: 24 bytes, Heap: 24 bytes
};
// Total stack: ~64 bytes (fast.)
// Total heap: ~34 bytes (only when needed.)
} // Scope ends:
// 1. report.data's Vec destructor called -> frees heap array
// 2. report.name's String destructor called -> frees heap string
// 3. Stack pointer moved back -> all stack memory "freed"
// All automatic, zero runtime cost, no GC.
// Compiler generates something like:
// fn process_reports() {
// // ... function code ...
// drop(report.data); // Compiler inserted
// drop(report.name); // Compiler inserted
// }
Summary:
Rust achieves efficient memory management without GC by:
- Defaulting to stack allocation (100x faster than heap)
- Using heap only when explicitly needed
- Enforcing single ownership at compile time
- Freeing memory deterministically when owner drops
- Having the compiler insert cleanup code automatically
- Zero runtime overhead – all safety checks at compile time
The result: Memory safety + C-level performance + predictable latency.
Functions
Java
public int add(int a, int b) {
return a + b;
}
public void greet(String name) {
System.out.println("Hello, " + name);
}
Rust
fn add(a: i32, b: i32) -> i32 {
a + b // No return keyword needed
}
fn greet(name: &str) {
println!("Hello, {}", name);
}
- No access modifiers – use module system for visibility
- Parameter types are required (no inference)
- Return type comes after
-> - Last expression is implicitly returned (no semicolon)
- No
void– functions that don’t return omit-> Type
Expression vs Statement
fn main() {
let y = {
let x = 3;
x + 1 // No semicolon - returns a value
};
println!("{}", y); // 4
}
Ownership (The Big Difference)
The Three Rules of Ownership
- Each value in Rust has a variable that’s called its owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped (freed)
Java vs Rust Memory Model
Java
String s1 = "hello";
String s2 = s1; // Both reference same object
System.out.println(s1); // Works fine
System.out.println(s2); // Works fine
Both s1 and s2 point to the same string. Garbage collector cleans up when no references exist.
Rust
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: s1 invalid
println!("{}", s2); // OK
What happened? Ownership moved from s1 to s2. s1 is no longer valid. This prevents double-free errors.
Borrowing and References
Like passing by reference in Java, but with compile-time checks:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Borrow s1
println!("Length of '{}' is {}", s1, len); // s1 still valid
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The & symbol means “borrow” – temporary access without taking ownership.
Ownership Analogy
Think of ownership like a single key to a house:
- Java: Everyone has a copy of the key (references). Garbage collector locks the house when no one’s using it.
- Rust: Only one person has the key at a time (owner). You can lend the key (&), but you get it back. The house is demolished when the key is returned for good.
Structs vs Classes
Java Class
public class User {
private String username;
private String email;
private int age;
public User(String username,
String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
public void displayInfo() {
System.out.println(username +
" <" + email + ">");
}
}
Rust Struct
struct User {
username: String,
email: String,
age: u32,
}
impl User {
fn new(username: String,
email: String, age: u32) -> User {
User { username, email, age }
}
fn display_info(&self) {
println!("{} <{}>",
self.username, self.email);
}
}
- Struct defines data,
implblocks define methods - No constructors – use associated functions (often called
new) &self=thisin Java (immutable access)&mut self=thisbut allows modificationself= consumes the instance (takes ownership)- No inheritance – use composition and traits
Enums and Pattern Matching
Enums in Rust are much more powerful than Java enums.
Basic Enums
Java
public enum Status {
PENDING,
APPROVED,
REJECTED
}
Status s = Status.PENDING;
if (s == Status.PENDING) {
// do something
}
Rust
enum Status {
Pending,
Approved,
Rejected,
}
let s = Status::Pending;
match s {
Status::Pending => println!("Waiting"),
Status::Approved => println!("Good"),
Status::Rejected => println!("Denied"),
}
Enums with Data
This is where Rust enums shine:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
match home {
IpAddr::V4(a, b, c, d) => {
println!("IPv4: {}.{}.{}.{}", a, b, c, d);
}
IpAddr::V6(addr) => {
println!("IPv6: {}", addr);
}
}
The Option Enum (Rust’s Solution to Null)
Java has null – Rust has Option<T>:
let name: Option<String> = None;
match name {
Some(n) => println!("{}", n.len()),
None => println!("No name"),
}
- Compiler forces you to handle the None case
- No NullPointerException at runtime
- Type system tracks possibility of absence
Traits vs Interfaces
Java Interface
public interface Drawable {
void draw();
default void display() {
System.out.println("Displaying...");
}
}
public class Circle implements Drawable {
@Override
public void draw() {
System.out.println("Drawing circle");
}
}
Rust Trait
trait Drawable {
fn draw(&self);
fn display(&self) {
println!("Displaying...");
}
}
struct Circle { radius: f64 }
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing circle");
}
}
Key Differences: Traits can be implemented for existing types (even primitives), use trait bounds instead of generics syntax, and there’s no inheritance.
Error Handling
Java Exception Model
public int divide(int a, int b)
throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException(
"Division by zero");
}
return a / b;
}
try {
int result = divide(10, 0);
} catch (ArithmeticException e) {
System.err.println(e.getMessage());
}
Rust Result Type
fn divide(a: i32, b: i32)
-> Result<i32, String> {
if b == 0 {
Err(String::from(
"Division by zero"))
} else {
Ok(a / b)
}
}
match divide(10, 0) {
Ok(result) => println!("Result: {}", result),
Err(e) => eprintln!("Error: {}", e),
}
The ? Operator (Like Checked Exceptions)
fn read_and_parse() -> Result<i32, String> {
let content = read_file()?; // If Err, return early
let number = parse_number(content)?; // If Err, return early
Ok(number)
}
The ? operator unwraps Ok values or returns Err early.
Collections
ArrayList vs Vec
Java
import java.util.ArrayList;
ArrayList<Integer> list =
new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(list.get(0));
Rust
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);
println!("{}", vec[0]);
// Or use the vec! macro
let vec = vec![1, 2, 3];
HashMap vs HashMap
Java
import java.util.HashMap;
HashMap<String, Integer> map =
new HashMap<>();
map.put("blue", 10);
map.put("red", 20);
Integer value = map.get("blue");
Rust
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(String::from("blue"), 10);
map.insert(String::from("red"), 20);
match map.get("blue") {
Some(val) => println!("{}", val),
None => println!("No value"),
}
Package Management
Maven/Gradle vs Cargo
Java (Maven pom.xml)
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
Rust (Cargo.toml)
[dependencies]
serde = "1.0"
serde_json = "1.0"
Commands
| Task | Java (Maven) | Rust (Cargo) |
|---|---|---|
| Build | mvn compile | cargo build |
| Run | mvn exec:java | cargo run |
| Test | mvn test | cargo test |
| Package | mvn package | cargo build --release |
| Add dependency | Edit pom.xml | cargo add serde |
Common Patterns
Iterator Pattern
Both languages have strong iterator support:
Java
list.stream()
.filter(x -> x > 5)
.map(x -> x * 2)
.collect(Collectors.toList());
Rust
vec.iter()
.filter(|&&x| x > 5)
.map(|x| x * 2)
.collect::<Vec<_>>();
Generics
Java
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// Usage
Box<Integer> intBox = new Box<>(42);
Box<String> strBox = new Box<>("hello");
Rust
struct Box<T> {
value: T,
}
impl<T> Box<T> {
fn new(value: T) -> Self {
Box { value }
}
fn get_value(&self) -> &T {
&self.value
}
}
// Usage
let int_box = Box::new(42);
let str_box = Box::new("hello");
Generic Functions
// Generic function with trait bounds
fn print_display<T: std::fmt::Display>(item: T) {
println!("{}", item);
}
// Multiple trait bounds
fn compare<T: PartialOrd + std::fmt::Display>(a: T, b: T) {
if a > b {
println!("{} is greater", a);
} else {
println!("{} is greater", b);
}
}
// Where clause for complex bounds
fn complex_function<T, U>(t: T, u: U) -> i32
where
T: std::fmt::Display + Clone,
U: Clone + std::fmt::Debug,
{
println!("t: {}", t);
println!("u: {:?}", u);
0
}
- Rust generics are zero-cost – they’re monomorphized at compile time
- Use trait bounds to specify what operations are available
- The
whereclause improves readability for complex bounds - No runtime overhead unlike Java’s type erasure
Lifetimes
Lifetimes are Rust’s way of ensuring that references are valid. There’s no direct Java equivalent because Java uses garbage collection.
Lifetime Annotations
// Without lifetimes - won't compile
// fn longest(x: &str, y: &str) -> &str { ... }
// With lifetime annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// result cannot be used outside this block
}
// println!("{}", result); // ERROR: string2 dropped
}
The lifetime annotation 'a says: “All these references must live at least as long as 'a“.
Lifetime in Structs
// Struct holding a reference needs lifetime
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let excerpt = ImportantExcerpt {
part: first_sentence,
};
// excerpt cannot outlive novel
}
Lifetime Elision Rules
Rust often infers lifetimes automatically:
// These are equivalent
fn first_word(s: &str) -> &str { ... }
fn first_word<'a>(s: &'a str) -> &'a str { ... }
// Multiple inputs require explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
- Each reference parameter gets its own lifetime
- If there’s exactly one input lifetime, it is assigned to all output lifetimes
- If there’s a
&selfor&mut self, its lifetime is assigned to outputs - Otherwise, you must specify lifetimes explicitly
Smart Pointers
Smart pointers are data structures that act like pointers but have additional metadata and capabilities. Java developers: think of these as specialized reference types with different ownership semantics.
Box<T> – Heap Allocation
// Box allocates data on the heap
let b = Box::new(5);
println!("b = {}", b);
// Useful for recursive types
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(
List::Cons(2, Box::new(
List::Cons(3, Box::new(List::Nil))
))
));
Java
// Everything is heap-allocated by default
Integer value = new Integer(5);
// Recursive structures work naturally
class Node {
int value;
Node next;
}
Rust
// Stack-allocated by default
let value = 5;
// Use Box for heap allocation
let boxed = Box::new(5);
// Box needed for recursive types
struct Node {
value: i32,
next: Option<Box<Node>>,
}
Rc<T> – Reference Counting
Similar to Java references, but explicit. Allows multiple owners of the same data.
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a); // Increments reference count
let c = Rc::clone(&a);
println!("Reference count: {}", Rc::strong_count(&a)); // 3
// When all Rc instances drop, memory is freed
Rc<T> is NOT thread-safe. Use Arc<T> (Atomic Reference Counting) for multi-threaded code.
RefCell<T> – Interior Mutability
Allows mutation even with immutable references, with runtime checks.
use std::cell::RefCell;
let value = RefCell::new(5);
// Borrow mutably at runtime
*value.borrow_mut() += 10;
// Borrow immutably
println!("Value: {}", value.borrow()); // 15
// This will panic at runtime if you try to borrow mutably
// while an immutable borrow exists
Combining Smart Pointers
use std::rc::Rc;
use std::cell::RefCell;
// Multiple owners with interior mutability
let value = Rc::new(RefCell::new(5));
let a = Rc::clone(&value);
let b = Rc::clone(&value);
*a.borrow_mut() += 10;
*b.borrow_mut() += 5;
println!("Value: {}", value.borrow()); // 20
Box<T>: Single owner, heap allocation (like unique_ptr in C++)Rc<T>: Multiple owners, single-threaded (like shared_ptr)Arc<T>: Multiple owners, thread-safeRefCell<T>: Interior mutability with runtime checksMutex<T>: Thread-safe interior mutability
Memory Efficiency: Why Rust is Superior
1. Zero-Cost Abstractions
Rust’s abstractions compile down to the same machine code you’d write by hand.
Java – Runtime Overhead
// Object header: 12-16 bytes overhead PER object
class Point {
int x; // 4 bytes
int y; // 4 bytes
}
// Total: ~24 bytes (including header + padding)
// ArrayList has wrapper overhead
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // Integer object: 16 bytes for storing 4 bytes.
Rust – Zero Overhead
// No object header - just the data
struct Point {
x: i32, // 4 bytes
y: i32, // 4 bytes
}
// Total: 8 bytes exactly
// No boxing needed
let vec = vec![1, 2, 3];
// Stores integers directly: 3 × 4 = 12 bytes
2. No Garbage Collection Pauses
| Aspect | Java (GC) | Rust (Ownership) |
|---|---|---|
| Memory freed | Unpredictable (GC decides) | Predictable (scope ends) |
| Pause times | 10-500ms typical | 0ms – no pauses |
| CPU overhead | 5-10% for GC | 0% – compile time only |
| Memory usage | 2-5x required | 1x required |
| Predictability | Low (GC tuning needed) | High (deterministic) |
3. Stack vs Heap Allocation
// Rust gives you control over allocation
fn demonstrate_allocation() {
// Stack allocated - FAST (just move stack pointer)
let x = 5;
let point = Point { x: 1, y: 2 };
let array = [1, 2, 3, 4, 5];
// Heap allocated - slower but flexible size
let vec = vec![1, 2, 3, 4, 5];
let boxed = Box::new(Point { x: 1, y: 2 });
let string = String::from("heap allocated");
}
// Everything freed immediately when function returns
In Java, all objects go to the heap, even tiny ones. Rust lets you use stack allocation for better cache locality and speed.
4. Memory Layout Control
// Rust gives you precise control over memory layout
#[repr(C)] // Use C-compatible layout
struct ControlledLayout {
a: u8,
b: u32,
c: u16,
}
// Pack struct to minimize size
#[repr(packed)]
struct PackedStruct {
a: u8,
b: u32,
}
// Check sizes at compile time
const _: () = assert!(std::mem::size_of::<Point>() == 8);
5. Real-World Performance Comparison
- Startup time: Rust 10-100x faster (no JVM warmup)
- Memory usage: Rust uses 2-5x less memory
- Throughput: Rust 1.5-3x higher (no GC pauses)
- Latency: Rust 99th percentile 10-100x better (predictable)
- Binary size: Rust smaller (no runtime included)
6. RAII (Resource Acquisition Is Initialization)
use std::fs::File;
use std::io::Write;
fn write_to_file() -> std::io::Result<()> {
let mut file = File::create("output.txt")?;
file.write_all(b"Hello")?;
// File automatically closed here - no try-finally needed.
Ok(())
}
// Compare with Java
// try (FileWriter fw = new FileWriter("output.txt")) {
// fw.write("Hello");
// } // finally block automatically generated
- No use-after-free
- No double-free
- No data races (in safe code)
- No null pointer dereferences
- No resource leaks
Concurrency in Rust
Rust’s motto: “Fearless Concurrency”. The ownership system prevents data races at compile time.
Threads
Java Threads
Thread thread = new Thread(() -> {
System.out.println("Hello from thread.");
});
thread.start();
thread.join();
// Shared mutable state requires synchronization
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
Rust Threads
use std::thread;
let handle = thread::spawn(|| {
println!("Hello from thread.");
});
handle.join().unwrap();
// Compiler prevents data races.
// This won't compile:
// let mut count = 0;
// thread::spawn(|| count += 1); // ERROR
Message Passing with Channels
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// Create channel
let (tx, rx) = mpsc::channel();
// Spawn thread that sends messages
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
// Receive messages
for received in rx {
println!("Got: {}", received);
}
}
Shared State with Mutex
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc = Atomic Reference Counting (thread-safe Rc)
// Mutex = Mutual Exclusion for interior mutability
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 || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Arc<T>allows multiple owners across threadsMutex<T>ensures only one thread accesses data at a time- Compiler verifies thread safety at compile time.
- If it compiles, it is thread-safe (in safe Rust)
Concurrency Comparison
| Feature | Java | Rust |
|---|---|---|
| Data race detection | Runtime (maybe crashes) | Compile-time (won’t compile) |
| Thread safety | Manual synchronization | Type system enforced |
| Deadlock prevention | No (manual care needed) | No (manual care needed) |
| Performance | Good (JIT optimized) | Excellent (zero-cost) |
| Thread creation cost | ~1MB per thread | ~2KB per thread |
Async/Await in Rust
Rust’s async/await enables concurrent operations without threads, using lightweight tasks.
Basic Async Function
// Async function returns a Future
async fn fetch_data() -> String {
// Simulate async work
String::from("data")
}
// Using tokio runtime
#[tokio::main]
async fn main() {
let data = fetch_data().await;
println!("{}", data);
}
Concurrent Async Operations
use tokio;
async fn download_file(url: &str) -> Result<String, Box<dyn std::error::Error>> {
// Simulate download
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
Ok(format!("Downloaded: {}", url))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Sequential execution
let file1 = download_file("url1").await?;
let file2 = download_file("url2").await?;
// Concurrent execution (much faster)
let (file1, file2) = tokio::join!(
download_file("url1"),
download_file("url2")
);
// Or use select! for racing
tokio::select! {
result = download_file("fast_url") => println!("Got: {:?}", result),
result = download_file("slow_url") => println!("Got: {:?}", result),
}
Ok(())
}
Async vs Threads
| Aspect | Threads | Async |
|---|---|---|
| Memory per task | ~2KB+ stack | ~50 bytes |
| Scalability | 1000s of threads | Millions of tasks |
| Context switching | OS controlled (expensive) | User-space (cheap) |
| CPU-bound work | Excellent | Poor (blocks executor) |
| I/O-bound work | Good | Excellent |
- Threads: CPU-intensive work, parallel computation
- Async: I/O-bound work (network, file system), web servers
- Both: Hybrid – async for I/O, thread pool for CPU work
Debugging Rust Code
1. Compiler is Your Best Friend
Rust’s compiler provides incredibly detailed error messages:
let s = String::from("hello");
let s2 = s;
println!("{}", s); // ERROR
// Compiler says:
// error[E0382]: borrow of moved value: `s`
// help: consider cloning the value if performance does not matter
// let s2 = s.clone();
2. println! Debugging
// Debug print with {:?}
let vec = vec![1, 2, 3];
println!("{:?}", vec); // [1, 2, 3]
// Pretty print with {:#?}
struct Person { name: String, age: u32 }
let p = Person { name: String::from("John"), age: 30 };
println!("{:#?}", p);
// Custom debug format
#[derive(Debug)]
struct Point { x: i32, y: i32 }
let point = Point { x: 1, y: 2 };
println!("{:?}", point); // Point { x: 1, y: 2 }
3. dbg! Macro
// dbg! shows file, line, and value
fn complex_calculation(x: i32) -> i32 {
let y = dbg!(x * 2); // [src/main.rs:23] x * 2 = 10
let z = dbg!(y + 5); // [src/main.rs:24] y + 5 = 15
z
}
// Takes ownership and returns it
let vec = vec![1, 2, 3];
let vec = dbg!(vec); // Can continue using vec
4. Using rust-gdb or rust-lldb
# Build with debug symbols
cargo build
# Start debugger
rust-gdb target/debug/your_program
# Common commands:
# break main - set breakpoint
# run - start program
# step - step into
# next - step over
# print variable - show value
# continue - continue execution
5. Clippy – Linter
# Install clippy
rustup component add clippy
# Run clippy
cargo clippy
# Clippy catches common mistakes:
# - Unnecessary clones
# - Inefficient patterns
# - Potential bugs
# - Non-idiomatic code
6. Common Debugging Patterns
// 1. Unwrap with context
let file = File::open("data.txt")
.expect("Failed to open data.txt");
// 2. Question mark with context
fn read_file() -> Result<String, Box<dyn std::error::Error>> {
let mut file = File::open("data.txt")
.map_err(|e| format!("Failed to open: {}", e))?;
// ...
}
// 3. Type inference help
let _: () = some_value; // Compiler will tell you the actual type
// 4. Panic backtrace
// Run with: RUST_BACKTRACE=1 cargo run
panic!("Something went wrong.");
- Use
cargo checkfor fast compile checking without code generation - Enable
cargo watchfor continuous checking - Use
#[must_use]attribute to catch ignored results - Enable more warnings:
#.[warn(rust_2018_idioms)]
Testing in Rust
Unit Tests
// Tests go in the same file
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-2, 3), 1);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn test_divide_by_zero() {
divide(10, 0);
}
}
// Run tests
// cargo test
Integration Tests
// tests/integration_test.rs
use my_crate::add;
#[test]
fn it_works() {
assert_eq!(add(2, 2), 4);
}
Test Organization
#[cfg(test)]
mod tests {
use super::*;
// Common test setup
fn setup() -> Vec<i32> {
vec![1, 2, 3]
}
#[test]
fn test_with_setup() {
let data = setup();
assert_eq!(data.len(), 3);
}
// Test modules for organization
mod addition_tests {
use super::super::*;
#[test]
fn positive_numbers() {
assert_eq!(add(1, 2), 3);
}
}
mod subtraction_tests {
// ...
}
}
Useful Test Commands
# Run all tests
cargo test
# Run specific test
cargo test test_add
# Run tests matching pattern
cargo test addition
# Show print output
cargo test -- --nocapture
# Run ignored tests
cargo test -- --ignored
# Run tests in parallel (default) or sequentially
cargo test -- --test-threads=1
- Tests are first-class citizens in Rust
- No separate testing framework needed
- Documentation tests ensure examples work
- Property-based testing with
proptestcrate
Quick Reference Card
Syntax Comparison
| Concept | Java | Rust |
|---|---|---|
| Variable | int x = 5; | let x = 5; |
| Mutable variable | int x = 5; | let mut x = 5; |
| Constant | final int X = 5; | const X: i32 = 5; |
| Function | int add(int a) {} | fn add(a: i32) -> i32 {} |
| String | String s = "hi"; | let s = String::from("hi"); |
| List | ArrayList<T> | Vec<T> |
| Map | HashMap<K,V> | HashMap<K,V> |
| Null check | if (x .= null) | if let Some(x) = opt |
| Interface | interface T {} | trait T {} |
System.out.println() | println!() |
Memory Management Quick Guide
| Scenario | Java | Rust |
|---|---|---|
| Create object | new Object() – GC manages |
Object::new() – owner cleans up |
| Pass to function | Reference passed, GC manages | Ownership transferred OR borrowed |
| Multiple references | OK – GC tracks | Must use borrowing & |
| Null values | Possible everywhere | Use Option<T> |
| Free memory | Automatic GC | Automatic when owner drops |
Next Steps
To get comfortable reading Rust code:
- Focus on understanding ownership – it is the core concept
- Read the
matchstatement carefully – it is everywhere in Rust - Look for
&and&mut– they tell you about borrowing - Understand
OptionandResult– they replace null and exceptions - Read
implblocks – that’s where methods live - Check function signatures – they tell you about ownership transfers
Practice Resources
- Rust Book: https://doc.rust-lang.org/book/
- Rustlings: Small exercises to learn Rust
- Rust by Example: https://doc.rust-lang.org/rust-by-example/
Common “Gotchas” for Java Developers
- Forgetting
mut: Variables are immutable by default - Fighting the borrow checker: Trust it – it prevents bugs
- Looking for inheritance: Use composition and traits instead
- Expecting nulls: Use
Option<T> - Using
clone()everywhere: Understand borrowing first - String vs &str confusion:
Stringis owned,&stris borrowed
Conclusion
Rust enforces at compile-time what Java checks at runtime. This means:
- More time fighting the compiler initially
- Way fewer bugs in production
- No garbage collection pauses
- Predictable performance
The learning curve is steep, but your Java knowledge gives you a huge head start. Focus on ownership, and everything else will fall into place.
Happy coding.
String vs &str: The Complete Guide
Java provides a single String type, while Rust distinguishes between two string types: String and &str.
Comparative Analysis
| Feature | String | &str |
|---|---|---|
| Ownership | Owns its data | Borrows data |
| Location | Always heap | Heap, stack, or static |
| Mutable | Yes (if mut) |
No (immutable) |
| Growable | Yes | No |
Implementation Example
// Recommended: Use &str for function parameters
fn print_name(name: &str) {
println!("Name: {}", name);
}
// Compatible with both types
let owned = String::from("Alice");
let borrowed = "Bob";
print_name(&owned); // Convert String to &str
print_name(borrowed); // &str works directly
// Recommended: Return String when creating new data
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
Usage Guidelines
- Function parameters: Use
&strfor flexibility - Return values: Use
Stringwhen creating new strings - Struct fields: Use
Stringfor owned data - Modification required: Use
String
Ownership System
Ownership Rules
- Each value has a variable designated as its owner
- Only one owner can exist at any given time
- When the owner goes out of scope, the value is automatically dropped
Memory Management Comparison
Java
String s1 = "hello";
String s2 = s1; // Both reference same object
System.out.println(s1); // Valid
System.out.println(s2); // Valid
Garbage collector handles deallocation when no references remain.
Rust
let s1 = String::from("hello");
let s2 = s1; // Ownership transferred to s2
// println!("{}", s1); // Compilation error
println!("{}", s2); // Valid
Ownership transfer prevents double-free errors at compile time.
Borrowing Mechanism
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Reference borrowed
println!("'{}' is {} long", s1, len); // s1 remains valid
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The & operator creates a reference, providing temporary access without transferring ownership.
Error Handling
Java
try {
int result = divide(10, 0);
} catch (Exception e) {
System.err.println(e);
}
Rust
fn divide(a: i32, b: i32)
-> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
match divide(10, 0) {
Ok(result) => println!("{}", result),
Err(e) => eprintln!("{}", e),
}
The ? Operator
fn read_and_parse() -> Result<i32, String> {
let content = read_file()?; // Return early if Err
let number = parse_number(content)?;
Ok(number)
}
Collections
ArrayList vs Vec
Java
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
System.out.println(list.get(0));
Rust
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
println!("{}", vec[0]);
// Or use macro
let vec = vec![1, 2, 3];
HashMap
Java
HashMap<String, Integer> map = new HashMap<>();
map.put("blue", 10);
Integer value = map.get("blue");
Rust
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("blue".to_string(), 10);
match map.get("blue") {
Some(val) => println!("{}", val),
None => println!("Not found"),
}
Package Management
Maven/Gradle vs Cargo
| Task | Java (Maven) | Rust (Cargo) |
|---|---|---|
| Build | mvn compile | cargo build |
| Run | mvn exec:java | cargo run |
| Test | mvn test | cargo test |
| Package | mvn package | cargo build --release |
Quick Reference Card
| Concept | Java | Rust |
|---|---|---|
| Variable | int x = 5; | let x = 5; |
| Mutable | int x = 5; | let mut x = 5; |
| Constant | final int X = 5; | const X: i32 = 5; |
| Function | int add(int a) {} | fn add(a: i32) -> i32 {} |
| List | ArrayList<T> | Vec<T> |
| Map | HashMap<K,V> | HashMap<K,V> |
| Null check | if (x != null) | if let Some(x) = opt |
| Interface | interface T {} | trait T {} |
Further Learning
Key Learning Priorities
- Master ownership concepts – Central to Rust’s memory management
- Study pattern matching –
matchstatements are extensively used - Understand borrowing syntax –
&and&mutindicate borrowing semantics - Learn Option and Result types – Replacements for null values and exceptions
- Analyze function signatures – Reveal ownership transfer patterns
Recommended Resources
- The Rust Programming Language: https://doc.rust-lang.org/book/
- Rustlings: Interactive exercises for learning Rust
- Rust by Example: https://doc.rust-lang.org/rust-by-example/
Common Challenges for Java Developers
- Immutability by default: Variables require explicit
mutdeclaration - Borrow checker enforcement: Compiler prevents memory safety violations
- Absence of inheritance: Rust uses composition and traits instead
- No null values:
Option<T>type handles optional values - String type distinction:
Stringrepresents owned data,&strrepresents borrowed references
Conclusion
Rust enforces at compile-time what Java validates at runtime. This results in:
- Extended initial development time due to compiler constraints
- Significantly reduced production defects
- Elimination of garbage collection pauses
- Deterministic performance characteristics
While the learning curve is substantial, existing Java expertise provides a strong foundation for Rust development.