这份文档不是 Rust 语法速查表,而是“Java 开发者应该怎样重新想问题”的映射表。
Rust 不鼓励你把 Java 的类层级、异常流和共享可变状态原封不动搬过来。你需要换成另一套默认值:
- 先设计类型,再写逻辑
- 先设计所有权,再谈共享
- 先设计错误,再谈异常处理
- 先设计边界,再谈性能
| Java 直觉 | Rust 对应 | 需要调整的思路 |
|---|---|---|
class + getter/setter |
struct + impl |
不要默认生成一堆访问器,先问这个字段是否真的应该被修改 |
interface |
trait |
trait 描述能力,不是继承树 |
try/catch |
Result + ? |
错误是值,不是“用来跳出流程的魔法” |
null |
Option |
缺失必须显式建模 |
final 字段 |
默认不可变绑定 | Rust 默认是“不可变”,不是“先可变再约束” |
CompletableFuture |
async fn + runtime |
async 不是线程,运行时也不是语言本身 |
Stream |
iterator | 组合数据处理,而不是把数据先变成共享可变集合 |
synchronized/锁 |
Mutex / RwLock |
共享可变状态要小心控制作用域 |
| 包/模块访问控制 | crate/module 可见性 | 边界比层级更重要 |
| 构造函数里抛异常 | 返回 Result<T, E> |
构造阶段就验证不变量 |
task_domain::Task 不是“一个随便装字段的对象”,而是一个已经通过校验的值:
pub fn new(title: impl Into<String>) -> Result<Self, TaskError>这个签名传达了三件事:
- 调用者可以传入可转成
String的值 - 构造过程可能失败
- 成功后返回的是已经校验过的
Task
TaskId 包装了 Uuid。这能避免把“任务 ID”与其他字符串或 UUID 混用。
对 Java 开发者来说,最重要的变化是:不要把一切都用 String 代替。
labs/java-to-rust/examples/errors.rs 里的模式是:
fn describe_creation(title: &str) -> Result<String, TaskError> {
let task = Task::new(title)?;
Ok(format!("created {} ({})", task.title(), task.id()))
}这段代码告诉你:
- 成功路径和失败路径是同等公民
?不是偷懒,而是把错误传递变成显式语义- 你应该在函数签名里暴露失败可能性
在 Java 里,很多对象默认是“到处引用”。Rust 里,值默认只有一个所有者。
如果你需要读,不要先复制,先借用:
fn title_length(title: &str) -> usize {
title.chars().count()
}如果你需要接管并修改,再把所有权拿过来:
fn normalize(mut title: String) -> String {
title.make_ascii_lowercase();
title
}labs/java-to-rust/src/lib.rs 里的 map_scores 体现的是“借用输入,返回新集合”:
pub fn map_scores(scores: &[u32], operation: impl Fn(u32) -> u32) -> Vec<u32> {
scores.iter().copied().map(operation).collect()
}这比“先修改原集合再继续传递”更符合 Rust 的默认方向。
- 把
clone()当成默认答案 - 把
Arc<Mutex<T>>当成“共享状态万能解” - 把
unwrap()当成“先跑起来再说” - 把 trait 当成接口继承树
- 把 async 当成“多线程自动加速”
- 先问这个值是谁拥有
- 再问这个值能不能借用
- 再问这个错误应该如何返回
- 最后才问能不能共享、能不能并发、能不能缓存
如果你习惯了这套顺序,Rust 会开始变得清晰。反过来硬套 Java 的默认模型,只会越写越别扭。