Achieve Runtime Polymorphism (dyn Trait)
Runtime Polymorphism (Interface vs Trait Object)
Difference between Box vs Rc vs Arc
Store data on the Heap (instead of Stack)
Why?
Stack memory is small and fixed-size
Heap allows large or dynamically-sized data
Example
fn main() {
let x = Box::new(10);
println!("{}", x);
}
What happens?
10 is stored on the heap
x (the pointer) is on the stack
✅ Use when data is large or you want heap allocation.
2️⃣ Enable Recursive Data Structures (MOST COMMON USE)
Rust does not allow infinite-size types on the stack.
❌ This is NOT allowed
enum List {
Cons(i32, List), // ❌ infinite size
Nil,
}
✅ Correct way using Box
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(
List::Cons(2, Box::new(
List::Nil
))
));
}
Why Box?
Box has a known size
Enables linked lists, trees, graphs
✅ This is the #1 reason Box exists
3️⃣ Achieve Runtime Polymorphism (dyn Trait)
Traits don’t have a fixed size.
❌ This is NOT allowed
let a: dyn Animal; // ❌ size unknown
✅ Correct with Box
trait Animal {
fn sound(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn sound(&self) {
println!("Bark");
}
}
impl Animal for Cat {
fn sound(&self) {
println!("Meow");
}
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in animals {
a.sound();
}
}
Output
Bark
Meow
✅ Use Box for runtime polymorphism
(similar to interfaces + new in Java)
4️⃣ Reduce Stack Size (Performance & Safety)
Large structs can overflow stack.
Example
struct LargeData {
data: [u8; 1_000_000],
}
fn main() {
let big = Box::new(LargeData {
data: [0; 1_000_000],
});
println!("Allocated safely on heap");
}
✅ Prevents stack overflow
✅ Safer for big objects
5️⃣ Transfer Ownership Without Copying Large Data
Example
fn take_ownership(x: Box<i32>) {
println!("{}", x);
}
fn main() {
let a = Box::new(42);
take_ownership(a); // ownership moved
}
Why useful?
Only pointer moves (cheap)
Heap data stays in one place
6️⃣ Box Enables Self-Referential / Tree-like Structures
Example: Binary Tree
enum Tree {
Node(i32, Box<Tree>, Box<Tree>),
Empty,
}
fn main() {
let tree = Tree::Node(
10,
Box::new(Tree::Empty),
Box::new(Tree::Empty),
);
}
✅ Used in:
ASTs
Compilers
Expression trees
7️⃣ Box vs Reference (&T)
Big Idea (One line)
Rust Box is closest to Java’s new object allocation on the heap — but with strict ownership rules.
Java:
Dog d = new Dog();
Rust:
let d = Box::new(Dog);
Both:
Allocate object on heap
Variable holds a reference/pointer
Used for dynamic size / polymorphism
Heap Allocation (Java default vs Rust explicit)
Java (default behavior)
class Person {
int age;
}
public class Main {
public static void main(String[] args) {
Person p = new Person(); // heap
System.out.println(p.age);
}
}
Objects are always on heap
Developer doesn’t think about memory location
Rust (explicit with Box)
struct Person {
age: i32,
}
fn main() {
let p = Box::new(Person { age: 30 });
println!("{}", p.age);
}
Recursive Data Structures (IMP difference)
Java – works naturally
class Node {
int value;
Node next; // allowed
}
Why?
Java references have fixed size
Rust – ❌ without Box (NOT allowed)
enum List {
Cons(i32, List), // ❌ infinite size
Nil,
}
Rust – ✅ with Box
enum List {
Cons(i32, Box<List>),
Nil,
}
Why Java doesn’t need Box?
Java reference size is fixed
Rust needs compile-time size
📌 This is the #1 place where Box has NO Java equivalent
3️⃣ Runtime Polymorphism (Interface vs Trait Object)
Java (Interface)
interface Animal {
void sound();
}
class Dog implements Animal {
public void sound() { System.out.println("Bark"); }
}
class Cat implements Animal {
public void sound() { System.out.println("Meow"); }
}
public class Main {
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.sound();
a2.sound();
}
}
Rust (Trait Object needs Box)
trait Animal {
fn sound(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn sound(&self) { println!("Bark"); }
}
impl Animal for Cat {
fn sound(&self) { println!("Meow"); }
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in animals {
a.sound();
}
}
Comparison
Passing Objects to Functions
Java
void process(Person p) {
System.out.println(p.age);
}
process(new Person());
Reference copied
Object stays on heap
Rust
fn process(p: Box<Person>) {
println!("{}", p.age);
}
fn main() {
let p = Box::new(Person { age: 20 });
process(p); // ownership moved
}
Prevent Stack Overflow (Large Objects)
Java
class Big {
byte[] data = new byte[1_000_000];
}
Heap allocation by default
Rust
struct Big {
data: [u8; 1_000_000],
}
fn main() {
let b = Box::new(Big { data: [0; 1_000_000] });
}
👉 Box in Rust = Java’s default object allocation
difference between Box vs Rc vs Arc
Box – Single Owner (Default Choice)
When to use
One owner only
Recursive structures
Heap allocation
Fastest option
Example
fn main() {
let a = Box::new(10);
let b = a; // ownership moved
// println!("{}", a); ❌ compile error
println!("{}", b);
}
Key rule
Only one variable owns the value.
Java analogy
Object a = new Object();
Object b = a; // shared reference (Rust does NOT allow this)
Rust forbids accidental sharing → memory safety.
3️⃣ Rc – Shared Ownership (Single-Threaded)
When to use
Multiple owners
Same thread only
Trees, graphs
UI state, ASTs
Example
use std::rc::Rc;
fn main() {
let a = Rc::new(10);
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("a = {}", a);
println!("b = {}", b);
println!("c = {}", c);
println!("count = {}", Rc::strong_count(&a));
}
Output
a = 10
b = 10
c = 10
count = 3
Key rule
Data is dropped only when count becomes zero.
Java analogy
Object o = new Object();
Object a = o;
Object b = o;
// GC cleans when unreachable
❗ Rc is NOT thread-safe
// ❌ compile error
std::thread::spawn(move || {
println!("{}", a);
});
4️⃣ Arc – Shared Ownership (Multi-Threaded)
When to use
Multiple owners
Across threads
Concurrent programs
Example
use std::sync::Arc;
use std::thread;
fn main() {
let a = Arc::new(10);
let handles: Vec<_> = (0..3).map(|_| {
let a = Arc::clone(&a);
thread::spawn(move || {
println!("{}", a);
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("count = {}", Arc::strong_count(&a));
}
Output
10
10
10
count = 1
Why Arc?
Uses atomic reference counting
Safe across threads
Java analogy
AtomicReference ref = new AtomicReference<>(10);
5️⃣ Mutability with Rc & Arc (VERY IMPORTANT)
❌ Rc cannot be mutated directly
let x = Rc::new(5);
// x += 1 ❌
✅ Rc + RefCell (Single Thread)
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let x = Rc::new(RefCell::new(5));
*x.borrow_mut() += 1;
println!("{}", x.borrow());
}
✅ Arc + Mutex (Multi Thread)
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let x = Arc::new(Mutex::new(5));
let handles: Vec<_> = (0..3).map(|_| {
let x = Arc::clone(&x);
thread::spawn(move || {
let mut val = x.lock().unwrap();
*val += 1;
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("{}", *x.lock().unwrap());
}
6️⃣ Recursive Structure Comparison
Box (single owner)
enum List {
Cons(i32, Box<List>),
Nil,
}
Rc (shared tree)
use std::rc::Rc;
enum Node {
Value(i32),
Next(Rc<Node>),
}
Arc (shared across threads)
use std::sync::Arc;
enum Node {
Value(i32),
Next(Arc<Node>),
}
Top comments (0)