Bỏ qua để đến nội dung

Rust cho lập trình viên Java: hướng dẫn thực hành

Nếu bạn đã gắn bó nhiều năm với Java, bạn đã có gần đủ mọi thứ cần thiết để học Rust. Bạn tư duy bằng kiểu. Bạn biết generic, interface, lambda và stream pipeline. Rust có tất cả những thứ đó: trait trông rất giống interface có phương thức mặc định, iterator đọc lên như stream, và Cargo làm việc của Maven hay Gradle, chỉ là cần ít cấu hình hơn rất nhiều.

Cái mới là tất cả những gì JVM từng làm giúp bạn. Rust không có bộ thu gom rác (garbage collector) và cũng không có máy ảo. Code của bạn biên dịch thành một file binary native, và trình biên dịch quyết định khi nào bộ nhớ được giải phóng bằng cách kiểm tra quyền sở hữu (ownership) và mượn (borrowing) trước khi chương trình chạy. Rust cũng không có null (bạn dùng Option), không có exception (bạn dùng Result), và không có kế thừa class. Bốn thay đổi này là nơi bạn sẽ tốn phần lớn thời gian học.

Tại sao lập trình viên Java học Rust? Thường là vì một trong những lý do sau: những service cần dùng ít bộ nhớ, ổn định và không bị GC làm khựng; công cụ dòng lệnh và serverless function cần khởi động tức thì mà không có JVM; một đường chạy nóng trong ứng dụng Java mà bạn muốn chuyển sang code native (Java có thể gọi Rust qua JNI hoặc qua Foreign Function & Memory API mới hơn); hoặc đơn giản là tò mò vì sao Rust cứ đứng đầu các khảo sát “ngôn ngữ được yêu thích nhất”. Trang này đối chiếu những gì bạn biết từ Java sang Rust, để bạn tập trung vào những phần thực sự khác biệt.

Nếu bạn muốn một bài so sánh chung với C++, Go và Python thì hãy xem Rust so với C++, Go và Python. Nếu bạn cũng biết .NET, có một trang riêng: Rust cho lập trình viên C#.

Hãy dùng bảng này như một cuốn sổ tay hội thoại. Cột cuối dẫn đến bài học trên trang này giải thích phía Rust.

Java Rust Tìm hiểu thêm
class có field và phương thức struct cho dữ liệu + một khối impl cho các phương thức Struct
Constructor và new Một hàm bình thường, theo quy ước tên là new, trả về Self Struct
interface trait Trait
Phương thức default trong interface Phương thức mặc định trong trait (hoạt động y như vậy) Trait
abstract class, extends, super Không có kế thừa. Dùng trait có phương thức mặc định cho hành vi dùng chung, và composition (struct nằm trong struct) cho dữ liệu dùng chung Trait, Mẫu thiết kế
List<Shape> chứa nhiều class khác nhau Vec<Box<dyn Shape>> (một trait object) Trait object
sealed interface + các record + switch có pattern (Java 17/21) Một enum có các biến thể (variant) mang dữ liệu, cộng với match Enum, match
enum có field và constructor enum mà mỗi biến thể có thể giữ dữ liệu khác nhau Enum
record Point(int x, int y) #[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } Struct, Các trait thông dụng
toString(), equals(), hashCode(), compareTo() Display / Debug, PartialEq / Eq, Hash, PartialOrd / Ord (thường dùng derive) Các trait thông dụng
== (cùng object) và .equals() (cùng giá trị) == luôn so sánh giá trị. Không có cái bẫy “so sánh tham chiếu” Toán tử
null và Optional<T> Option<T>: hoặc Some(value) hoặc None Option
orElse, orElseGet, orElseThrow, map, flatMap, ifPresent unwrap_or, unwrap_or_else, ok_or / expect, map, and_then, if let Some(...) Option, If let / while let
Checked exception và throws Result<T, E> trong kiểu trả về, cộng với toán tử ? Result, Lỗi tự định nghĩa
Unchecked exception cho bug thật sự (IllegalStateException) panic! (dừng luồng hiện tại) Panic
Nối chuỗi exception (new ConfigException(msg, cause)) map_err, hoặc #[source] của thiserror và .context(...) của anyhow anyhow và thiserror
try-with-resources và AutoCloseable Drop, tự động chạy khi chủ sở hữu ra khỏi phạm vi Phạm vi, Con trỏ thông minh
Bộ thu gom rác Quyền sở hữu và mượn, được kiểm tra lúc biên dịch Quyền sở hữu, Mượn
Biến cục bộ final let thông thường (mặc định bất biến); viết let mut để cho phép thay đổi Biến
Hằng số static final const Biến
Field static có thể thay đổi, singleton static với LazyLock hoặc OnceLock, bọc trong Mutex nếu cần thay đổi Trạng thái dùng chung
Generic với <T extends Comparable<T>> Generic với ràng buộc trait (trait bound): T: Ord hoặc where T: Ord Generic
Type erasure, List<Integer> thay vì List<int> Monomorphization: code generic được biên dịch riêng cho từng kiểu, và Vec<i32> lưu số thuần Generic, Generic nâng cao
Lambda, Function<T, R>, Supplier<T>, Consumer<T> Closure, được mô tả bằng các trait Fn, FnMut, FnOnce Closure, Fn, FnMut và FnOnce
Stream: filter, map, reduce, collect Iterator adapter: filter, map, fold, sum, collect Iterator
Iterable<T> và Iterator<T> IntoIterator và Iterator (một phương thức: next, trả về một Option) Iterator tự định nghĩa
parallelStream() par_iter() từ crate rayon Lập trình đồng thời
Thread, ExecutorService std::thread::spawn và std::thread::scope; rayon hoặc tokio cho pool Lập trình đồng thời
synchronized, ReentrantLock Mutex<T>, bọc chính dữ liệu mà nó bảo vệ Trạng thái dùng chung
AtomicInteger, AtomicLong AtomicI32, AtomicI64, AtomicUsize Trạng thái dùng chung
BlockingQueue giữa các luồng Channel (std::sync::mpsc) Channel
CompletableFuture, virtual thread async fn và .await, được chạy bởi một runtime như tokio Async/Await
String (bất biến) và StringBuilder &str (một góc nhìn mượn vào văn bản) và String (sở hữu, có thể nới rộng) Chuỗi
char (một đơn vị UTF-16) char (bất kỳ ký tự Unicode nào, 4 byte); chuỗi là UTF-8 Chuỗi
int, long, double và Integer đóng hộp i32, i64, f64, cùng các kiểu không dấu từ u8 tới u128. Không bao giờ đóng hộp Kiểu dữ liệu
Tràn số nguyên lặng lẽ Panic trong bản build debug; dùng checked_add, wrapping_add, saturating_add để chọn cách xử lý Tràn số nguyên
Ép kiểu như (int) x as cho số, From / TryFrom cho chuyển đổi an toàn Ép kiểu, Chuyển đổi
BigDecimal, BigInteger Các crate rust_decimal và num-bigint Kiểu dữ liệu
Package và import Module (mod) và use Module
public, private, protected, package-private Mặc định là private; pub và pub(crate). Không có protected Module
Một JAR / artifact Maven Một crate (một thư viện hoặc một file binary) Cargo và crate
Annotation (@Override, @Data của Lombok) Attribute như #[derive(Debug)] và #[test] Macro
Reflection, xử lý annotation lúc chạy Gần như không có lúc chạy. Thay vào đó macro sinh code lúc biên dịch Procedural macro
Nạp chồng phương thức (overloading) Không có overloading. Dùng tên khác nhau, hoặc một tham số generic Hàm
Kiểm tra instanceof và downcast match trên một enum (có downcast bằng Any, nhưng hiếm khi dùng) So khớp mẫu
JNI, Foreign Function & Memory API Hàm extern "C" FFI (gọi C)

Java Collections Framework ánh xạ khá gọn sang std::collections. Khác biệt lớn nhất: collection của Rust không bao giờ trả về null. Một lần tra cứu sẽ cho bạn một Option.

Java Rust Tìm hiểu thêm
ArrayList<T> Vec<T> Vector
int[], String[] [T; N] khi biết độ dài lúc biên dịch, nếu không thì Vec<T>. Truyền cả hai dưới dạng slice &[T] Mảng, Slice
List.of(...) (không thể sửa) Một mảng, hoặc một Vec mà bạn chỉ đưa ra dưới dạng &Vec<T> / &[T] (không ai thay đổi được nó qua một tham chiếu dùng chung) Mượn
HashMap<K, V> HashMap<K, V> (get trả về Option<&V>) HashMap
map.merge(word, 1, Integer::sum) *map.entry(word).or_insert(0) += 1 HashMap
TreeMap<K, V> BTreeMap<K, V> (khóa luôn được sắp xếp) HashMap
LinkedHashMap<K, V> (giữ thứ tự thêm vào) IndexMap từ crate indexmap
HashSet<T>, TreeSet<T> HashSet<T>, BTreeSet<T> HashSet và BTreeMap
ArrayDeque<T> VecDeque<T> VecDeque và BinaryHeap
Stack<T>, hoặc Deque dùng làm ngăn xếp Vec<T> với push / pop Vector
PriorityQueue<T> (nhỏ nhất ra trước) BinaryHeap<T> (lớn nhất ra trước; bọc trong Reverse để nhỏ nhất ra trước) VecDeque và BinaryHeap
LinkedList<T> LinkedList<T> có tồn tại, nhưng Vec hoặc VecDeque gần như luôn nhanh hơn VecDeque và BinaryHeap
ConcurrentHashMap<K, V> Mutex<HashMap<K, V>>, hoặc crate dashmap Trạng thái dùng chung
list.sort(Comparator.comparing(...)) v.sort_by_key(...) hoặc v.sort_by(...) Sắp xếp và tìm kiếm
list.removeIf(...) v.retain(...) (giữ lại các phần tử khớp điều kiện) Vector

Giống HashMap của Java, HashMap của Rust không có thứ tự cố định, và thứ tự có thể thay đổi giữa các lần chạy. Nếu bạn cần kết quả được sắp xếp, hãy dùng BTreeMap hoặc sắp xếp các khóa trước.

Phần lớn lập trình viên Java mê Cargo rất nhanh. Chỉ có một công cụ, một định dạng cấu hình, và không có XML cho plugin. Build, phụ thuộc, test, tài liệu, định dạng code và lint đều đến từ cùng một chỗ.

Java Rust Ghi chú
Maven / Gradle cargo Build, chạy, test, thêm phụ thuộc, publish. Xem Cargo và crate
pom.xml / build.gradle.kts Cargo.toml Ngắn gọn, và dự án thông thường không cần build script
Khóa phụ thuộc của Gradle Cargo.lock Được tạo và cập nhật tự động cho bạn
Build nhiều module Một Cargo workspace Một Cargo.toml ở thư mục gốc liệt kê các crate thành viên
Maven Central crates.io Tài liệu API của mọi crate đã publish đều có trên docs.rs (tiếng Anh)
Thêm một <dependency> cargo add serde
mvn archetype:generate / gradle init cargo new my_app (cargo new --lib cho thư viện)
mvn compile / mvn test / mvn package cargo build / cargo test / cargo build --release Bản build debug chạy chậm hơn nhiều; hãy đo hiệu năng bằng bản release
java -jar app.jar ./target/release/app Một file binary native duy nhất. Không cần cài JVM trên server
Phiên bản JDK, SDKMAN!, toolchain rustup và một file rust-toolchain.toml Xem Công cụ và cài đặt
javac rustc Cargo gọi nó giúp bạn; bạn hiếm khi tự chạy nó
JShell Rust Playground (tiếng Anh) ngay trong trình duyệt, hoặc evcxr_repl
Checkstyle, SpotBugs, PMD, Error Prone cargo clippy Hàng trăm lint chính thức
google-java-format, Spotless cargo fmt Một phong cách chuẩn duy nhất; không ai cãi nhau về dấu ngoặc nhọn nữa
IntelliJ IDEA, Eclipse JetBrains RustRover, hoặc VS Code + rust-analyzer rust-analyzer cho bạn gợi ý code, nhảy tới định nghĩa và báo lỗi ngay trong editor
Javadoc Doc comment /// viết bằng Markdown + cargo doc Ví dụ code trong tài liệu được chạy như test. Xem Tài liệu
JUnit 5 #[test] có sẵn và cargo test (cargo nextest là trình chạy nhanh hơn được nhiều người dùng) Xem Kiểm thử
@ParameterizedTest rstest
Mockito mockall, hoặc tự viết bản giả (fake) triển khai trait của bạn Xem Kiểm thử nâng cao
AssertJ / Hamcrest assert!, assert_eq!, và pretty_assertions cho diff dễ đọc
WireMock, Testcontainers wiremock, testcontainers Cùng ý tưởng, phiên bản Rust
JaCoCo cargo llvm-cov hoặc cargo tarpaulin
JMH criterion Xem Hiệu năng
JFR, async-profiler, VisualVM cargo flamegraph (cài bằng cargo install flamegraph), perf Không có GC hay JIT nào phải tinh chỉnh
GraalVM native-image Không cần Mọi bản build Rust vốn đã là code native

Đây là một Cargo.toml nhỏ. Hãy so sánh nó với pom.xml mà bạn cần cho cùng ba phụ thuộc:

[package]
name = "orders-api"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Thư viện và framework: “Vậy thay vì … thì dùng gì?”

Phần tiêu đề “Thư viện và framework: “Vậy thay vì … thì dùng gì?””

Đây là câu hỏi đầu tiên mà nhóm Java nào cũng hỏi: “Bọn mình dùng Spring Boot và Hibernate. Trong Rust có gì?” Rust không có Spring, không có một framework duy nhất mang theo mọi thứ. Thay vào đó bạn kết hợp vài crate chuyên một việc. Dưới đây là những thứ tương đương gần nhất.

Java Rust Ghi chú
Spring Boot (Spring MVC / WebFlux) axum (chúng ta xây một cái trong Dự án: Web API), actix-web, Rocket axum và actix-web được dùng rộng rãi trong production
@RestController + @GetMapping Route của axum trỏ tới các handler async fn bình thường Không có annotation hay quét classpath; bạn nối route bằng code
Servlet filter, interceptor của Spring Các layer của tower và crate tower-http (CORS, nén, tracing, timeout) axum dùng trực tiếp tower
Dependency injection của Spring (@Autowired) State dùng chung trong axum, constructor thông thường và trait Xem dòng DI bên dưới
springdoc-openapi utoipa
Thymeleaf askama (template được kiểm tra lúc biên dịch) hoặc tera
Spring WebSocket WebSocket trong axum (feature ws), hoặc tokio-tungstenite
Spring for GraphQL async-graphql
gRPC Java tonic (kèm prost cho protobuf)
java.net.http.HttpClient, OkHttp, WebClient reqwest Mặc định bất đồng bộ, có client blocking tùy chọn
Netty tokio + hyper
Spring Security (JWT, băm mật khẩu) jsonwebtoken, argon2 Không có framework bảo mật tất cả trong một
Java Rust Ghi chú
Hibernate / JPA / Spring Data JPA SeaORM: ORM bất đồng bộ với entity, quan hệ và migration Cảm giác gần JPA nhất; xây dựng trên sqlx
Hibernate, nếu bạn coi trọng kiểu chặt chẽ Diesel: ORM và query builder; truy vấn được kiểm tra kiểu lúc biên dịch Trưởng thành (2.x), mặc định là đồng bộ (có diesel-async)
JDBC, JdbcTemplate, jOOQ, MyBatis sqlx: SQL thuần, và macro query! kiểm tra nó với cơ sở dữ liệu thật của bạn ngay lúc biên dịch Bất đồng bộ, rất phổ biến, không phải ORM
Flyway / Liquibase sqlx migrate (sqlx-cli), diesel migration, hoặc refinery
Jedis / Lettuce (Redis) redis
Kafka client, Spring Kafka rdkafka (xây dựng trên librdkafka)
RabbitMQ Java client lapin
AWS SDK for Java AWS SDK chính thức cho Rust: aws-config, aws-sdk-s3, …

Nếu bạn thích entity của JPA, hãy thử SeaORM trước. Nếu bạn thích tự viết SQL (hội jOOQ và MyBatis), hãy thử sqlx. Nếu bạn muốn đảm bảo mạnh nhất lúc biên dịch, hãy thử Diesel. Bài Cơ sở dữ liệu trình bày cả ba với code chạy được.

Java Rust Ghi chú
Jackson / Gson serde + serde_json Tiêu chuẩn chung. Còn làm được TOML, YAML và nhiều định dạng khác. Xem JSON với Serde
JAXB, Jackson XML quick-xml (với feature serialize cho serde)
SLF4J + Logback / Log4j 2 tracing (+ tracing-subscriber), hoặc log + env_logger Xem Ghi log
Lombok Các derive có sẵn (Debug, Clone, PartialEq, Default, …); bon hoặc derive_builder cho builder Không có mẹo bytecode; tất cả đều là macro bình thường
MapStruct Các triển khai From / Into Viết bằng tay, được trình biên dịch kiểm tra. Xem Chuyển đổi
IoC container của Spring Thường là không cần gì: truyền phụ thuộc vào constructor, dùng trait cho những thứ bạn muốn thay thế khi test Dự án Rust hiếm khi dùng DI container
Bean Validation (@NotNull, @Size) validator hoặc garde (kiểm tra dữ liệu dựa trên derive) Không cần @NotNull: chỉ dùng Option ở nơi giá trị có thể bị thiếu
Resilience4j backon, hoặc middleware retry và timeout của tower
Caffeine (cache) moka
Guava, Apache Commons Chủ yếu là thư viện chuẩn, cộng thêm itertools
java.time chrono hoặc jiff Xem Ngày và giờ
picocli clap Xem Dự án: công cụ CLI với clap
@Scheduled, Quartz tokio-cron-scheduler
Micrometer metrics, hoặc opentelemetry
Jakarta Mail lettre
Java Rust Ghi chú
JavaFX / Swing Tauri (giao diện web + backend Rust), Slint, egui, Iced Tauri 2 và Slint 1.x ổn định nhất. Xem Ứng dụng giao diện (GUI)
LibGDX, jMonkeyEngine Bevy, macroquad Bevy chưa tới 1.0 và thay đổi API giữa các phiên bản. Xem Phát triển game
Code ứng dụng Android Giữ Kotlin hoặc Java cho giao diện; đặt logic dùng chung vào một thư viện Rust build bằng Android NDK uniffi của Mozilla sinh binding cho Kotlin
Gọi code native từ Java Xuất các hàm extern "C" và gọi chúng qua FFM API (JDK 22+), hoặc viết hàm JNI bằng crate jni Xem FFI (gọi C)

Đặt cạnh nhau: abstract class trở thành trait

Phần tiêu đề “Đặt cạnh nhau: abstract class trở thành trait”

Đây là một cây kế thừa Java kinh điển: một base class trừu tượng có một phương thức trừu tượng và một phương thức dùng chung.

abstract class Employee {
protected final String name;
Employee(String name) { this.name = name; }
abstract int monthlyPay();
String payslip() { return name + " nhận " + monthlyPay() + " mỗi tháng"; }
}
class Salaried extends Employee {
private final int yearly;
Salaried(String name, int yearly) { super(name); this.yearly = yearly; }
@Override int monthlyPay() { return yearly / 12; }
}
class Contractor extends Employee {
private final int hours, rate;
Contractor(String name, int hours, int rate) { super(name); this.hours = hours; this.rate = rate; }
@Override int monthlyPay() { return hours * rate; }
}
List<Employee> staff = List.of(new Salaried("Ada", 60000), new Contractor("Linus", 120, 50));
for (Employee e : staff) System.out.println(e.payslip());

Rust không có extends. Phương thức dùng chung chuyển vào một trait dưới dạng phương thức mặc định, còn mỗi struct giữ dữ liệu riêng của mình:

trait Employee {
fn name(&self) -> &str;
fn monthly_pay(&self) -> u32;
// Một phương thức mặc định: giống payslip() cụ thể trong abstract class.
fn payslip(&self) -> String {
format!("{} nhận {} mỗi tháng", self.name(), self.monthly_pay())
}
}
struct Salaried {
name: String,
yearly: u32,
}
struct Contractor {
name: String,
hours: u32,
rate: u32,
}
impl Employee for Salaried {
fn name(&self) -> &str {
&self.name
}
fn monthly_pay(&self) -> u32 {
self.yearly / 12
}
}
impl Employee for Contractor {
fn name(&self) -> &str {
&self.name
}
fn monthly_pay(&self) -> u32 {
self.hours * self.rate
}
}
fn main() {
// Box<dyn Employee> đóng vai trò “một tham chiếu tới một Employee nào đó”.
let staff: Vec<Box<dyn Employee>> = vec![
Box::new(Salaried { name: "Ada".to_string(), yearly: 60_000 }),
Box::new(Contractor { name: "Linus".to_string(), hours: 120, rate: 50 }),
];
for e in &staff {
println!("{}", e.payslip());
}
}

Kết quả:

Ada nhận 5000 mỗi tháng
Linus nhận 6000 mỗi tháng
▶ Tự chạy thử

Trait không thể chứa field, nên protected name trở thành một phương thức name() nhỏ mà mỗi struct tự triển khai. Lúc đầu có vẻ phải gõ thêm, nhưng nó loại bỏ cả một nhóm vấn đề: không có base class mong manh, không có lời gọi super để quên, và không có cây kế thừa sâu hun hút phải cuộn qua. Nếu bạn biết phương thức default trên interface của Java 8, bạn đã biết cách Rust chia sẻ hành vi. Và khi tập hợp các kiểu là cố định (như một sealed interface trong Java 17), một enum với match thường đơn giản hơn một trait.

Optional của Java là một bước tiến lớn, nhưng nó chỉ được dùng cho giá trị trả về, và map.get() hay field nào cũng vẫn có thể là null:

static Optional<String> findEmail(int userId) {
return userId == 1 ? Optional.of("[email protected]") : Optional.empty();
}
for (int id : new int[] {1, 2}) {
System.out.println(findEmail(id).map(String::toUpperCase).orElse("không có email trong hồ sơ"));
}
findEmail(1).ifPresent(email -> System.out.println("đang gửi tới " + email));

Option của Rust có API gần như giống hệt, nhưng được dùng ở mọi nơi một giá trị có thể bị thiếu, kể cả khi tra cứu collection. Và không có null nào ẩn phía sau nó:

fn find_email(user_id: u32) -> Option<String> {
if user_id == 1 {
Some("[email protected]".to_string())
} else {
None
}
}
fn main() {
for id in [1, 2] {
// map + orElseGet
let shown = find_email(id)
.map(|email| email.to_uppercase())
.unwrap_or_else(|| "không có email trong hồ sơ".to_string());
println!("{shown}");
}
// ifPresent
if let Some(email) = find_email(1) {
println!("đang gửi tới {email}");
}
// flatMap được gọi là and_then
let domain = find_email(1).and_then(|email| email.split_once('@').map(|(_, d)| d.to_string()));
println!("{domain:?}");
}

Kết quả:

không có email trong hồ sơ
đang gửi tới [email protected]
Some("example.com")
▶ Tự chạy thử

Trình biên dịch không cho bạn gọi .to_uppercase() trên một Option<String> cho tới khi bạn đã xử lý None, nên NullPointerException đơn giản là không tồn tại. Option cũng không tốn thêm gì: một Option<&T> có cùng kích thước với một tham chiếu bình thường. Hãy học các phương thức hỗ trợ ở trang Option.

Đặt cạnh nhau: checked exception trở thành Result

Phần tiêu đề “Đặt cạnh nhau: checked exception trở thành Result”

Checked exception có một ý tưởng hay ở cốt lõi: chữ ký hàm cho bạn biết điều gì có thể sai. Đây là một phiên bản Java điển hình:

class InsufficientFundsException extends Exception {
InsufficientFundsException(int needed) { super("cần thêm " + needed); }
}
static int withdraw(int balance, int amount) throws InsufficientFundsException {
if (amount > balance) throw new InsufficientFundsException(amount - balance);
return balance - amount;
}
static int pay(int balance, int amount) throws InsufficientFundsException {
int left = withdraw(balance, amount);
System.out.println("đã trả " + amount + ", còn lại " + left);
return left;
}
try { pay(100, 80); pay(50, 80); }
catch (InsufficientFundsException e) { System.out.println("lỗi: " + e.getMessage()); }

Rust giữ lại ý tưởng hay đó và bỏ phần còn lại. Lỗi là một phần của kiểu trả về (Result<u32, BankError>), và ? làm việc mà throws làm: “nếu bước này thất bại, chuyển lỗi lên cho nơi gọi tôi”. Lỗi thường là một enum, nên một kiểu có thể liệt kê mọi cách mà một thao tác có thể thất bại:

use std::fmt;
#[derive(Debug)]
enum BankError {
InvalidAmount,
InsufficientFunds { needed: u32 },
}
// Display đóng vai trò của getMessage().
impl fmt::Display for BankError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BankError::InvalidAmount => write!(f, "số tiền phải lớn hơn 0"),
BankError::InsufficientFunds { needed } => write!(f, "cần thêm {needed}"),
}
}
}
fn withdraw(balance: u32, amount: u32) -> Result<u32, BankError> {
if amount == 0 {
return Err(BankError::InvalidAmount);
}
if amount > balance {
return Err(BankError::InsufficientFunds { needed: amount - balance });
}
Ok(balance - amount)
}
fn pay(balance: u32, amount: u32) -> Result<u32, BankError> {
let left = withdraw(balance, amount)?; // giống gọi một phương thức có “throws”
println!("đã trả {amount}, còn lại {left}");
Ok(left)
}
fn main() {
for (balance, amount) in [(100, 80), (50, 80), (100, 0)] {
if let Err(e) = pay(balance, amount) {
println!("lỗi: {e}");
}
}
}

Kết quả:

đã trả 80, còn lại 20
lỗi: cần thêm 30
lỗi: số tiền phải lớn hơn 0
▶ Tự chạy thử

Hãy để ý những gì đã biến mất: không có khối try, không có stack unwinding, không có exception bị bọc trong RuntimeException chỉ để lọt qua được một lambda. Lỗi là giá trị bình thường, nên chúng hoạt động tốt bên trong closure và chuỗi iterator. Dấu ? là một dấu hiệu nhìn thấy được ở mọi chỗ mà hàm có thể trả về sớm. Tự viết Display bằng tay sẽ nhanh chán; crate thiserror sinh nó cho bạn, còn anyhow cho ứng dụng một kiểu lỗi “hứng tất cả” (xem anyhow và thiserror). Với bug thật sự, loại bạn sẽ báo bằng một unchecked exception, Rust có panic!.

Một stream pipeline có collector groupingBy:

record Order(String customer, String city, int total) {}
List<Order> orders = List.of(
new Order("Ada", "London", 120), new Order("Grace", "New York", 80),
new Order("Linus", "Helsinki", 200), new Order("Ken", "New York", 150));
List<String> big = orders.stream()
.filter(o -> o.total() > 100)
.sorted(Comparator.comparingInt(Order::total).reversed())
.map(Order::customer)
.toList();
System.out.println(String.join(", ", big));
Map<String, Integer> byCity = orders.stream()
.collect(Collectors.groupingBy(Order::city, TreeMap::new, Collectors.summingInt(Order::total)));
byCity.forEach((city, sum) -> System.out.println(city + ": " + sum));

Trong Rust, .iter() đóng vai trò của .stream(), và các adapter mang những cái tên quen thuộc:

use std::cmp::Reverse;
use std::collections::BTreeMap;
struct Order {
customer: String,
city: String,
total: u32,
}
fn order(customer: &str, city: &str, total: u32) -> Order {
Order { customer: customer.to_string(), city: city.to_string(), total }
}
fn main() {
let orders = vec![
order("Ada", "London", 120),
order("Grace", "New York", 80),
order("Linus", "Helsinki", 200),
order("Ken", "New York", 150),
];
// filter + toList
let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect();
// sorted(...reversed()): việc sắp xếp diễn ra trên Vec, không phải bên trong iterator.
big.sort_by_key(|o| Reverse(o.total));
// map
let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect();
println!("{}", names.join(", "));
// groupingBy(city, TreeMap::new, summingInt(total))
let mut by_city: BTreeMap<&str, u32> = BTreeMap::new();
for o in &orders {
*by_city.entry(o.city.as_str()).or_insert(0) += o.total;
}
for (city, sum) in &by_city {
println!("{city}: {sum}");
}
}

Kết quả:

Linus, Ken, Ada
Helsinki: 200
London: 120
New York: 230
▶ Tự chạy thử

Giống stream, iterator của Rust là lười (lazy) và chỉ chạy khi có thứ gì đó tiêu thụ chúng (collect, sum, một vòng lặp for). Khác với stream, không có class Collectors: collect() nhìn vào kiểu bạn yêu cầu (Vec, HashMap, String, …) và tạo ra nó. Để nhóm dữ liệu, câu trả lời thường gặp là một vòng lặp đơn giản với entry API; crate itertools bổ sung các hàm trợ giúp nếu bạn nhớ groupingBy. Và chuỗi iterator được biên dịch thành cùng mã máy như một vòng lặp viết tay, không phải đóng hộp int nào dọc đường. Đọc thêm ở Iterator và Sắp xếp và tìm kiếm.

Đặt cạnh nhau: type erasure và monomorphization

Phần tiêu đề “Đặt cạnh nhau: type erasure và monomorphization”

Generic của Java chỉ tồn tại lúc biên dịch. Lúc chạy, T bị xóa đi, nên bạn không thể viết new T(), và bạn không thể đặt một int vào List mà không đóng hộp nó:

static <T> List<T> filled(int n) {
List<T> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(new T()); // không biên dịch: T đã bị xóa
return list;
}
List<int> ids; // cũng không biên dịch: bạn cần List<Integer>

Rust biên dịch một bản sao riêng của mỗi hàm generic cho từng kiểu mà bạn dùng với nó (gọi là monomorphization). Vì vậy hàm biết chính xác T là gì, và có thể gọi T::default():

use std::mem::size_of;
// “T phải có giá trị mặc định và clone được”
fn filled<T: Default + Clone>(n: usize) -> Vec<T> {
vec![T::default(); n]
}
fn main() {
let ids: Vec<i32> = filled(3);
let names: Vec<String> = filled(2);
let flags: Vec<bool> = filled(2);
println!("{ids:?} {names:?} {flags:?}");
// Không đóng hộp: một Vec<i32> lưu các số 4 byte thuần nằm cạnh nhau.
println!("một i32 chiếm {} byte", size_of::<i32>());
println!("một bool chiếm {} byte", size_of::<bool>());
}

Kết quả:

[0, 0, 0] ["", ""] [false, false]
một i32 chiếm 4 byte
một bool chiếm 1 byte
▶ Tự chạy thử

Cái giá phải trả: nhiều bản sao code hơn đồng nghĩa với binary lớn hơn và thời gian biên dịch lâu hơn, nhưng mọi lời gọi đều trực tiếp và có thể được inline. Khi bạn muốn một bản code dùng chung (như cách Java làm), hãy dùng một trait object như Box<dyn Trait>. Các ràng buộc sau dấu hai chấm (T: Default + Clone) là phiên bản Rust của <T extends ...>. Xem Generic và Generic nâng cao.

Đặt cạnh nhau: synchronized trở thành Arc và Mutex

Phần tiêu đề “Đặt cạnh nhau: synchronized trở thành Arc và Mutex”

Bốn luồng cùng tăng một bộ đếm dùng chung. Trong Java, khóa nằm trên object, và việc nhớ viết synchronized là trách nhiệm của bạn:

class Counter {
private int count = 0;
synchronized void increment() { count++; }
synchronized int get() { return count; }
}
Counter counter = new Counter();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 4; i++) {
Thread t = new Thread(() -> { for (int j = 0; j < 1000; j++) counter.increment(); });
t.start();
threads.add(t);
}
for (Thread t : threads) t.join();
System.out.println("count = " + counter.get());

Trong Rust, Mutex sở hữu con số. Không có cách nào chạm tới nó mà không khóa trước. Arc (một con trỏ đếm tham chiếu kiểu atomic) cho phép nhiều luồng dùng chung mutex:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
// Mỗi luồng có handle riêng trỏ tới cùng một Mutex.
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() trả về một guard; khóa được nhả khi guard bị drop.
*counter.lock().unwrap() += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("count = {}", *counter.lock().unwrap());
}

Kết quả:

count = 4000
▶ Tự chạy thử

Đây là phần mà lập trình viên Java rất thích: nếu bạn quên synchronized trong Java, code vẫn biên dịch và thỉnh thoảng cho ra số sai. Trong Rust, nếu bạn cố dùng chung một i32 hay Vec bình thường giữa các luồng rồi thay đổi nó, chương trình không biên dịch. Data race bị hệ thống kiểu bắt lại (thông qua các trait Send và Sync). Với một bộ đếm đơn lẻ, AtomicUsize sẽ nhẹ hơn mutex, giống AtomicInteger. Xem Lập trình đồng thời và Trạng thái dùng chung.

Những chỗ dễ vấp với lập trình viên Java

Phần tiêu đề “Những chỗ dễ vấp với lập trình viên Java”

Lập trình viên Java nào cũng vấp phải cùng những ổ gà này. Biết trước sẽ khiến chúng nhỏ đi.

  • Truyền một giá trị vào hàm là move nó. Trong Java, truyền một object là truyền một tham chiếu, và bạn vẫn dùng biến của mình tiếp được. Trong Rust, truyền một String hay Vec theo giá trị sẽ chuyển (move) nó vào hàm:

    fn greet(name: String) {
    println!("Hello, {name}!");
    }
    fn main() {
    let name = String::from("Ada");
    greet(name);
    greet(name);
    }
    error[E0382]: use of moved value: `name`
    --> src/main.rs:8:11
    |
    6 | let name = String::from("Ada");
    | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
    7 | greet(name);
    | ---- value moved here
    8 | greet(name);
    | ^^^^ value used here after move

    Trình biên dịch báo rằng name đã bị move vào lần gọi greet đầu tiên ở dòng 7, nên dòng 8 không được dùng nó nữa. Cách sửa thường gặp là để hàm mượn: fn greet(name: &str) và gọi greet(&name). Hãy đọc Quyền sở hữu trước; bài đó giải thích quy tắc này, còn trang E0382 nói chi tiết về lỗi này.

  • Borrow checker. Tại một thời điểm, bạn có thể có nhiều người đọc (&T) hoặc một người ghi (&mut T), không bao giờ cả hai. Những đồ thị object bình thường trong Java, như object con trỏ ngược về cha, hay nhiều object cùng giữ và thay đổi một object dùng chung, cần đến con trỏ thông minh hoặc một thiết kế khác. Vài tuần đầu có thể khá bực bội. Rồi mọi thứ sẽ dễ hơn.

  • Không có kế thừa, không có protected, không có super. Bạn sẽ muốn dựng lại cây kế thừa class bằng trait. Hãy thử enum và composition trước; chúng thường hợp hơn.

  • == so sánh giá trị. Lần này là tin tốt: a == b với hai String sẽ so sánh văn bản, nên bug kinh điển == với .equals() không thể xảy ra. Với struct của riêng bạn, == chỉ dùng được sau khi bạn derive hoặc triển khai PartialEq.

  • BinaryHeap lấy ra phần tử lớn nhất. PriorityQueue của Java cho bạn phần tử nhỏ nhất trước. Trong Rust, hãy bọc phần tử trong std::cmp::Reverse để có hành vi min-heap. Xem VecDeque và BinaryHeap.

  • Tràn số nguyên không lặng lẽ. Integer.MAX_VALUE + 1 âm thầm quay vòng trong Java. Trong bản build debug của Rust, nó gây panic. Hãy nói rõ ý bạn bằng checked_add, wrapping_add hoặc saturating_add. Xem Tràn số nguyên.

  • String và &str, và không có charAt. Chuỗi Rust là UTF-8, nên s[0] không biên dịch. Dùng s.chars() để duyệt qua các ký tự. Hàm thường nhận &str; struct thường lưu String. Xem Chuỗi.

  • Không có nạp chồng phương thức. Bạn không thể có hai phương thức print với tham số khác nhau. Hãy dùng tên khác nhau (print, print_with_color), hoặc một tham số generic.

  • Không có phép màu lúc chạy. Không có quét classpath, không có @Autowired, không có proxy âm thầm bọc transaction quanh phương thức của bạn. Mọi thứ đều được nối rõ ràng bằng code, và macro chạy lúc biên dịch. Gõ nhiều hơn một chút, nhưng bạn luôn thấy được chuyện gì đang xảy ra.

  • Không có null và không có exception. Lúc đầu, Option và Result ở khắp nơi có vẻ như thêm việc. Vài tuần sau bạn sẽ nhận ra mình chưa hề gặp lại NullPointerException hay một stack trace bất ngờ nào. Gọi .unwrap() khi đang học thì không sao, nhưng trong code thật thì nó là phiên bản Rust của “cứ để nó sập”.

  • Thời gian biên dịch. Một bản build sạch kéo về và biên dịch mọi phụ thuộc từ mã nguồn, nên chậm hơn javac. Build tăng dần (incremental) nhanh hơn nhiều, và cargo check (bỏ qua bước sinh mã) là lệnh bạn sẽ chạy nhiều nhất trong lúc sửa code. Tin tốt ở đầu bên kia: chương trình khởi động tức thì, không phải chờ JVM khởi động.

  • Async cần một runtime. Không có virtual thread. Một async fn không làm gì cả cho tới khi có executor chạy nó, nên dự án web thêm tokio và đặt #[tokio::main] lên main. Xem Async/Await.

Bạn không cần đọc trang này theo thứ tự. Lộ trình dưới đây bỏ qua những gì Java đã dạy bạn và dành thời gian cho chỗ Rust khác biệt:

  1. Bắt đầu với Rust và Công cụ và cài đặt: cài Rust, chọn một editor, chạy cargo new.
  2. Biến và Kiểu dữ liệu: đọc lướt, chủ yếu để biết let mut và các kiểu số.
  3. Quyền sở hữu, rồi Mượn, rồi Slice: những trang quan trọng nhất với bạn. Cứ từ từ.
  4. Chuỗi: String và &str.
  5. Struct và Enum, rồi match và So khớp mẫu.
  6. Option và Result, rồi Lỗi tự định nghĩa và anyhow và thiserror.
  7. Trait, Generic, Các trait thông dụng và Trait object: interface và abstract class của bạn.
  8. Closure và Iterator: lambda và stream của bạn.
  9. Vector, HashMap và VecDeque và BinaryHeap.
  10. Module, Cargo và crate và Kiểm thử.
  11. Lifetime và Con trỏ thông minh.
  12. Lập trình đồng thời, Trạng thái dùng chung, Channel và Async/Await.
  13. JSON với Serde, Ghi log và Cơ sở dữ liệu, rồi xây dự án Web API.
  14. Muốn tăng tốc một ứng dụng Java có sẵn? Hãy đọc FFI (gọi C) để xem Rust xuất các hàm extern "C" như thế nào.

Cả hai bài tập đều cho bạn một đoạn Java. Hãy chuyển nó sang Rust.

1. Chuyển một record và một stream pipeline

Phần tiêu đề “1. Chuyển một record và một stream pipeline”

Đoạn code này in ra những học sinh đã đậu, có ai đạt điểm tuyệt đối không, và điểm cao nhất. Gợi ý: iterator có any (giống anyMatch) và max (trả về một Option, giống OptionalInt). Thay cho Collectors.joining, hãy collect vào một Vec<&str> rồi gọi .join(", ").

record Student(String name, int score) {}
List<Student> students = List.of(
new Student("Ada", 91), new Student("Brian", 42),
new Student("Grace", 100), new Student("Ken", 67));
String passed = students.stream()
.filter(s -> s.score() >= 50)
.map(Student::name)
.collect(Collectors.joining(", "));
boolean perfect = students.stream().anyMatch(s -> s.score() == 100);
int best = students.stream().mapToInt(Student::score).max().orElse(0);
System.out.println("đậu: " + passed);
System.out.println("có điểm tuyệt đối: " + perfect);
System.out.println("cao nhất: " + best);
Xem lời giải
#[derive(Debug, Clone, PartialEq)]
struct Student {
name: String,
score: u32,
}
fn student(name: &str, score: u32) -> Student {
Student { name: name.to_string(), score }
}
fn main() {
let students = vec![
student("Ada", 91),
student("Brian", 42),
student("Grace", 100),
student("Ken", 67),
];
let passed: Vec<&str> = students
.iter()
.filter(|s| s.score >= 50)
.map(|s| s.name.as_str())
.collect();
let perfect = students.iter().any(|s| s.score == 100);
let best = students.iter().map(|s| s.score).max().unwrap_or(0);
println!("đậu: {}", passed.join(", "));
println!("có điểm tuyệt đối: {perfect}");
println!("cao nhất: {best}");
}

Kết quả:

đậu: Ada, Grace, Ken
có điểm tuyệt đối: true
cao nhất: 100
▶ Tự chạy thử

Các derive cho bạn những gì một record của Java có sẵn miễn phí: chuỗi debug ({:?}), sao chép (.clone()) và so sánh theo giá trị (==). Hàm trợ giúp student đóng vai trò constructor của record. Khác với stream của Java, bạn có thể gọi students.iter() bao nhiêu lần tùy thích, vì mỗi lần gọi tạo ra một iterator mới trên cùng một Vec.

2. Chuyển một checked exception có nguyên nhân

Phần tiêu đề “2. Chuyển một checked exception có nguyên nhân”

Phương thức này đọc một dòng port=<số> từ file cấu hình. Nó ném một checked exception ConfigException, và bọc NumberFormatException mà nó nhận được từ Integer.parseInt. Hãy chuyển nó sang Rust sao cho read_port trả về Result<u16, String>. Gợi ý: line.split_once('=') trả về một Option<(&str, &str)>; biến nó thành Result bằng .ok_or_else(...). .parse::<u16>() trả về một Result, và .map_err(...) thay lỗi của nó bằng thông báo của riêng bạn, giống như bọc một exception trong một exception khác.

class ConfigException extends Exception {
ConfigException(String message) { super(message); }
}
static int readPort(String line) throws ConfigException {
String[] parts = line.split("=", 2);
if (parts.length != 2 || !parts[0].trim().equals("port"))
throw new ConfigException("cần port=<số>, nhận được '" + line + "'");
try {
return Integer.parseInt(parts[1].trim());
} catch (NumberFormatException e) {
throw new ConfigException("số cổng không hợp lệ: " + parts[1].trim());
}
}
for (String line : List.of("port = 8080", "port=tám mươi", "host=localhost")) {
try { System.out.println("cổng là " + readPort(line)); }
catch (ConfigException e) { System.out.println("lỗi cấu hình: " + e.getMessage()); }
}
Xem lời giải
fn read_port(line: &str) -> Result<u16, String> {
let expected = || format!("cần port=<số>, nhận được '{line}'");
let (key, value) = line.split_once('=').ok_or_else(expected)?;
if key.trim() != "port" {
return Err(expected());
}
let value = value.trim();
value
.parse::<u16>()
.map_err(|_| format!("số cổng không hợp lệ: {value}"))
}
fn main() {
for line in ["port = 8080", "port=tám mươi", "host=localhost"] {
match read_port(line) {
Ok(port) => println!("cổng là {port}"),
Err(e) => println!("lỗi cấu hình: {e}"),
}
}
}

Kết quả:

cổng là 8080
lỗi cấu hình: số cổng không hợp lệ: tám mươi
lỗi cấu hình: cần port=<số>, nhận được 'host=localhost'
▶ Tự chạy thử

Mệnh đề throws ConfigException trở thành kiểu trả về Result, khối try/catch quanh parseInt trở thành một map_err duy nhất, và throw trở thành Err(...). Dùng u16 cho cổng là một điểm cộng nhỏ: một số lớn hơn 65535 bị chính parse từ chối, điều mà int trong Java không làm. Closure nhỏ expected chỉ tạo thông báo lỗi khi cần, giống một supplier trong orElseThrow(() -> ...).

  • Rất nhiều thứ trong Java mang sang được: generic, interface (dưới dạng trait), lambda (dưới dạng closure) và stream pipeline (dưới dạng iterator).
  • Những thay đổi lớn là quyền sở hữu thay cho bộ thu gom rác, Option thay cho null, Result và ? thay cho exception, và trait cộng composition thay cho kế thừa.
  • Generic được biên dịch riêng cho từng kiểu (monomorphization), nên không có type erasure và không phải đóng hộp số.
  • Dữ liệu dùng chung giữa các luồng nằm bên trong một Mutex (bọc trong Arc), và quên khóa là lỗi biên dịch, chứ không phải một data race.
  • Cargo thay thế Maven hoặc Gradle; clippy và rustfmt thay thế Checkstyle, SpotBugs và các công cụ định dạng; kiểm thử có sẵn trong ngôn ngữ.
  • Thay Spring Boot hãy xem axum hoặc actix-web, thay JPA hãy xem SeaORM hoặc Diesel, thay JDBC và jOOQ hãy xem sqlx, và thay Jackson hãy dùng serde.

Kiểm tra nhanh

1. Một abstract class trong Java có một phương thức trừu tượng và một phương thức cụ thể gọi tới nó. Thứ thay thế thường dùng trong Rust là gì?

2. PriorityQueue của Java trả về phần tử nhỏ nhất trước. Còn BinaryHeap::pop trong Rust trả về gì?

3. Vì sao một hàm generic trong Rust gọi được T::default(), còn Java không viết được new T()?

4. Bạn dùng chung một Vec bình thường giữa các luồng và cố push vào nó mà không có Mutex. Điều gì xảy ra trong Rust?

Điểm: 0 / 4

  • The Rust Programming Language (tiếng Anh): cuốn sách chính thức. Chương 4 (ownership), 9 (xử lý lỗi), 10 (generic và trait) và 16 (lập trình đồng thời) quan trọng nhất với lập trình viên Java.
  • std::collections (tiếng Anh): hướng dẫn của chính thư viện chuẩn về cách chọn giữa Vec, VecDeque, HashMap, BTreeMap và các collection khác.
  • JEP 454: Foreign Function & Memory API (tiếng Anh): tính năng của JDK 22 để gọi thư viện native, kể cả thư viện Rust, từ Java mà không cần viết code JNI.
  • Crate jni trên docs.rs (tiếng Anh): binding Rust cho JNI, nếu bạn cần gọi Rust từ các JDK cũ hơn hoặc gọi ngược lại vào Java.
  • Are We Web Yet? (tiếng Anh): tổng quan của cộng đồng về các web framework, thư viện cơ sở dữ liệu và nhiều thứ khác của Rust, nếu bạn đang tìm thứ thay cho bộ công nghệ Spring Boot.