Rust for Java developers
You know Java well, so a lot of Rust will feel familiar. This page covers the parts that do not. Every Java program here ran with javac and java from Temurin JDK 25.0.5, and every Rust program ran with Rust 1.99.1, in the image behind this site (Linux ARM64). Maven and Gradle are not in the image, so anything about them is marked not run. Compile the Java files one at a time: two of them declare a Point.
RememberRust checks things that Java leaves to convention: who owns a value, who may change it, and what may cross a thread.
What carries over
Section titled “What carries over”- Static types, inference and generics with bounds.
<T extends Comparable<T>>becomesT: PartialOrd(Generics). - Interfaces and lambdas. A
traitis aninterfacewith default methods, and a closure is a lambda that may also change what it captured (Traits, Closures). - Streams.
iter().filter().map().collect()is a lazy pipeline, like aStream(Iterators). - Records and sealed interfaces. A
structwith derives is a record, and anenumwith data plusmatchis a sealed interface with a patternswitch(Enums). - Cleanup at the end of a block.
try-with-resources is the nearest idea to what Rust does for every value (habit 7).
Ten habits to unlearn
Section titled “Ten habits to unlearn”Each habit has the Java program on the left and what Rust does on the right. Where Rust refuses the Java shape, the right side is the real compiler error; fixes are in the idiom table or Try it yourself. The compiler errors name src/bin/fail.rs because each failing program was copied to that file to build it.
1. Reuse code with extends
Section titled “1. Reuse code with extends”A subclass inherits fields and methods, and a User variable can hold an Admin. Rust has no extends (No inheritance). The Admin holds a User, and a trait plays the part of the interface.
class User { String name;
User(String name) { this.name = name; }
String describe() { return "user " + name; }}
class Admin extends User { Admin(String name) { super(name); }
@Override String describe() { return "admin " + super.describe(); }}
public class Inherit { public static void main(String[] args) { User u = new Admin("ana"); System.out.println(u.describe()); System.out.println(u.name); }}admin user ana ana
trait Describe { fn describe(&self) -> String;}
struct User { name: String,}
struct Admin { user: User,}
impl Describe for User { fn describe(&self) -> String { format!("user {}", self.name) }}
impl Describe for Admin { fn describe(&self) -> String { format!("admin {}", self.user.describe()) }}
fn show(thing: &dyn Describe) { println!("{}", thing.describe());}
fn main() { let admin = Admin { user: User { name: String::from("ana"), }, }; show(&admin); println!("{}", admin.user.name);}admin user ana ana
super.describe() became self.user.describe(), and u.name became admin.user.name. You write the path. Shared behaviour goes in a trait’s default methods, shared data in a field (Traits).
2. Let null mean “no value”
Section titled “2. Let null mean “no value””ages.get("bo") returned null, and unboxing it into an int threw at run time, after the first line had printed. Rust’s get returns an Option, and an Option<&i32> is not an i32, so this does not compile.
import java.util.HashMap;import java.util.Map;
public class Nulls { public static void main(String[] args) { Map<String, Integer> ages = new HashMap<>(); ages.put("ana", 31); int ana = ages.get("ana"); System.out.println("ana: " + ana); int bo = ages.get("bo"); System.out.println("bo: " + bo); }}ana: 31 Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null at Nulls.main(Nulls.java:10)
use std::collections::HashMap;
fn main() { let mut ages = HashMap::new(); ages.insert("ana", 31); let ana: i32 = ages.get("ana"); println!("ana: {ana}");}error[E0308]: mismatched types
--> src/bin/fail.rs:6:20
|
6 | let ana: i32 = ages.get("ana");
| --- ^^^^^^^^^^^^^^^ expected `i32`, found `Option<&{integer}>`
| |
| expected due to this
|
= note: expected type `i32`
found enum `Option<&{integer}>`You decide what a missing key means: unwrap_or(0), a match, or an if let (Option). Java’s Optional exists, but null stays a legal value of a variable of type Optional too, as the Option lesson runs. Task 2 below runs the fix.
3. Two names for one list
Section titled “3. Two names for one list”In Java List<String> b = a copies the reference, and the == line shows one object. Rust moves the Vec into b, and using a afterwards is error E0382.
import java.util.ArrayList;import java.util.List;
public class Alias { public static void main(String[] args) { List<String> a = new ArrayList<>(); a.add("x"); List<String> b = a; b.add("y"); System.out.println(a); System.out.println(a == b); }}[x, y] true
fn main() { let a = vec![String::from("x")]; let mut b = a; b.push(String::from("y")); println!("{a:?}");}error[E0382]: borrow of moved value: `a`
--> src/bin/fail.rs:5:16
|
2 | let a = vec![String::from("x")];
| - move occurs because `a` has type `Vec<String>`, which does not implement the `Copy` trait
3 | let mut b = a;
| - value moved here
4 | b.push(String::from("y"));
5 | println!("{a:?}");
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let mut b = a.clone();
| ++++++++If you want two lists, call clone(). If you want two names for one list, borrow it with &mut. Task 1 runs both. See Ownership and Copy or reference?.
4. Trust final
Section titled “4. Trust final”final stops the variable from pointing at another list. The list itself still took two add calls. In Rust let freezes the value too, unless the type allows interior mutation (Smart pointers).
import java.util.ArrayList;import java.util.List;
public class Final { public static void main(String[] args) { final List<String> names = new ArrayList<>(); names.add("ana"); names.add("bo"); System.out.println(names); }}[ana, bo]
fn main() { let names = Vec::new(); names.push("ana"); println!("{names:?}");}error[E0596]: cannot borrow `names` as mutable, as it is not declared as mutable
--> src/bin/fail.rs:3:5
|
3 | names.push("ana");
| ^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut names = Vec::new();
| +++let mut opts in. The same split shows in signatures: &Vec<T> can read, &mut Vec<T> can change (Variables, Mutable borrows).
5. Report failure with an exception
Section titled “5. Report failure with an exception”Integer.parseInt declares nothing, so javac did not ask for a catch, and the failure arrived as a stack trace. Rust’s parse returns a Result, and using it as a u16 does not compile.
public class Parse { static int port(String text) { return Integer.parseInt(text); }
public static void main(String[] args) { System.out.println(port("8080")); System.out.println(port("eighty")); }}8080 Exception in thread "main" java.lang.NumberFormatException: For input string: "eighty" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:565) at java.base/java.lang.Integer.parseInt(Integer.java:662) at Parse.port(Parse.java:3) at Parse.main(Parse.java:8)
fn port(text: &str) -> u16 { text.parse()}
fn main() { println!("{}", port("8080"));}error[E0308]: mismatched types
--> src/bin/fail.rs:2:5
|
1 | fn port(text: &str) -> u16 {
| --- expected `u16` because of return type
2 | text.parse()
| ^^^^^^^^^^^^ expected `u16`, found `Result<_, _>`
|
= note: expected type `u16`
found enum `Result<_, _>`
help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err`
|
2 | text.parse().expect("REASON")
| +++++++++++++++++The failure is part of the return type, so every caller sees it. Handle it with match, give it a default (row 12 of the table), or pass it up with ? (the stack-traces bullet below); see Errors. Java’s checked exceptions are the nearest relative, but a Result is an ordinary value that you can store or collect.
6. Return whatever you have, and let the GC keep it alive
Section titled “6. Return whatever you have, and let the GC keep it alive”longest returned one of its two arguments, and the program printed the result even though inner, a new String on the heap, had gone out of scope: the object lives while something refers to it. Rust has no collector to keep a borrowed string alive, so the signature has to say which input the result borrows from.
public class Longest { static String longest(String a, String b) { return a.length() >= b.length() ? a : b; }
public static void main(String[] args) { String result; { String inner = new String("a longer string"); result = longest("short", inner); } System.out.println(result); }}a longer string
fn longest(a: &str, b: &str) -> &str { if a.len() >= b.len() { a } else { b }}
fn main() { println!("{}", longest("short", "a longer string"));}error[E0106]: missing lifetime specifier
--> src/bin/fail.rs:1:33
|
1 | fn longest(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++The compiler’s suggestion, <'a>, ties the result to both inputs. If a function builds new text, return an owned String and skip lifetimes (Returning references, Lifetimes).
7. Close resources with try, and only there
Section titled “7. Close resources with try, and only there”a and b were closed in reverse order because the program used try-with-resources. c was opened without it, and in this program nothing ever printed close c. In Rust a value is dropped when its owner goes out of scope, so _c was closed at the end of main and nobody had to remember anything.
class Res implements AutoCloseable { final String name;
Res(String name) { this.name = name; System.out.println("open " + name); }
@Override public void close() { System.out.println("close " + name); }}
public class Cleanup { public static void main(String[] args) { try (Res a = new Res("a"); Res b = new Res("b")) { System.out.println("working"); } Res c = new Res("c"); System.out.println("done"); }}open a open b working close b close a open c done
struct Res(&'static str);
impl Res { fn open(name: &'static str) -> Res { println!("open {name}"); Res(name) }}
impl Drop for Res { fn drop(&mut self) { println!("close {}", self.0); }}
fn main() { { let _a = Res::open("a"); let _b = Res::open("b"); println!("working"); } let _c = Res::open("c"); println!("done");}open a open b working close b close a open c done close c
You write the Drop once, for the type (Drop). See the Java Tutorials on try-with-resources.
8. Expect generics to be erased
Section titled “8. Expect generics to be erased”At run time a List<String> and a List<Integer> were the same class (the first two lines: that is erasure). The last line is boxing, not erasure: a List cannot hold an int, so the two 1000s became separate Integer objects and == compared references. Rust’s generics are not erased: each Vec<T> is compiled for its own T, and == compares values.
import java.util.ArrayList;import java.util.List;
public class Erasure { public static void main(String[] args) { List<String> words = new ArrayList<>(); List<Integer> numbers = new ArrayList<>(); numbers.add(1000); numbers.add(1000); System.out.println(words.getClass() == numbers.getClass()); System.out.println(words.getClass().getName()); System.out.println(numbers.get(0) == numbers.get(1)); }}true java.util.ArrayList false
use std::any::type_name;
fn element_type<T>(_: &Vec<T>) -> &'static str { type_name::<T>()}
fn main() { let words: Vec<String> = Vec::new(); let numbers: Vec<i32> = vec![1000, 1000, 7]; println!("{} and {}", element_type(&words), element_type(&numbers)); let gap = numbers[1..].as_ptr().addr() - numbers.as_ptr().addr(); println!("bytes between neighbours: {gap}"); println!("{}", numbers[0] == numbers[1]);}alloc::string::String and i32 bytes between neighbours: 4 true
The names that type_name prints are for diagnostics, and its docs do not promise their text. The gap of 4 bytes comes from the i32 size (Generics; the Java Tutorials describe erasure).
9. Write getters, setters and equals by hand
Section titled “9. Write getters, setters and equals by hand”A plain class with private fields and getters is not equal to another one with the same numbers until you write equals and hashCode; a record writes them. A Rust struct has no equality either, and the compiler says so instead of answering false.
class PointClass { private final int x; private final int y;
PointClass(int x, int y) { this.x = x; this.y = y; }
int getX() { return x; } int getY() { return y; }}
record Point(int x, int y) {}
public class Records { public static void main(String[] args) { System.out.println(new PointClass(1, 2).equals(new PointClass(1, 2))); System.out.println(new Point(1, 2).equals(new Point(1, 2))); System.out.println(new Point(1, 2)); }}false true Point[x=1, y=2]
struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; println!("{}", a == b);}error[E0369]: binary operation `==` cannot be applied to type `Point`
--> src/bin/fail.rs:9:22
|
9 | println!("{}", a == b);
| - ^^ - Point
| |
| Point
|
note: an implementation of `PartialEq` might be missing for `Point`
--> src/bin/fail.rs:1:1
|
1 | struct Point {
| ^^^^^^^^^^^^ must implement `PartialEq`
help: consider annotating `Point` with `#[derive(PartialEq)]`
|
1 + #[derive(PartialEq)]
2 | struct Point {One line of derive writes the rest:
#[derive(Debug, PartialEq, Eq, Hash)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; println!("{}", a == b); println!("{a:?}");}true
Point { x: 1, y: 2 }Fields are private to their module, not to the type, so code in the same module can read p.x (Structs, Modules, Equality and hashing).
10. Share a list between threads and add a lock later
Section titled “10. Share a list between threads and add a lock later”Four threads added to one ArrayList. javac did not object, and the run printed a smaller count than the 400,000 expected, or one thread died with ArrayIndexOutOfBoundsException (this output varies between runs). In Rust the same shape does not compile: the Vec moves into the first thread’s closure.
import java.util.ArrayList;import java.util.List;
public class Racy { public static void main(String[] args) throws InterruptedException { List<Integer> results = new ArrayList<>(); List<Thread> threads = new ArrayList<>(); for (int i = 0; i < 4; i++) { int n = i; threads.add(new Thread(() -> { for (int j = 0; j < 100_000; j++) { results.add(n * 10); } })); } threads.forEach(Thread::start); for (var t : threads) { t.join(); } System.out.println(results.size() + " items (expected 400000)"); }}103148 items (expected 400000)
This output changes from run to run. This is one real run.
use std::thread;
fn main() { let mut results = Vec::new(); let mut handles = Vec::new(); for i in 0..4 { handles.push(thread::spawn(move || { for _ in 0..100_000 { results.push(i * 10); } })); } for h in handles { h.join().unwrap(); } println!("{} items", results.len());}error[E0382]: borrow of moved value: `results`
--> src/bin/fail.rs:16:26
|
4 | let mut results = Vec::new();
| ----------- move occurs because `results` has type `Vec<i32>`, which does not implement the `Copy` trait
5 | let mut handles = Vec::new();
6 | for i in 0..4 {
| ------------- inside of this loop
7 | handles.push(thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
8 | for _ in 0..100_000 {
| ------------------- inside of this loop
...
16 | println!("{} items", results.len());
| ^^^^^^^ value borrowed here after move
|
help: consider cloning the value before moving it into the closure
|
7 ~ let value = results.clone();
8 ~ handles.push(thread::spawn(move || {
9 | for _ in 0..100_000 {
10 ~ value.push(i * 10);To share it, you wrap it in Arc<Mutex<_>>, and the count is right:
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let results = Arc::new(Mutex::new(Vec::new())); let mut handles = Vec::new(); for i in 0..4 { let results = Arc::clone(&results); handles.push(thread::spawn(move || { for _ in 0..100_000 { results.lock().unwrap().push(i * 10); } })); } for h in handles { h.join().unwrap(); } println!("{} items (expected 400000)", results.lock().unwrap().len());}400000 items (expected 400000)
Send and Sync explains why safe Rust code cannot contain a data race. It does not stop deadlocks, or a wrong check-then-act across two locks (Shared state).
Idiom by idiom
Section titled “Idiom by idiom”Every cell was run: the Java cells in one program, the Rust cells in another. Open the box below the table to see both and what they printed. Where a program spreads a cell over several lines, the table shows it on one.
| # | You write in Java | In Rust | Lesson |
|---|---|---|---|
| 1 | final var x = 1; |
let x = 1; |
Variables |
| 2 | count++; |
count += 1; |
What Rust doesn’t have |
| 3 | String size = n > 5 ? "big" : "small"; |
let size = if n > 5 { "big" } else { "small" }; |
If / else |
| 4 | String level = switch (n) { case 1, 2 -> "low"; default -> "high"; }; |
let level = match n { 1 | 2 => "low", _ => "high" }; |
Match |
| 5 | Optional.of("hi").map(String::length).orElse(0) |
Some("hi").map(|s| s.len()).unwrap_or(0) |
Option |
| 6 | ages.getOrDefault("bo", 0) |
ages.get("bo").copied().unwrap_or(0) |
Maps |
| 7 | counts.merge("a", 1, Integer::sum); |
*counts.entry("a").or_insert(0) += 1; |
Maps |
| 8 | nums.stream().filter(m -> m % 2 == 0).map(m -> m * m).toList() |
nums.iter().filter(|n| **n % 2 == 0).map(|n| n * n).collect::<Vec<_>>() |
Iterators |
| 9 | for (String s : names) |
for s in &names |
Looping over a collection |
| 10 | String.join(", ", names) |
names.join(", ") |
Working with text |
| 11 | word.length() |
word.chars().count() (word.len() counts bytes) |
Strings |
| 12 | try { parsed = Integer.parseInt(s); } catch (NumberFormatException e) { parsed = -1; } |
let parsed = s.parse::<i32>().unwrap_or(-1); |
Errors |
| 13 | record Point(int x, int y) {} |
#[derive(Debug, PartialEq, Eq, Hash)] struct Point { x: i32, y: i32 } |
Common traits |
| 14 | if (shape instanceof Circle c) |
if let Shape::Circle(r) = shape |
Enums |
| 15 | default String hi() { return "hi"; } |
fn hi(&self) -> String { String::from("hi") } |
Traits |
| 16 | new ArrayList<>(original) |
original.clone() |
Ownership |
| 17 | CompletableFuture.supplyAsync(() -> 5).join() |
thread::spawn(|| 5).join().unwrap() |
Threads |
| 18 | synchronized (lock) { box[0]++; } |
*count.lock().unwrap() += 1; |
Shared state |
| 19 | (int) 3.9 |
3.9_f64 as i32 |
Numbers and types |
| 20 | "x=" + n |
format!("x={n}") |
Printing and formatting |
Rows 11, 17 and 18 are not exact twins. Row 11: length() in Java counts UTF-16 units, and Rust’s chars() counts Unicode scalar values. Both gave 5 for héllo, while len() gave 6. Row 17: supplyAsync ran the work on a pool (the 17 pool line printed ForkJoinPool.commonPool-worker-1), and thread::spawn starts an operating-system thread. Row 18: in Rust the Mutex owns the number, and in Java the lock is a separate object next to it.
The two programs behind the table
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Optional;import java.util.concurrent.CompletableFuture;
sealed interface Shape permits Circle, Square {}record Circle(double radius) implements Shape {}record Square() implements Shape {}record Point(int x, int y) {}
interface Greeter { default String hi() { return "hi"; }}
class English implements Greeter {}
public class Idioms { public static void main(String[] args) { final var x = 1; System.out.println("1: " + x);
int count = 0; count++; System.out.println("2: " + count);
int n = 7; String size = n > 5 ? "big" : "small"; System.out.println("3: " + size);
String level = switch (n) { case 1, 2 -> "low"; default -> "high"; }; System.out.println("4: " + level);
int len = Optional.of("hi").map(String::length).orElse(0); System.out.println("5: " + len);
Map<String, Integer> ages = new HashMap<>(); ages.put("ana", 31); int bo = ages.getOrDefault("bo", 0); System.out.println("6: " + bo);
Map<String, Integer> counts = new HashMap<>(); counts.merge("a", 1, Integer::sum); System.out.println("7: " + counts.get("a"));
List<Integer> nums = List.of(1, 2, 3, 4); List<Integer> squares = nums.stream().filter(m -> m % 2 == 0).map(m -> m * m).toList(); System.out.println("8: " + squares);
List<String> names = List.of("ana", "bo"); StringBuilder seen = new StringBuilder(); for (String s : names) { seen.append(s); } System.out.println("9: " + seen);
System.out.println("10: " + String.join(", ", names));
String word = "héllo"; System.out.println("11: " + word.length() + " chars");
String s = "eighty"; int parsed; try { parsed = Integer.parseInt(s); } catch (NumberFormatException e) { parsed = -1; } System.out.println("12: " + parsed);
System.out.println("13: " + new Point(1, 2).equals(new Point(1, 2)));
for (Shape shape : List.of(new Circle(2.5), new Square())) { if (shape instanceof Circle c) { System.out.println("14: circle, radius " + c.radius()); } else { System.out.println("14: not a circle"); } }
System.out.println("15: " + new English().hi());
List<String> original = List.of("a", "b"); List<String> copy = new ArrayList<>(original); copy.add("c"); System.out.println("16: " + original.size() + " " + copy.size());
System.out.println("17: " + CompletableFuture.supplyAsync(() -> 5).join()); System.out.println("17 pool: " + CompletableFuture.supplyAsync(() -> Thread.currentThread().getName()).join());
Object lock = new Object(); int[] box = {0}; synchronized (lock) { box[0]++; } System.out.println("18: " + box[0]);
System.out.println("19: " + (int) 3.9);
System.out.println("20: " + "x=" + n); }}1: 1 2: 1 3: big 4: high 5: 2 6: 0 7: 1 8: [4, 16] 9: anabo 10: ana, bo 11: 5 chars 12: -1 13: true 14: circle, radius 2.5 14: not a circle 15: hi 16: 2 3 17: 5 17 pool: ForkJoinPool.commonPool-worker-1 18: 1 19: 3 20: x=7
use std::collections::HashMap;use std::sync::Mutex;use std::thread;
#[derive(Debug, PartialEq, Eq, Hash)]struct Point { x: i32, y: i32,}
enum Shape { Circle(f64), Square,}
trait Greeter { fn hi(&self) -> String { String::from("hi") }}
struct English;
impl Greeter for English {}
fn main() { let x = 1; println!("1: {x}");
let mut count = 0; count += 1; println!("2: {count}");
let n = 7; let size = if n > 5 { "big" } else { "small" }; println!("3: {size}");
let level = match n { 1 | 2 => "low", _ => "high", }; println!("4: {level}");
let len = Some("hi").map(|s| s.len()).unwrap_or(0); println!("5: {len}");
let mut ages = HashMap::new(); ages.insert("ana", 31); let bo = ages.get("bo").copied().unwrap_or(0); println!("6: {bo}");
let mut counts = HashMap::new(); *counts.entry("a").or_insert(0) += 1; println!("7: {}", counts["a"]);
let nums = vec![1, 2, 3, 4]; let squares = nums .iter() .filter(|n| **n % 2 == 0) .map(|n| n * n) .collect::<Vec<_>>(); println!("8: {squares:?}");
let names = vec!["ana", "bo"]; let mut seen = String::new(); for s in &names { seen.push_str(s); } println!("9: {seen}");
println!("10: {}", names.join(", "));
let word = "héllo"; println!( "11: {} chars, len() is {}", word.chars().count(), word.len() );
let s = "eighty"; let parsed = s.parse::<i32>().unwrap_or(-1); println!("12: {parsed}");
let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; println!("13: {}", a == b);
for shape in [Shape::Circle(2.5), Shape::Square] { if let Shape::Circle(r) = shape { println!("14: circle, radius {r}"); } else { println!("14: not a circle"); } }
println!("15: {}", English.hi());
let original = vec!["a", "b"]; let mut copy = original.clone(); copy.push("c"); println!("16: {} {}", original.len(), copy.len());
println!("17: {}", thread::spawn(|| 5).join().unwrap());
let count = Mutex::new(0); *count.lock().unwrap() += 1; println!("18: {}", count.lock().unwrap());
println!("19: {}", 3.9_f64 as i32);
println!("20: {}", format!("x={n}"));}1: 1 2: 1 3: big 4: high 5: 2 6: 0 7: 1 8: [4, 16] 9: anabo 10: ana, bo 11: 5 chars, len() is 6 12: -1 13: true 14: circle, radius 2.5 14: not a circle 15: hi 16: 2 3 17: 5 18: 1 19: 3 20: x=7
Toolchain map
Section titled “Toolchain map”Cargo is the build tool, and also the front door to the compiler, the formatter, the linter and the documentation generator. The commands below ran on a two-function project (one Java file, one Rust crate with a unit test and a doc-test).
| Job | Java | Rust | Run? |
|---|---|---|---|
| New project | mvn archetype:generate, gradle init |
cargo new demo |
Cargo run. Maven and Gradle not run |
| Build file | pom.xml, build.gradle |
Cargo.toml |
Not run for Java |
| Compile | javac Slug.java |
cargo build |
Both run |
| Run | java Slug, or java Slug.java |
cargo run |
Both run |
| Test | JUnit through Maven or Gradle | cargo test |
Rust run. JUnit not run |
| Format | a plugin or tool such as Spotless or google-java-format | cargo fmt --check |
Rust run. Java not run |
| Lint | javac -Xlint:all (the compiler’s own warnings, not a linter) |
cargo clippy |
Both run |
| Docs | javadoc |
cargo doc |
Both run |
| Dependencies | coordinates in pom.xml, from Maven Central |
cargo add, from crates.io (Using crates) |
Not run |
| Debugger | jdb |
rust-gdb, rust-lldb (wrappers for gdb and lldb) |
jdb ran with -version. No debugger session run: gdb and lldb are not in the image |
| REPL | jshell |
none in cargo --list |
jshell found; no Cargo command mentions a REPL |
The Rust and Java commands that ran
An empty output means the command printed nothing. For cargo fmt --check and cargo clippy that is the pass result in this run, and javac is quiet when there is nothing to report.
$ cargo new demo Creating binary (application) `demo` package note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html $ cargo build $ cargo test running 2 tests test tests::joins_words ... ok test tests::ignores_extra_spaces ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests slugtools running 1 test test src/lib.rs - slug (line 3) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s all doctests ran in 0.12s; merged doctests compilation took 0.12s $ cargo fmt --check $ cargo clippy $ cargo doc --no-deps Documenting slugtools v0.1.0 (/home/you/habits) Generated target/doc/slugtools/index.html $ cargo run hello-rust-world
This output changes from run to run. This is one real run.
/// Turns a title into a URL slug: lowercase words joined by `-`.////// ```/// assert_eq!(slugtools::slug("Hello Rust World"), "hello-rust-world");/// ```pub fn slug(title: &str) -> String { title .split_whitespace() .map(str::to_lowercase) .collect::<Vec<_>>() .join("-")}
#[cfg(test)]mod tests { use super::*;
#[test] fn joins_words() { assert_eq!(slug("Hello Rust World"), "hello-rust-world"); }
#[test] fn ignores_extra_spaces() { assert_eq!(slug(" Hello Rust "), "hello-rust"); }}$ javac -Xlint:all Slug.java $ java Slug hello-java-world $ java Slug.java hello-java-world $ javadoc -quiet -d docs Slug.java $ command -v jshell /opt/jdk/bin/jshell $ jdb -version This is jdb version 25.0 (Java SE version 25.0.5)
/** Turns a title into a URL slug: lowercase words joined by "-". */public class Slug { private Slug() {}
/** * Builds the slug. * * @param title the text to convert * @return lowercase words joined by "-" */ public static String slug(String title) { return String.join("-", title.trim().toLowerCase().split("\\s+")); }
/** * Prints the slug of a fixed title. * * @param args not used */ public static void main(String[] args) { System.out.println(slug("Hello Java World")); }}What the image does not have, and the Cargo commands that mention a REPL:
$ for t in mvn gradle gdb lldb rust-gdb; do command -v $t || echo "$t: not found"; done mvn: not found gradle: not found gdb: not found lldb: not found /opt/cargo/bin/rust-gdb $ cargo --list | grep -ci repl # how many cargo commands mention a REPL 0
A reading path
Section titled “A reading path”In this order, for a Java reader:
- What Rust doesn’t have: your reflexes (
++,?:,null,new, overloads) in one table. - Values, the stack and the heap: a Java object variable is a reference; a Rust variable holds the value.
- Ownership, Borrowing and Mutable borrows: habits 3 and 4 are these three lessons.
- Structs and Methods: no constructors, no inheritance.
- Enums, Option and Errors: sealed interfaces,
nulland exceptions. - Traits, Generics and Trait objects: interfaces,
<T extends ...>, andList<Shape>. - Closures and Iterators: lambdas and streams, with references in the items.
- Returning references and Lifetimes: where the garbage collector used to decide.
- Smart pointers and Working with the borrow checker:
Rcfor shared ownership, indices for graphs. - Threads, Shared state and Send and Sync: habit 10 in full.
- Async basics: futures that start only when polled, compared with
CompletableFutureand virtual threads. - Project structure: Cargo workspaces next to Maven or Gradle multi-module builds.
If you also read Kotlin or C#, the same page exists for them: Rust for Kotlin developers and Rust for C# developers.
What you will miss
Section titled “What you will miss”-
A REPL.
jshellis in the JDK. In this image nocargocommand mentions a REPL (the last session printed 0). Crates such as evcxr provide one; I did not run it. -
Automatic stack traces. Habit 5’s Java failure printed five frames. A Rust
mainthat returns anErrprints one line:Rustsrc/bin/noframes.rsfn main() -> Result<(), std::num::ParseIntError> {let port: u16 = "eighty".parse()?;println!("port {port}");Ok(())}$ cargo run --bin noframesError: ParseIntError { kind: InvalidDigit }To get a backtrace, the anyhow guide runs a program with
RUST_BACKTRACE=1. -
Run-time reflection. The
std::anymodule offersAny,TypeId,type_nameand downcasting. Its docs call it utilities for “dynamic typing or type reflection”, and say&dyn Anyis limited to testing for a concrete type. I found nothing there that lists a type’s fields or methods (std::any). Rust code generation happens at compile time, through macros (Macros you’ll read). -
Overloading and default arguments. One name, one function. Use different names, a builder or a struct (the lesson).
-
Object graphs with back-pointers. A parent pointer or a cycle is just a field in Java. In Rust you choose
RcwithWeak, or ids into aVec(Cycles, Indices, not references).
Try it yourself
Section titled “Try it yourself”I ran all three in the image.
-
Fix habit 3 in two ways. Change
let mut b = a;tolet mut b = a.clone();andastays["x"]. Makeamutable and writelet b = &mut a;instead, and the push throughbshows inabecause they are one list:terminal$ cargo run --bin one # with: let mut b = a.clone(); ["x"] $ cargo run --bin two # with: let mut a = vec![...]; let b = &mut a; ["x", "y"]
-
Make the missing key a value. Replace the
i32line of habit 2 withages.get("ana").copied().unwrap_or(0)and look up"bo"the same way. It compiles, andbois 0:terminal$ cargo run --bin fixed # with: .get("ana").copied().unwrap_or(0), and a lookup of "bo" ana: 31 bo: 0 -
Run both racing programs three times. Expect a Java count below 400000 that changes between runs (or an
ArrayIndexOutOfBoundsException: 5 of 60 separate runs of mine, not shown here), and Rust’s 400000 each time:Java$ for i in 1 2 3; do java Racy; done 122298 items (expected 400000) 105329 items (expected 400000) 103826 items (expected 400000)This output changes from run to run. This is one real run.
Rust$ for i in 1 2 3; do cargo run --bin shared; done 400000 items (expected 400000) 400000 items (expected 400000) 400000 items (expected 400000)
Read more
Section titled “Read more”- The Rust Book, chapter 4: ownership (habits 3 and 6)
- The Rust Book, chapter 9: error handling (habit 5)
- The Rust Book, chapter 18: object-oriented programming features (habit 1)
std::option::Option(habit 2) andstd::ops::Drop(habit 7)cargo newin the Cargo Book- Java Tutorials: type erasure and the try-with-resources statement
- How Rust wants you to think and the standard library tour
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.