面向 Java 开发者的 Rust:实用指南
如果你和 Java 打了很多年交道,学 Rust 需要的东西你大多已经有了。你习惯用类型思考,你懂泛型、接口、lambda 和 Stream 流水线。这些 Rust 全都有:trait 很像带默认方法的接口,迭代器读起来就像 Stream,Cargo 干的是 Maven 或 Gradle 的活,只是配置少得多。
新东西是以前 JVM 替你包办的那一切。Rust 没有垃圾回收器,也没有虚拟机。你的代码会编译成原生二进制文件,什么时候释放内存,由编译器在程序运行之前检查所有权和借用来决定。Rust 也没有 null(用的是 Option)、没有异常(用的是 Result),也没有类继承。你的大部分学习时间都会花在这四点上。
Java 开发者为什么会学 Rust?通常是因为下面某个原因:服务需要又低又稳的内存占用,而且不能有 GC 停顿;命令行工具和 Serverless 函数要秒启动,不想带一个 JVM;Java 应用里有一条热点路径想搬到原生代码里(Java 可以通过 JNI 或较新的 Foreign Function & Memory API 调用 Rust);或者你只是好奇,Rust 为什么总能在“最受推崇的语言”调查里拿第一。这一页把你在 Java 里学到的东西一一对应到 Rust,好让你专心对付真正不一样的部分。
如果你想看 Rust 和 C++、Go、Python 的整体对比,请看 Rust 与 C++、Go、Python 对比。如果你也懂 .NET,还有单独的一页:面向 C# 开发者的 Rust。
语言概念:从 Java 到 Rust
Section titled “语言概念:从 Java 到 Rust”把这张表当成一本“短语手册”来用。最后一列链接到本站讲解 Rust 这一边的课程。
| Java | Rust | 延伸学习 |
|---|---|---|
带字段和方法的 class |
用 struct 存数据,再用 impl 块写方法 |
Rust 结构体 |
构造函数和 new |
一个返回 Self 的普通函数,按约定叫 new |
Rust 结构体 |
interface |
trait |
Rust trait |
接口里的 default 方法 |
trait 里的默认方法(用法一样) | Rust trait |
abstract class、extends、super |
没有继承。共享行为用带默认方法的 trait,共享数据用组合(结构体里套结构体) | Rust trait、设计模式 |
装着不同类的 List<Shape> |
Vec<Box<dyn Shape>>(trait 对象) |
trait 对象(dyn) |
sealed interface + record + 模式 switch(Java 17/21) |
变体携带数据的 enum,再加上 match |
Rust 枚举、Rust match |
带字段和构造函数的 enum |
每个变体都能存放不同数据的 enum |
Rust 枚举 |
record Point(int x, int y) |
#[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } |
Rust 结构体、常用 trait |
toString()、equals()、hashCode()、compareTo() |
Display / Debug、PartialEq / Eq、Hash、PartialOrd / Ord(通常用派生) |
常用 trait |
==(同一个对象)与 .equals()(值相同) |
== 永远比较值。不存在“比较的是引用”这个坑 |
Rust 运算符 |
null 和 Optional<T> |
Option<T>:要么是 Some(value),要么是 None |
Rust Option |
orElse、orElseGet、orElseThrow、map、flatMap、ifPresent |
unwrap_or、unwrap_or_else、ok_or / expect、map、and_then、if let Some(...) |
Rust Option、if let / while let |
受检异常和 throws |
返回类型里写 Result<T, E>,再加上 ? 运算符 |
Rust Result、自定义错误类型 |
表示真正 bug 的非受检异常(IllegalStateException) |
panic!(终止当前线程) |
Rust panic |
异常链(new ConfigException(msg, cause)) |
map_err,或者 thiserror 的 #[source] 和 anyhow 的 .context(...) |
anyhow 与 thiserror |
try-with-resources 和 AutoCloseable |
Drop,所有者离开作用域时自动运行 |
Rust 作用域、Rust 智能指针 |
| 垃圾回收器 | 所有权和借用,在编译时检查 | Rust 所有权、Rust 借用 |
final 局部变量 |
普通的 let(默认不可变);要允许修改就写 let mut |
Rust 变量 |
static final 常量 |
const |
Rust 变量 |
可变的 static 字段、单例 |
配合 LazyLock 或 OnceLock 的 static,需要修改时再包一层 Mutex |
共享状态 |
带 <T extends Comparable<T>> 的泛型 |
带 trait 约束的泛型:T: Ord 或 where T: Ord |
Rust 泛型 |
类型擦除,要写 List<Integer> 而不是 List<int> |
单态化:泛型代码会为每种类型各编译一份,Vec<i32> 里存的就是普通数字 |
Rust 泛型、高级泛型 |
lambda、Function<T, R>、Supplier<T>、Consumer<T> |
闭包,由 Fn、FnMut、FnOnce 这几个 trait 描述 |
Rust 闭包、Fn、FnMut 与 FnOnce |
Stream:filter、map、reduce、collect |
迭代器适配器:filter、map、fold、sum、collect |
Rust 迭代器 |
Iterable<T> 和 Iterator<T> |
IntoIterator 和 Iterator(只有一个方法:next,返回一个 Option) |
自定义迭代器 |
parallelStream() |
rayon crate 里的 par_iter() |
Rust 并发 |
Thread、ExecutorService |
std::thread::spawn 和 std::thread::scope;线程池用 rayon 或 tokio |
Rust 并发 |
synchronized、ReentrantLock |
Mutex<T>,它把要保护的数据包在里面 |
共享状态 |
AtomicInteger、AtomicLong |
AtomicI32、AtomicI64、AtomicUsize |
共享状态 |
线程之间的 BlockingQueue |
信道(std::sync::mpsc) |
信道(channel) |
CompletableFuture、虚拟线程 |
async fn 和 .await,由 tokio 这样的运行时来执行 |
async/await |
String(不可变)和 StringBuilder |
&str(借用的文本视图)和 String(拥有所有权、可增长) |
Rust 字符串 |
char(一个 UTF-16 码元) |
char(任意一个 Unicode 字符,4 个字节);字符串是 UTF-8 编码的 |
Rust 字符串 |
int、long、double 和装箱的 Integer |
i32、i64、f64,以及从 u8 到 u128 的无符号类型。从不装箱 |
Rust 数据类型 |
| 整数悄悄溢出 | debug 构建里会 panic;用 checked_add、wrapping_add、saturating_add 自己选择行为 |
Rust 整数溢出 |
(int) x 这样的强制转换 |
数字之间用 as,安全转换用 From / TryFrom |
Rust 类型转换、From 与 Into 转换 |
BigDecimal、BigInteger |
rust_decimal 和 num-bigint 这两个 crate |
Rust 数据类型 |
包和 import |
模块(mod)和 use |
Rust 模块 |
public、private、protected、包级私有 |
默认私有;用 pub 和 pub(crate)。没有 protected |
Rust 模块 |
| JAR / Maven 构件 | crate(库或可执行程序) | Cargo 与 crate |
注解(@Override、Lombok 的 @Data) |
#[derive(Debug)]、#[test] 这样的属性 |
Rust 宏 |
| 反射、运行时注解处理 | 运行时基本没有。改用宏在编译时生成代码 | 过程宏 |
| 方法重载 | 没有重载。用不同的名字,或者用泛型参数 | Rust 函数 |
instanceof 判断和向下转型 |
对枚举做 match(用 Any 向下转型也行,但很少见) |
Rust 模式匹配 |
| JNI、Foreign Function & Memory API | extern "C" 函数 |
FFI(调用 C) |
Java 集合框架可以很整齐地对应到 std::collections。最大的区别是:Rust 的集合从不返回 null,查找操作给你的是一个 Option。
| Java | Rust | 延伸学习 |
|---|---|---|
ArrayList<T> |
Vec<T> |
Rust vector(Vec) |
int[]、String[] |
长度在编译时已知就用 [T; N],否则用 Vec<T>。两者都可以作为 slice &[T] 传递 |
Rust 数组、Rust slice(切片) |
List.of(...)(不可修改) |
数组,或者一个只以 &Vec<T> / &[T] 形式交出去的 Vec(通过共享引用谁都改不了它) |
Rust 借用 |
HashMap<K, V> |
HashMap<K, V>(get 返回 Option<&V>) |
Rust HashMap |
map.merge(word, 1, Integer::sum) |
*map.entry(word).or_insert(0) += 1 |
Rust HashMap |
TreeMap<K, V> |
BTreeMap<K, V>(键始终保持有序) |
Rust HashMap |
LinkedHashMap<K, V>(保持插入顺序) |
indexmap crate 里的 IndexMap |
|
HashSet<T>、TreeSet<T> |
HashSet<T>、BTreeSet<T> |
HashSet 与 BTreeMap |
ArrayDeque<T> |
VecDeque<T> |
VecDeque 与 BinaryHeap |
Stack<T>,或者当栈用的 Deque |
用 push / pop 的 Vec<T> |
Rust vector(Vec) |
PriorityQueue<T>(最小的先出) |
BinaryHeap<T>(最大的先出;包一层 Reverse 就变成最小的先出) |
VecDeque 与 BinaryHeap |
LinkedList<T> |
有 LinkedList<T>,但 Vec 或 VecDeque 几乎总是更快 |
VecDeque 与 BinaryHeap |
ConcurrentHashMap<K, V> |
Mutex<HashMap<K, V>>,或者 dashmap crate |
共享状态 |
list.sort(Comparator.comparing(...)) |
v.sort_by_key(...) 或 v.sort_by(...) |
排序与查找 |
list.removeIf(...) |
v.retain(...)(保留符合条件的元素) |
Rust vector(Vec) |
和 Java 的 HashMap 一样,Rust 的 HashMap 没有固定顺序,而且每次运行的顺序都可能不同。需要有序输出,就用 BTreeMap,或者先给键排序。
工具链:从 Maven 和 Gradle 到 Cargo
Section titled “工具链:从 Maven 和 Gradle 到 Cargo”大多数 Java 开发者很快就会爱上 Cargo。只有一个工具、一种配置格式,没有插件 XML。构建、依赖、测试、文档、格式化和 lint 全都出自同一个地方。
| Java | Rust | 说明 |
|---|---|---|
| Maven / Gradle | cargo |
构建、运行、测试、添加依赖、发布。见 Cargo 与 crate |
pom.xml / build.gradle.kts |
Cargo.toml |
很短,普通项目不需要构建脚本 |
| Gradle 依赖锁定 | Cargo.lock |
自动生成和更新 |
| 多模块构建 | Cargo 工作空间 | 顶层的一个 Cargo.toml 列出所有成员 crate |
| Maven Central | crates.io | 每个已发布 crate 的 API 文档都在 docs.rs(英文)上 |
添加一个 <dependency> |
cargo add serde |
|
mvn archetype:generate / gradle init |
cargo new my_app(库用 cargo new --lib) |
|
mvn compile / mvn test / mvn package |
cargo build / cargo test / cargo build --release |
debug 构建运行起来慢得多;测性能要用 release |
java -jar app.jar |
./target/release/app |
单个原生二进制文件。服务器上不用装 JVM |
| JDK 版本、SDKMAN!、toolchain | rustup 和一个 rust-toolchain.toml 文件 |
见 Rust 工具与环境配置 |
javac |
rustc |
Cargo 会替你调用它;你很少需要自己运行 |
| JShell | 浏览器里的 Rust Playground,或者 evcxr_repl |
|
| Checkstyle、SpotBugs、PMD、Error Prone | cargo clippy |
数百条官方 lint |
| google-java-format、Spotless | cargo fmt |
一套标准风格;没人再为大括号吵架 |
| IntelliJ IDEA、Eclipse | JetBrains RustRover,或者 VS Code + rust-analyzer | rust-analyzer 提供补全、跳转到定义和行内报错 |
| Javadoc | 用 Markdown 写的 /// 文档注释加 cargo doc |
文档里的代码示例会作为测试运行。见 Rust 文档 |
| JUnit 5 | 内置的 #[test] 和 cargo test(cargo nextest 是一个流行的、更快的测试运行器) |
见 Rust 测试 |
@ParameterizedTest |
rstest |
|
| Mockito | mockall,或者手写一个实现了你的 trait 的假对象 |
见 测试进阶 |
| AssertJ / Hamcrest | assert!、assert_eq!,以及让差异更易读的 pretty_assertions |
|
| WireMock、Testcontainers | wiremock、testcontainers |
思路相同,Rust 版本 |
| JaCoCo | cargo llvm-cov 或 cargo tarpaulin |
|
| JMH | criterion |
见 性能优化 |
| JFR、async-profiler、VisualVM | cargo flamegraph(用 cargo install flamegraph 安装)、perf |
没有 GC 或 JIT 需要调优 |
| GraalVM native-image | 不需要 | 每次 Rust 构建产出的本来就是原生代码 |
下面是一个小小的 Cargo.toml。拿它和同样三个依赖所需的 pom.xml 比一比:
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }库和框架:“那我们用什么来代替……?”
Section titled “库和框架:“那我们用什么来代替……?””这是每个 Java 团队最先问的问题:“我们用的是 Spring Boot 和 Hibernate,Rust 里对应的是什么?”Rust 没有 Spring,没有一个大包大揽的框架。你要把几个各管一摊的 crate 组合起来用。下面是最接近的替代品。
Web 与网络
Section titled “Web 与网络”| Java | Rust | 说明 |
|---|---|---|
| Spring Boot(Spring MVC / WebFlux) | axum(我们在项目:用 Axum 写 Web API 里做了一个)、actix-web、Rocket | axum 和 actix-web 在生产环境中用得很广 |
@RestController + @GetMapping |
axum 路由指向普通的 async fn 处理函数 |
没有注解,也没有类路径扫描;路由在代码里接好 |
| Servlet 过滤器、Spring 拦截器 | tower layer 和 tower-http crate(CORS、压缩、追踪、超时) |
axum 直接使用 tower |
Spring 依赖注入(@Autowired) |
axum 里共享的 State、普通的构造函数和 trait |
见下面依赖注入那一行 |
| springdoc-openapi | utoipa |
|
| Thymeleaf | askama(模板在编译时检查)或 tera |
|
| Spring WebSocket | axum 里的 WebSocket(ws feature),或者 tokio-tungstenite |
|
| Spring for GraphQL | async-graphql |
|
| gRPC Java | tonic(protobuf 用 prost) |
|
java.net.http.HttpClient、OkHttp、WebClient |
reqwest |
默认异步,也可以选用阻塞式客户端 |
| Netty | tokio + hyper |
|
| Spring Security(JWT、密码哈希) | jsonwebtoken、argon2 |
没有一站式的安全框架 |
| Java | Rust | 说明 |
|---|---|---|
| Hibernate / JPA / Spring Data JPA | SeaORM:异步 ORM,支持实体、关系和迁移 | 感觉最接近 JPA;构建在 sqlx 之上 |
| 看重严格类型的 Hibernate 用法 | Diesel:ORM 和查询构建器;查询在编译时做类型检查 | 成熟(2.x),默认是同步的(另有 diesel-async) |
JDBC、JdbcTemplate、jOOQ、MyBatis |
sqlx:直接写 SQL,query! 宏会在编译时拿你的真实数据库来检查它 |
异步,非常流行,不是 ORM |
| Flyway / Liquibase | sqlx migrate(sqlx-cli)、diesel migration 或 refinery |
|
| Jedis / Lettuce(Redis) | redis |
|
| Kafka 客户端、Spring Kafka | rdkafka(基于 librdkafka) |
|
| RabbitMQ Java 客户端 | lapin |
|
| AWS SDK for Java | 官方的 AWS SDK for Rust:aws-config、aws-sdk-s3…… |
如果你喜欢 JPA 的实体,先试试 SeaORM。如果你更喜欢自己写 SQL(jOOQ 和 MyBatis 那一派),就试试 sqlx。如果你想要最强的编译时保证,就试试 Diesel。数据库这一页用能跑的代码演示了这三个库。
日常基础组件
Section titled “日常基础组件”| Java | Rust | 说明 |
|---|---|---|
| Jackson / Gson | serde + serde_json |
事实标准,还支持 TOML、YAML 等格式。见 用 Serde 处理 JSON |
| JAXB、Jackson XML | quick-xml(打开它的 serialize feature 就能配合 serde) |
|
| SLF4J + Logback / Log4j 2 | tracing(+ tracing-subscriber),或 log + env_logger |
见 日志 |
| Lombok | 内置的派生(Debug、Clone、PartialEq、Default……);builder 用 bon 或 derive_builder |
没有字节码黑魔法,全是普通的宏 |
| MapStruct | From / Into 实现 |
手写,由编译器检查。见 From 与 Into 转换 |
| Spring 的 IoC 容器 | 通常什么都不用:把依赖传进构造函数,需要在测试里替换的东西就用 trait | Rust 项目很少用 DI 容器 |
Bean Validation(@NotNull、@Size) |
validator 或 garde(基于派生的校验) |
不需要 @NotNull:只在值可能缺失的地方用 Option |
| Resilience4j | backon,或者 tower 的重试和超时中间件 |
|
| Caffeine(缓存) | moka |
|
| Guava、Apache Commons | 大多用标准库就够了,再加上 itertools |
|
java.time |
chrono 或 jiff |
见 日期与时间 |
| picocli | clap |
见 项目:用 clap 写命令行工具 |
@Scheduled、Quartz |
tokio-cron-scheduler |
|
| Micrometer | metrics 或 opentelemetry |
|
| Jakarta Mail | lettre |
桌面、游戏、Android 与互操作
Section titled “桌面、游戏、Android 与互操作”| Java | Rust | 说明 |
|---|---|---|
| JavaFX / Swing | Tauri(Web 界面 + Rust 后端)、Slint、egui、Iced |
Tauri 2 和 Slint 1.x 最稳定。见 GUI 应用 |
| LibGDX、jMonkeyEngine | Bevy、macroquad |
Bevy 还没到 1.0,每次发布都会改 API。见 游戏开发 |
| Android 应用代码 | 界面继续用 Kotlin 或 Java;共享的逻辑放进一个用 Android NDK 构建的 Rust 库 | Mozilla 的 uniffi 能生成 Kotlin 绑定 |
| 从 Java 调用原生代码 | 导出 extern "C" 函数,再通过 FFM API(JDK 22+)调用它们;或者用 jni crate 写 JNI 函数 |
见 FFI(调用 C) |
对照看:抽象类变成 trait
Section titled “对照看:抽象类变成 trait”下面是一个经典的 Java 继承结构:一个抽象基类,有一个抽象方法和一个共享方法。
abstract class Employee { protected final String name; Employee(String name) { this.name = name; }
abstract int monthlyPay();
String payslip() { return name + " 每月收入 " + monthlyPay(); }}
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 没有 extends。共享的方法搬进一个 trait,成为默认方法,每个结构体各自保管自己的数据:
trait Employee { fn name(&self) -> &str; fn monthly_pay(&self) -> u32;
// 默认方法:相当于抽象类里那个具体的 payslip()。 fn payslip(&self) -> String { format!("{} 每月收入 {}", 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> 扮演的就是“某个 Employee 的引用”这个角色。 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()); }}输出:
Ada 每月收入 5000Linus 每月收入 6000trait 不能有字段,所以 protected name 变成了每个结构体都要实现的一个小方法 name()。一开始会觉得要多打几个字,但它消除了一整类问题:没有脆弱的基类,没有会被忘掉的 super 调用,也没有要翻来翻去的深层继承结构。如果你懂 Java 8 接口里的 default 方法,你就已经知道 Rust 是怎么共享行为的了。另外,当类型的集合是固定的(就像 Java 17 的 sealed interface),用枚举加 match 往往比 trait 更简单。
对照看:Optional 变成 Option
Section titled “对照看:Optional 变成 Option”Java 的 Optional 是一大进步,但它只用在返回值上,map.get() 或任何字段仍然可能是 null:
static Optional<String> findEmail(int userId) {}
for (int id : new int[] {1, 2}) { System.out.println(findEmail(id).map(String::toUpperCase).orElse("没有登记邮箱"));}findEmail(1).ifPresent(email -> System.out.println("发送给 " + email));Rust 的 Option 有几乎一样的 API,但凡是值可能缺失的地方,处处都用它,包括集合的查找。而且它背后没有藏着 null:
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(|| "没有登记邮箱".to_string()); println!("{shown}"); }
// ifPresent if let Some(email) = find_email(1) { println!("发送给 {email}"); }
// flatMap 在这里叫 and_then let domain = find_email(1).and_then(|email| email.split_once('@').map(|(_, d)| d.to_string())); println!("{domain:?}");}输出:
没有登记邮箱Some("example.com")在处理好 None 之前,编译器不让你在 Option<String> 上调用 .to_uppercase(),所以 NullPointerException 根本不存在。Option 也没有额外开销:Option<&T> 和一个普通引用一样大。辅助方法可以在 Rust Option 这一页学。
对照看:受检异常变成 Result
Section titled “对照看:受检异常变成 Result”受检异常的初衷是好的:看签名就知道可能出什么错。下面是一个典型的 Java 写法:
class InsufficientFundsException extends Exception { InsufficientFundsException(int needed) { super("还差 " + 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("已支付 " + amount + ",剩余 " + left); return left;}
try { pay(100, 80); pay(50, 80); }catch (InsufficientFundsException e) { System.out.println("错误:" + e.getMessage()); }Rust 留下了这个好点子,扔掉了其余的部分。错误是返回类型的一部分(Result<u32, BankError>),? 做的就是 throws 做的事:“如果这一步失败了,就把错误交给我的调用者”。错误通常是一个 enum,这样一个类型就能列出某个操作失败的所有方式:
use std::fmt;
#[derive(Debug)]enum BankError { InvalidAmount, InsufficientFunds { needed: u32 },}
// Display 扮演的就是 getMessage() 的角色。impl fmt::Display for BankError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { BankError::InvalidAmount => write!(f, "金额必须大于零"), BankError::InsufficientFunds { needed } => write!(f, "还差 {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)?; // 就像调用一个会“抛异常”的方法 println!("已支付 {amount},剩余 {left}"); Ok(left)}
fn main() { for (balance, amount) in [(100, 80), (50, 80), (100, 0)] { if let Err(e) = pay(balance, amount) { println!("错误:{e}"); } }}输出:
已支付 80,剩余 20错误:还差 30错误:金额必须大于零注意少了什么:没有 try 块,没有栈展开,也不用为了让异常穿过 lambda 而把它包进 RuntimeException。错误就是普通的值,所以在闭包和迭代器链里也能正常使用。函数每个可能提前返回的地方,都有一个看得见的 ? 标记。手写 Display 很快就会让人烦;thiserror crate 能帮你生成它,anyhow 则给应用程序提供一个兜底的错误类型(见 anyhow 与 thiserror)。至于真正的 bug,也就是你在 Java 里会用非受检异常表示的那种,Rust 有 panic!。
对照看:Stream 变成迭代器
Section titled “对照看:Stream 变成迭代器”一个带 groupingBy 收集器的 Stream 流水线:
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));在 Rust 里,.iter() 扮演 .stream() 的角色,适配器的名字也都很眼熟:
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()):排序是在 Vec 上做的,而不是在迭代器里。 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}"); }}输出:
Linus, Ken, AdaHelsinki: 200London: 120New York: 230城市名特意保留了英文:BTreeMap 按键的字母顺序排列,用英文才能看清这个效果。
和 Stream 一样,Rust 的迭代器是惰性的,只有在被消费时(collect、sum、for 循环)才会运行。和 Stream 不同的是,Rust 没有 Collectors 类:collect() 会看你要的是什么类型(Vec、HashMap、String……),然后把它构建出来。分组通常就用一个普通循环加 entry API;如果你想念 groupingBy,itertools crate 提供了一些辅助方法。而且迭代器链会被编译成和手写循环一样的机器码,中间也不会把 int 装箱。更多内容请看 Rust 迭代器和排序与查找。
对照看:类型擦除与单态化
Section titled “对照看:类型擦除与单态化”Java 的泛型只存在于编译时。到了运行时 T 就被擦除了,所以你没法写 new T(),也没法不装箱就把 int 放进 List:
static <T> List<T> filled(int n) { List<T> list = new ArrayList<>(); for (int i = 0; i < n; i++) list.add(new T()); // 编译不过:T 被擦除了 return list;}
List<int> ids; // 这也编译不过:你得写 List<Integer>Rust 会为你使用的每一种类型,单独编译一份泛型函数(这叫作单态化,monomorphization)。所以函数清楚地知道 T 是什么,可以调用 T::default():
use std::mem::size_of;
// “T 必须有默认值,而且可以克隆”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:?}");
// 不装箱:Vec<i32> 里存的就是一个挨一个的 4 字节数字。 println!("一个 i32 占 {} 个字节", size_of::<i32>()); println!("一个 bool 占 {} 个字节", size_of::<bool>());}输出:
[0, 0, 0] ["", ""] [false, false]一个 i32 占 4 个字节一个 bool 占 1 个字节代价是:代码的副本多了,二进制文件会更大,编译时间也更长;但每次调用都是直接调用,还能被内联。当你想要只有一份共享的代码时(就像 Java 那样),就用 Box<dyn Trait> 这样的 trait 对象。冒号后面的约束(T: Default + Clone)就是 Rust 版的 <T extends ...>。见 Rust 泛型和高级泛型。
对照看:synchronized 变成 Arc 和 Mutex
Section titled “对照看:synchronized 变成 Arc 和 Mutex”四个线程给一个共享计数器加一。在 Java 里,锁挂在对象上,记不记得写 synchronized 全靠你自己:
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("计数:" + counter.get());在 Rust 里,数字归 Mutex 所有。不先加锁,就没有任何办法碰到它。Arc(原子引用计数指针)让多个线程可以共享这个互斥器:
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 { // 每个线程都拿到一个指向同一个 Mutex 的句柄。 let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { for _ in 0..1000 { // lock() 返回一个守卫;守卫被丢弃时,锁就会释放。 *counter.lock().unwrap() += 1; } })); }
for handle in handles { handle.join().unwrap(); }
println!("计数:{}", *counter.lock().unwrap());}输出:
计数:4000这是 Java 开发者最喜欢的部分:在 Java 里忘了写 synchronized,代码照样能编译,只是偶尔给出错误的数字。在 Rust 里,如果你想在线程之间共享并修改一个普通的 i32 或 Vec,程序根本编译不过。数据竞争由类型系统(通过 Send 和 Sync 这两个 trait)抓出来。如果只是一个计数器,AtomicUsize 会比互斥器更轻量,就像 AtomicInteger。见 Rust 并发和共享状态。
Java 开发者容易踩的坑
Section titled “Java 开发者容易踩的坑”每个 Java 开发者都会遇到同样的几个坎。提前知道,坎就会小一些。
-
把值传给函数会移动它。 在 Java 里,传对象传的是引用,之后你还能继续用这个变量。在 Rust 里,按值传递
String或Vec会把它移动进函数:fn greet(name: String) {println!("你好,{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 move编译器说的是:
name的值在第 7 行已经移动进了greet,第 8 行又被使用了。通常的解决办法是让函数借用:写成fn greet(name: &str),调用时写greet(&name)。请先读 Rust 所有权,那一页讲的就是这条规则;E0382 这一页详细讲解了这个错误。 -
借用检查器。 同一时间,你可以有多个读者(
&T),或者一个写者(&mut T),但不能两者兼有。在 Java 里很常见的对象图,比如子对象指回它的父对象,或者很多对象一起持有并修改同一个共享对象,在 Rust 里需要用智能指针或者换一种设计。头几周可能会有点受挫。刚开始觉得别扭很正常,之后真的会轻松很多。 -
没有继承,没有
protected,没有super。 你会忍不住想用 trait 重建类继承结构。先试试枚举和组合;它们通常更合适。 -
==比较的是值。 难得的好消息:对两个String写a == b,比较的是文本,所以经典的==与.equals()的 bug 不可能发生。对你自己的结构体,要先派生或实现PartialEq,==才能用。 -
BinaryHeap弹出的是最大的元素。 Java 的PriorityQueue先给你最小的。在 Rust 里,把元素包进std::cmp::Reverse就能得到小顶堆的效果。见 VecDeque 与 BinaryHeap。 -
整数溢出不会悄无声息。 在 Java 里,
Integer.MAX_VALUE + 1会默默回绕。在 Rust 的 debug 构建里,它会 panic。用checked_add、wrapping_add或saturating_add把你的意图写清楚。见 Rust 整数溢出。 -
String与&str,以及没有charAt。 Rust 的字符串是 UTF-8 编码的,所以s[0]编译不过。用s.chars()逐个遍历字符。函数参数通常用&str,结构体字段通常存String。见 Rust 字符串。 -
没有方法重载。 你不能有两个参数不同的
print方法。用不同的名字(print、print_with_color),或者用泛型参数。 -
没有运行时魔法。 没有类路径扫描,没有
@Autowired,也没有在你的方法外面套上事务的代理。一切都在代码里显式地接好,宏在编译时运行。要多打一些字,但你永远看得清发生了什么。 -
没有
null,也没有异常。 一开始,到处都是Option和Result会让人觉得多费功夫。过几周你就会发现,自己再也没见过NullPointerException或者突如其来的堆栈跟踪。学习阶段调用.unwrap()没问题,但在真实代码里,它就相当于 Rust 版的“崩了就崩了吧”。 -
编译时间。 从零开始构建会从源码拉取并编译所有依赖,所以比
javac慢。增量构建快得多,而且改代码时你最常运行的是cargo check(它跳过代码生成)。另一头也有好消息:程序秒启动,没有 JVM 预热。 -
async 需要运行时。 Rust 没有虚拟线程。
async fn在执行器运行它之前什么都不会做,所以 Web 项目要加上 tokio,并在main上标注#[tokio::main]。见 async/await。
Java 开发者的学习路线
Section titled “Java 开发者的学习路线”你不必按顺序读完整个网站。下面这条路线会跳过 Java 已经教会你的东西,把时间花在 Rust 不一样的地方:
- Rust 快速上手和 Rust 工具与环境配置:安装 Rust,选一个编辑器,运行
cargo new。 - Rust 变量和 Rust 数据类型:快速浏览,主要是学会
let mut和各种数字类型。 - Rust 所有权,然后是 Rust 借用,再然后是 Rust slice(切片):对你来说最重要的几页,慢慢来。
- Rust 字符串:
String与&str。 - Rust 结构体和 Rust 枚举,然后是 Rust match 和 Rust 模式匹配。
- Rust Option 和 Rust Result,然后是自定义错误类型和 anyhow 与 thiserror。
- Rust trait、Rust 泛型、常用 trait 和 trait 对象(dyn):你的接口和抽象类。
- Rust 闭包和 Rust 迭代器:你的 lambda 和 Stream。
- Rust vector(Vec)、Rust HashMap 和 VecDeque 与 BinaryHeap。
- Rust 模块、Cargo 与 crate 和 Rust 测试。
- Rust 生命周期和 Rust 智能指针。
- Rust 并发、共享状态、信道(channel)和 async/await。
- 用 Serde 处理 JSON、日志和数据库,然后动手做 Web API 项目。
- 想给现有的 Java 应用提速?读一读 FFI(调用 C),看看 Rust 怎么导出
extern "C"函数。
两道题都会给你一段 Java 代码,请把它移植到 Rust。
1. 移植一个 record 和一条 Stream 流水线
Section titled “1. 移植一个 record 和一条 Stream 流水线”下面这段代码打印及格的学生、有没有人拿到满分,以及最高分。提示:迭代器有 any(相当于 anyMatch)和 max(返回一个 Option,相当于 OptionalInt)。对于 Collectors.joining,先收集成一个 Vec<&str>,再调用 .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("及格:" + passed);System.out.println("有满分吗:" + perfect);System.out.println("最高分:" + best);查看答案
#[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!("及格:{}", passed.join(", ")); println!("有满分吗:{perfect}"); println!("最高分:{best}");}输出:
及格:Ada, Grace, Ken有满分吗:true最高分:100这几个派生给了你 Java record 白送的那些功能:调试字符串({:?})、复制(.clone())和值相等(==)。student 这个辅助函数扮演了 record 构造函数的角色。和 Java 的 Stream 不同,students.iter() 想调用多少次都行,因为每次调用都会在同一个 Vec 上创建一个新的迭代器。
2. 移植一个带原因的受检异常
Section titled “2. 移植一个带原因的受检异常”下面这个方法从配置文件里读取一行 port=<number>。它会抛出受检的 ConfigException,并把 Integer.parseInt 抛出的 NumberFormatException 包装起来。请把它移植过来,让 read_port 返回 Result<u16, String>。提示:line.split_once('=') 返回一个 Option<(&str, &str)>;用 .ok_or_else(...) 把它变成 Result。.parse::<u16>() 返回一个 Result,.map_err(...) 能把它的错误换成你自己的信息,就像把一个异常包进另一个异常。
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("应为 port=<数字>,实际是“" + line + "”"); try { return Integer.parseInt(parts[1].trim()); } catch (NumberFormatException e) { throw new ConfigException("端口号无效:" + parts[1].trim()); }}
for (String line : List.of("port = 8080", "port=八十", "host=localhost")) { try { System.out.println("端口是 " + readPort(line)); } catch (ConfigException e) { System.out.println("配置错误:" + e.getMessage()); }}查看答案
fn read_port(line: &str) -> Result<u16, String> { let expected = || format!("应为 port=<数字>,实际是“{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!("端口号无效:{value}"))}
fn main() { for line in ["port = 8080", "port=八十", "host=localhost"] { match read_port(line) { Ok(port) => println!("端口是 {port}"), Err(e) => println!("配置错误:{e}"), } }}输出:
端口是 8080配置错误:端口号无效:八十配置错误:应为 port=<数字>,实际是“host=localhost”throws ConfigException 子句变成了 Result 返回类型,包在 parseInt 外面的 try/catch 变成了一个 map_err,throw 变成了 Err(...)。端口用 u16 还有个小好处:大于 65535 的数字会直接被 parse 拒绝,Java 的 int 可做不到这一点。那个小小的 expected 闭包只在需要时才构建错误信息,就像 orElseThrow(() -> ...) 里的 supplier。
- Java 的很多东西都能直接带过来:泛型、接口(变成 trait)、lambda(变成闭包)和 Stream 流水线(变成迭代器)。
- 最大的转变是:用所有权代替垃圾回收器,用
Option代替null,用Result和?代替异常,用 trait 加组合代替继承。 - 泛型会为每种类型各编译一份(单态化),所以没有类型擦除,数字也不用装箱。
- 线程之间共享的数据放在
Mutex里(再包一层Arc),忘了加锁是编译错误,而不是数据竞争。 - Cargo 取代了 Maven 或 Gradle;clippy 和 rustfmt 取代了 Checkstyle、SpotBugs 和各种格式化工具;测试是内置的。
- 想找 Spring Boot 就看 axum 或 actix-web,想找 JPA 就看 SeaORM 或 Diesel,想找 JDBC 和 jOOQ 就看 sqlx,Jackson 的位置交给 serde。
小测验
1. 一个 Java 抽象类有一个抽象方法,还有一个调用它的具体方法。在 Rust 里通常用什么代替?
2. Java 的 PriorityQueue 先返回最小的元素。在 Rust 里,BinaryHeap::pop 返回什么?
3. 为什么 Rust 的泛型函数能调用 T::default(),而 Java 却没法写 new T()?
4. 你在线程之间共享一个普通的 Vec,并且不用 Mutex 就往里 push。在 Rust 里会怎样?
得分: 0 / 4
- 《Rust 程序设计语言》简体中文版:官方教程。对 Java 开发者来说,第 4 章(所有权)、第 9 章(错误处理)、第 10 章(泛型和 trait)和第 16 章(并发)最重要。
- std::collections(英文):标准库自己写的指南,教你怎么在
Vec、VecDeque、HashMap、BTreeMap等集合之间做选择。 - JEP 454: Foreign Function & Memory API(英文):JDK 22 的新特性,不写 JNI 代码就能从 Java 调用原生库,包括用 Rust 写的库。
- docs.rs 上的 jni crate(英文):JNI 的 Rust 绑定,适合需要从较老的 JDK 调用 Rust,或者从 Rust 回调 Java 的情况。
- Are We Web Yet?(英文):社区整理的概览,涵盖 Rust 的 Web 框架、数据库库等等,适合正在为自己的 Spring Boot 技术栈找替代品的你。