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#.
Khái niệm ngôn ngữ: từ Java sang Rust
Phần tiêu đề “Khái niệm ngôn ngữ: từ Java sang Rust”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) |
Collection
Phần tiêu đề “Collection”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.
Công cụ: từ Maven và Gradle sang Cargo
Phần tiêu đề “Công cụ: từ Maven và Gradle sang Cargo”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.
Web và mạng
Phần tiêu đề “Web và mạng”| 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 |
Dữ liệu và hàng đợi tin nhắn
Phần tiêu đề “Dữ liệu và hàng đợi tin nhắn”| 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.
Những viên gạch dùng hằng ngày
Phần tiêu đề “Những viên gạch dùng hằng ngày”| 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 |
Desktop, game, Android và tương tác
Phần tiêu đề “Desktop, game, Android và tương tác”| 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ángLinus nhận 6000 mỗi thángTrait 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.
Đặt cạnh nhau: Optional trở thành Option
Phần tiêu đề “Đặt cạnh nhau: Optional trở thành Option”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) {}
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 { } 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")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 20lỗi: cần thêm 30lỗi: số tiền phải lớn hơn 0Hã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!.
Đặt cạnh nhau: stream trở thành iterator
Phần tiêu đề “Đặt cạnh nhau: stream trở thành iterator”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, AdaHelsinki: 200London: 120New York: 230Giố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 bytemột bool chiếm 1 byteCá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Đâ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
StringhayVectheo 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` trait7 | greet(name);| ---- value moved here8 | greet(name);| ^^^^ value used here after moveTrình biên dịch báo rằng
nameđã bị move vào lần gọigreetđầ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ọigreet(&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 == bvới haiStringsẽ 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 khaiPartialEq. -
BinaryHeaplấy ra phần tử lớn nhất.PriorityQueuecủa Java cho bạn phần tử nhỏ nhất trước. Trong Rust, hãy bọc phần tử trongstd::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ằngchecked_add,wrapping_addhoặcsaturating_add. Xem Tràn số nguyên. -
Stringvà&str, và không cócharAt. Chuỗi Rust là UTF-8, nêns[0]không biên dịch. Dùngs.chars()để duyệt qua các ký tự. Hàm thường nhận&str; struct thường lưuString. 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
printvớ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ó
nullvà không có exception. Lúc đầu,Optionvà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ạiNullPointerExceptionhay 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 fnkhô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ênmain. Xem Async/Await.
Lộ trình học cho lập trình viên Java
Phần tiêu đề “Lộ trình học cho lập trình viên Java”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:
- 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. - Biến và Kiểu dữ liệu: đọc lướt, chủ yếu để biết
let mutvà các kiểu số. - 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ừ.
- Chuỗi:
Stringvà&str. - Struct và Enum, rồi match và So khớp mẫu.
- Option và Result, rồi Lỗi tự định nghĩa và anyhow và thiserror.
- Trait, Generic, Các trait thông dụng và Trait object: interface và abstract class của bạn.
- Closure và Iterator: lambda và stream của bạn.
- Vector, HashMap và VecDeque và BinaryHeap.
- Module, Cargo và crate và Kiểm thử.
- Lifetime và Con trỏ thông minh.
- Lập trình đồng thời, Trạng thái dùng chung, Channel và Async/Await.
- JSON với Serde, Ghi log và Cơ sở dữ liệu, rồi xây dự án Web API.
- 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.
Luyện tập
Phần tiêu đề “Luyện tập”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, Kencó điểm tuyệt đối: truecao nhất: 100Cá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à 8080lỗi cấu hình: số cổng không hợp lệ: tám mươilỗi cấu hình: cần port=<số>, nhận được 'host=localhost'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(() -> ...).
Tóm tắt
Phần tiêu đề “Tóm tắt”- 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,
Optionthay chonull,Resultvà?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 trongArc), 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
Phần tiêu đề “Kiểm tra nhanh”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
Đọc thêm
Phần tiêu đề “Đọc thêm”- 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,BTreeMapvà 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.