Jump to content

Rust for Java Developers: Difference between revisions

From JOHNWICK
PC (talk | contribs)
Created page with "If you come from a Java background, you’re used to interfaces, inheritance, and garbage collection taking care of memory. Rust re-imagines these ideas — favoring compile-time safety and performance without a garbage collector. This post bridges that gap so you can map familiar Java concepts to Rust’s world. Basic Concepts in Rust Traits in Rust are conceptually like a Java interface. A trait specifies a set of method signatures that must be implemented. // Defines..."
 
(No difference)

Latest revision as of 17:48, 15 November 2025

If you come from a Java background, you’re used to interfaces, inheritance, and garbage collection taking care of memory. Rust re-imagines these ideas — favoring compile-time safety and performance without a garbage collector. This post bridges that gap so you can map familiar Java concepts to Rust’s world. Basic Concepts in Rust Traits in Rust are conceptually like a Java interface. A trait specifies a set of method signatures that must be implemented. // Defines a summary interface trait Summary {

   fn summarize(&self) -> String;

} A Type in Rust defines the structure and characteristics of a value. Rust is a statically-typed language, meaning the type of every variable is known at compile time. You can define custom types using Structs and Enums. // Define a struct (the data type) struct NewsArticle {

   headline: String,
   location: String,

}

// Define a second struct (the data type) struct Tweet {

   username: String,
   content: String,

} When a struct implements a trait, it means the struct provides concrete implementations for all the methods defined in that trait. // Struct NewsArticle implements Summary interface impl Summary for NewsArticle {

   fn summarize(&self) -> String {
       format!("{}, by our correspondent in {}", self.headline, self.location)
   }

}

// Struct Tweet implements Summary interface impl Summary for Tweet {

   fn summarize(&self) -> String {
       format!("@{}: {}", self.username, self.content)
   }

} Polymorphism (“many forms”) is the ability to treat objects of different types / structs uniformly. A function can accept any structure which implements a given trait. The below syntax means, Item is of type T, and T must implement the Summary trait. // The function can accept any type T that implements the 'Summary' interface fn notify<T: Summary>(item: &T) {

   println!("Breaking news! {}", item.summarize());

} Static & Dynamic Dispatch In programming, dispatch means how a program decides which exact function implementation to call when a method is invoked on an object. Static Dispatch (Compile Time) When you compile this, Rust generates two specialized versions of notify — one for NewsArticle, one for Tweet. This process is called monomorphization. The advantage is zero runtime overhead. Monomorphization (from “mono” = single, and “morph” = form/type) is the process Rust uses to turn generic code into specialized concrete code for each type that you use it with — effectively “baking in” the type parameters at compile time. fn main() {

   let article = NewsArticle {
       headline: String::from("Mars Mission Successful"),
       location: String::from("Bangalore"),
   };
   let tweet = Tweet {
       username: String::from("astro_kalpana"),
       content: String::from("We’ve landed on Mars!"),
   };
   notify(&article);
   notify(&tweet);

} Dynamic Dispatch (Runtime) Here, notify_dynamic doesn’t know what concrete type item is — it could be any type implementing Summary. Every struct in Rust stores a pointer to data inside the struct, and also a pointer to the functions it has implemented (vtable — virtual function table). At runtime, Rust looks up the correct method via the vtable and calls it. fn notify_dynamic(item: &dyn Summary) {

   println!("(Dynamic) Breaking news! {}", item.summarize());

}

fn main() {

   let article = NewsArticle {
       headline: String::from("Ocean Cleanup Succeeds"),
       location: String::from("San Francisco"),
   };
   let tweet = Tweet {
       username: String::from("eco_warrior"),
       content: String::from("Over 100 tons of plastic removed from the ocean."),
   };
   // Both types share a common trait reference
   let items: Vec<&dyn Summary> = vec![&article, &tweet];
   for item in items {
       notify_dynamic(item); // Uses vtable to resolve the correct method
   }

} Memory Safety in Rust The borrower (or borrowing) concept in Rust is central to how the language enforces memory safety without a garbage collector. Every value in Rust has one owner — the variable that holds it. When that variable goes out of scope, the memory is automatically freed. fn main() {

   let s = String::from("hello"); // s owns this String, main owns s
   println!("{}", s);

} // s goes out of scope here → String memory is freed But what if you want to let another function use that value without taking ownership of it? Borrowing means temporarily accessing someone else’s data without becoming its owner. You do this using references: &T for immutable borrows and &mut T for mutable borrows. You can have any number of immutable (&T) borrows or exactly one mutable (&mut T) borrow at a time — never both. Immutable Borrow (Read-Only)

  • The ownership of s stays with main.
  • print_length borrows s immutably using &String.
  • After the function call, you can still use s because you never transferred ownership.

fn print_length(s: &String) {

   println!("Length is {}", s.len());

}

fn main() {

   let s = String::from("hello");
   print_length(&s); // Borrow `s` (read-only)
   println!("{}", s); // Still can use s afterward

} Mutable Borrow (Read + Write)

  • While s is borrowed mutably, no one else (not even the owner) can access s.

fn append_world(s: &mut String) {

   s.push_str(" world");

}

fn main() {

   let mut s = String::from("hello");
   append_world(&mut s); // Mutable borrow
   println!("{}", s);

} Heap Cleanup in Rust

  • When Rust drops a reference, it also drops the value it points to.

// Java Example void main() {

   String s = new String("hello");

} // s reference disappears, but actual memory freed later by GC

// Rust Example fn main() {

   let s = String::from("hello");

} // s dropped immediately here, heap freed right now Compile Time Errors

  • Rust’s borrow checker ensures that no reference outlives its owner.

let r; {

   let s = String::from("hello");
   r = &s; // ❌ r will live, s will go out of scope, rust error here

} println!("{}", r);

  • Syntax: <'a> (read as “for some lifetime 'a”). In below example, the function promises: “the returned reference will be valid for as long as both x and y are valid.”

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

   if x.len() > y.len() {
       x
   } else {
       y
   }

}

let s1 = String::from("hello"); let result; {

   let s2 = String::from("world");
   
   // ❌ result lives, s2 goes out of scope
   result = longest(s1.as_str(), s2.as_str());  // error out here

} println!("Longest is {}", result);

  • If you create an immutable reference (borrow) to s. You can read s through r1. But as long as this immutable borrow is active, no one else can modify s. If you create a mutable reference to the same variable s while the immutable reference r1 still exists, the compiler sees that r1 might be used in the same scope as r2 . Because allowing bothr1 (a reader) and r2 (a writer) at the same time could lead to a data race — one part of the code could be reading while another is writing.

let mut s = String::from("hello"); let r1 = &s; let r2 = &mut s; // ❌ cannot borrow as mutable while immutable borrow exists println!("{} {}", r1, r2);

  • You can fix the above by making sure the immutable borrow r1 is no longer used when you create the mutable one.

fn main() {

   let mut s = String::from("hello");
   {
       let r1 = &s; // immutable borrow
       println!("{}", r1); // used here
   } // r1 goes out of scope here
   let r2 = &mut s; // now mutable borrow is allowed
   r2.push_str(" world");
   println!("{}", r2);

} Multi Threading in Rust In Rust, ownership and borrowing rules are enforced across threads too. In the below example, v belongs to the main thread. The spawned thread might outlive it, leading to a dangling reference. Rust refuses to allow that. use std::thread;

fn main() {

   let v = vec![1, 2, 3];
   thread::spawn(|| {
       // ❌ Error: `v` does not live long enough
       println!("{:?}", v);
   });

} Note, || syntax in Rust is how you define a closure, which is Rust’s equivalent of a lambda expression in Java. A closure is an anonymous function that can capture variables from the surrounding scope. In above case there is no variable. Here is an example below, let add = |x, y| x + y; println!("{}", add(2, 3)); // prints 5 Closures can capture variables from their surrounding environment automatically — by reference, mutable reference, or by value — depending on how you use them. let v = vec![1, 2, 3]; let print_v = || println!("{:?}", v); print_v(); But inside a new thread, Rust needs to ensure safety. If the spawned thread might outlive v, the compiler complains — hence you must use move to explicitly transfer ownership. Now the closure takes ownership of v, and Rust knows it’s safe. use std::thread;

fn main() {

   let v = vec![1, 2, 3];
   thread::spawn(move || {
       // ❌ Error: `v` does not live long enough
       println!("{:?}", v);
   });

}