Rust for Java Engineers

Rust for Java Developers – Complete Guide

Rust for Java Developers

A comprehensive guide with side-by-side comparisons for Java engineers transitioning to Rust

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!");
}
Notable Differences
  • Rust does not require class definitions for functions
  • Function declarations use the fn keyword
  • 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;
Note: Rust defaults to immutability for safety. Variables must be explicitly declared as mutable using the mut keyword.

Data Types

Primitive Types Comparison

Java Rust Notes
bytei88-bit signed
shorti1616-bit signed
inti3232-bit signed (default)
longi6464-bit signed
floatf3232-bit float
doublef6464-bit float (default)
booleanbooltrue/false
StringStringHeap-allocated, growable
&strString 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:

  1. Speed: Stack allocation is ~100x faster than heap
  2. Cache Friendly: Stack data is contiguous, great for CPU cache
  3. No Fragmentation: Stack Never fragments
  4. Predictable: Allocation/deallocation cost is constant
  5. 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:

  1. Compile-Time Memory Management: Compiler determines when to free memory
  2. Zero Runtime Cost: No GC tracing, no reference counting checks
  3. Predictable Performance: No GC pauses, consistent latency
  4. Memory Safety: Impossible to use freed memory (compiler prevents it)
  5. No Leaks: Memory Always freed when owner drops (enforced by type system)

Key Principles Summary

How Rust Achieves Efficiency Without GC:

  1. Stack by Default: Most data lives on stack (fast, automatic cleanup)
  2. Explicit Heap: Only use heap when necessary (Vec, String, Box, etc.)
  3. Single Owner: Each value has exactly one owner
  4. Scope-Based Cleanup: Memory freed when owner goes out of scope
  5. Compiler Enforced: Ownership rules checked at compile time
  6. Zero-Cost Abstraction: No runtime overhead for safety
  7. RAII Pattern: Resources tied to object lifetime
  8. 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);
}
Key Differences:
  • 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)

This is a core concept in Rust. There’s no equivalent in Java because Java uses garbage collection.

The Three Rules of Ownership

  1. Each value in Rust has a variable that’s called its owner
  2. There can only be one owner at a time
  3. 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);
    }
}
Key Differences:
  • Struct defines data, impl blocks define methods
  • No constructors – use associated functions (often called new)
  • &self = this in Java (immutable access)
  • &mut self = this but allows modification
  • self = 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"),
}
Why this is better:
  • 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 Similarities: Both define behavior contracts, support default implementations, and types can implement multiple traits/interfaces.

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)
Buildmvn compilecargo build
Runmvn exec:javacargo run
Testmvn testcargo test
Packagemvn packagecargo build --release
Add dependencyEdit pom.xmlcargo 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
}
Key Points:
  • Rust generics are zero-cost – they’re monomorphized at compile time
  • Use trait bounds to specify what operations are available
  • The where clause 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.

The Problem: How long does a reference remain valid? Rust needs to know at compile time.

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 { ... }
Lifetime Rules:
  • 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 &self or &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
Important: 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
Smart Pointer Summary:
  • 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-safe
  • RefCell<T>: Interior mutability with runtime checks
  • Mutex<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
Java Comparison:

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

Benchmark Results (typical scenarios):
  • 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
Key Insight: Rust’s ownership system eliminates entire categories of bugs:
  • No use-after-free
  • No double-free
  • No data races (in safe code)
  • No null pointer dereferences
  • No resource leaks
All checked at compile time with zero runtime cost.

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());
}
Type System Prevents Data Races:
  • Arc<T> allows multiple owners across threads
  • Mutex<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
When to use what:
  • 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.");
Pro Tips:
  • Use cargo check for fast compile checking without code generation
  • Enable cargo watch for 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
Rust Testing Philosophy:
  • Tests are first-class citizens in Rust
  • No separate testing framework needed
  • Documentation tests ensure examples work
  • Property-based testing with proptest crate

Quick Reference Card

Syntax Comparison

Concept Java Rust
Variableint x = 5;let x = 5;
Mutable variableint x = 5;let mut x = 5;
Constantfinal int X = 5;const X: i32 = 5;
Functionint add(int a) {}fn add(a: i32) -> i32 {}
StringString s = "hi";let s = String::from("hi");
ListArrayList<T>Vec<T>
MapHashMap<K,V>HashMap<K,V>
Null checkif (x .= null)if let Some(x) = opt
Interfaceinterface T {}trait T {}
PrintSystem.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:

  1. Focus on understanding ownership – it is the core concept
  2. Read the match statement carefully – it is everywhere in Rust
  3. Look for & and &mut – they tell you about borrowing
  4. Understand Option and Result – they replace null and exceptions
  5. Read impl blocks – that’s where methods live
  6. Check function signatures – they tell you about ownership transfers

Practice Resources

Common “Gotchas” for Java Developers

  1. Forgetting mut: Variables are immutable by default
  2. Fighting the borrow checker: Trust it – it prevents bugs
  3. Looking for inheritance: Use composition and traits instead
  4. Expecting nulls: Use Option<T>
  5. Using clone() everywhere: Understand borrowing first
  6. String vs &str confusion: String is owned, &str is 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

Important Distinction

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 &str for flexibility
  • Return values: Use String when creating new strings
  • Struct fields: Use String for owned data
  • Modification required: Use String

Ownership System

Fundamental Concept: Ownership is a core Rust concept without direct equivalent in Java. It enables memory safety without garbage collection.

Ownership Rules

  1. Each value has a variable designated as its owner
  2. Only one owner can exist at any given time
  3. 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)
Buildmvn compilecargo build
Runmvn exec:javacargo run
Testmvn testcargo test
Packagemvn packagecargo build --release

Quick Reference Card

Concept Java Rust
Variableint x = 5;let x = 5;
Mutableint x = 5;let mut x = 5;
Constantfinal int X = 5;const X: i32 = 5;
Functionint add(int a) {}fn add(a: i32) -> i32 {}
ListArrayList<T>Vec<T>
MapHashMap<K,V>HashMap<K,V>
Null checkif (x != null)if let Some(x) = opt
Interfaceinterface T {}trait T {}