Rust для разработчиков на Java: практическое руководство
Если ты много лет пишешь на Java, у тебя уже есть почти всё, что нужно, чтобы выучить Rust. Ты мыслишь типами. Ты знаешь обобщения, интерфейсы, лямбды и конвейеры стримов. Всё это есть и в Rust: трейты очень похожи на интерфейсы с методами по умолчанию, итераторы читаются как стримы, а Cargo делает работу Maven или Gradle, только с гораздо меньшим количеством настроек.
Новое — всё то, что раньше за тебя делала JVM. В Rust нет сборщика мусора и нет виртуальной машины. Код компилируется в нативный исполняемый файл, а компилятор решает, когда освобождать память, проверяя владение (ownership) и заимствование (borrowing) до запуска программы. Ещё в Rust нет null (вместо него Option), нет исключений (вместо них Result) и нет наследования классов. Именно на эти четыре изменения уйдёт большая часть времени на обучение.
Зачем разработчики на Java берутся за Rust? Обычно по одной из этих причин: сервисы, которым нужно низкое и стабильное потребление памяти без пауз GC; консольные утилиты и serverless-функции, которые должны запускаться мгновенно, без JVM; горячий участок Java-приложения, который хочется перенести в нативный код (Rust можно вызывать из Java через JNI или более новый Foreign Function & Memory API); или просто любопытство: почему Rust год за годом возглавляет опросы «самый любимый язык». Эта страница сопоставляет то, что ты знаешь по Java, с Rust, чтобы ты мог сосредоточиться на том, что действительно отличается.
Если тебе нужно общее сравнение с C++, Go и Python, загляни в урок Rust против C++, Go и Python. Если ты знаешь ещё и .NET, есть отдельная страница: Rust для разработчиков на C#.
Понятия языка: из Java в Rust
Заголовок раздела «Понятия языка: из Java в Rust»Пользуйся этой таблицей как разговорником. В последнем столбце — ссылка на урок этого сайта, который объясняет сторону Rust.
| Java | Rust | Подробнее |
|---|---|---|
class с полями и методами |
struct для данных + блок impl для методов |
Структуры |
Конструкторы и new |
Обычная функция, по соглашению называемая new, которая возвращает Self |
Структуры |
interface |
trait |
Трейты |
Методы default в интерфейсе |
Методы по умолчанию в трейте (работают так же) | Трейты |
abstract class, extends, super |
Наследования нет. Для общего поведения используй трейты с методами по умолчанию, для общих данных — композицию (структура внутри структуры) | Трейты, Паттерны проектирования |
List<Shape> из разных классов |
Vec<Box<dyn Shape>> (трейт-объект) |
Трейт-объекты |
sealed interface + record + switch с шаблонами (Java 17/21) |
enum, у которого варианты несут данные, плюс match |
Перечисления, match |
enum с полями и конструкторами |
enum, у которого каждый вариант может хранить разные данные |
Перечисления |
record Point(int x, int y) |
#[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } |
Структуры, Популярные трейты |
toString(), equals(), hashCode(), compareTo() |
Display / Debug, PartialEq / Eq, Hash, PartialOrd / Ord (обычно через derive) |
Популярные трейты |
== (тот же объект) и .equals() (то же значение) |
== всегда сравнивает значения. Ловушки «сравнения ссылок» нет |
Операторы |
null и Optional<T> |
Option<T>: либо Some(value), либо 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 |
Проверяемые исключения и throws |
Result<T, E> в типе возвращаемого значения плюс оператор ? |
Result, Свои типы ошибок |
Непроверяемые исключения для настоящих ошибок в коде (IllegalStateException) |
panic! (останавливает поток) |
Паника |
Цепочки исключений (new ConfigException(msg, cause)) |
map_err или #[source] из thiserror и .context(...) из anyhow |
anyhow и thiserror |
try-with-resources и AutoCloseable |
Drop, который срабатывает автоматически, когда владелец выходит из области видимости |
Область видимости, Умные указатели |
| Сборщик мусора | Владение и заимствование, проверяемые при компиляции | Владение, Заимствование |
Локальные переменные final |
Обычный let (по умолчанию неизменяемый); пиши let mut, чтобы разрешить изменения |
Переменные |
Константы static final |
const |
Переменные |
Изменяемые поля static, синглтоны |
static с LazyLock или OnceLock, обёрнутый в Mutex, если его нужно менять |
Общее состояние |
Обобщения с <T extends Comparable<T>> |
Обобщения с ограничениями трейтов: T: Ord или where T: Ord |
Обобщения |
Стирание типов, List<Integer> вместо List<int> |
Мономорфизация: обобщённый код компилируется для каждого типа, а Vec<i32> хранит обычные числа |
Обобщения, Продвинутые обобщения |
Лямбды, Function<T, R>, Supplier<T>, Consumer<T> |
Замыкания, описываемые трейтами Fn, FnMut, FnOnce |
Замыкания, Fn, FnMut и FnOnce |
Стримы: filter, map, reduce, collect |
Адаптеры итераторов: filter, map, fold, sum, collect |
Итераторы |
Iterable<T> и Iterator<T> |
IntoIterator и Iterator (один метод: next, который возвращает Option) |
Свои итераторы |
parallelStream() |
par_iter() из крейта rayon |
Многопоточность |
Thread, ExecutorService |
std::thread::spawn и std::thread::scope; rayon или tokio для пулов |
Многопоточность |
synchronized, ReentrantLock |
Mutex<T>, который оборачивает данные, которые защищает |
Общее состояние |
AtomicInteger, AtomicLong |
AtomicI32, AtomicI64, AtomicUsize |
Общее состояние |
BlockingQueue между потоками |
Каналы (std::sync::mpsc) |
Каналы |
CompletableFuture, виртуальные потоки |
async fn и .await, которые запускает среда выполнения вроде tokio |
Async/Await |
String (неизменяемая) и StringBuilder |
&str (заимствованный взгляд на текст) и String (владеющая, растущая строка) |
Строки |
char (единица UTF-16) |
char (любой символ Unicode, 4 байта); строки хранятся в UTF-8 |
Строки |
int, long, double и упакованный Integer |
i32, i64, f64, плюс беззнаковые от u8 до u128. Никогда не упаковываются |
Типы данных |
| Тихое переполнение целых чисел | Паника в отладочных сборках; checked_add, wrapping_add, saturating_add — выбирай сам |
Переполнение целых чисел |
Приведения вроде (int) x |
as для чисел, From / TryFrom для безопасных преобразований |
Приведение типов, Преобразования |
BigDecimal, BigInteger |
Крейты rust_decimal и num-bigint |
Типы данных |
Пакеты и import |
Модули (mod) и use |
Модули |
public, private, protected, доступ на уровне пакета |
По умолчанию всё приватно; pub и pub(crate). protected нет |
Модули |
| JAR / артефакт Maven | Крейт (библиотека или исполняемый файл) | Cargo и крейты |
Аннотации (@Override, @Data из Lombok) |
Атрибуты вроде #[derive(Debug)] и #[test] |
Макросы |
| Рефлексия, обработка аннотаций во время работы | Во время работы почти нет. Вместо этого макросы генерируют код при компиляции | Процедурные макросы |
| Перегрузка методов | Перегрузки нет. Используй разные имена или обобщённый параметр | Функции |
Проверки instanceof и приведение вниз |
match по перечислению (приведение вниз через Any существует, но нужно редко) |
Сопоставление с образцом |
| JNI, Foreign Function & Memory API | Функции extern "C" |
FFI: вызов C |
Коллекции
Заголовок раздела «Коллекции»Java Collections Framework аккуратно ложится на std::collections. Главное отличие: коллекции Rust никогда не возвращают null. Поиск даёт тебе Option.
| Java | Rust | Подробнее |
|---|---|---|
ArrayList<T> |
Vec<T> |
Векторы |
int[], String[] |
[T; N], если длина известна при компиляции, иначе Vec<T>. Передавать и то и другое можно как срез &[T] |
Массивы, Срезы |
List.of(...) (неизменяемый) |
Массив или Vec, который ты отдаёшь только как &Vec<T> / &[T] (через общую ссылку его никто не изменит) |
Заимствование |
HashMap<K, V> |
HashMap<K, V> (get возвращает Option<&V>) |
HashMap |
map.merge(word, 1, Integer::sum) |
*map.entry(word).or_insert(0) += 1 |
HashMap |
TreeMap<K, V> |
BTreeMap<K, V> (ключи остаются отсортированными) |
HashMap |
LinkedHashMap<K, V> (порядок вставки) |
IndexMap из крейта indexmap |
|
HashSet<T>, TreeSet<T> |
HashSet<T>, BTreeSet<T> |
HashSet |
ArrayDeque<T> |
VecDeque<T> |
VecDeque и BinaryHeap |
Stack<T> или Deque в роли стека |
Vec<T> с push / pop |
Векторы |
PriorityQueue<T> (сначала наименьший) |
BinaryHeap<T> (сначала наибольший; оберни элементы в Reverse, чтобы сначала был наименьший) |
VecDeque и BinaryHeap |
LinkedList<T> |
LinkedList<T> есть, но Vec или VecDeque почти всегда быстрее |
VecDeque и BinaryHeap |
ConcurrentHashMap<K, V> |
Mutex<HashMap<K, V>> или крейт dashmap |
Общее состояние |
list.sort(Comparator.comparing(...)) |
v.sort_by_key(...) или v.sort_by(...) |
Сортировка и поиск |
list.removeIf(...) |
v.retain(...) (оставляет подходящие элементы) |
Векторы |
Как и у HashMap в Java, у HashMap в Rust нет фиксированного порядка, и он может меняться от запуска к запуску. Если нужен отсортированный вывод, используй BTreeMap или сначала отсортируй ключи.
Инструменты: из Maven и Gradle в Cargo
Заголовок раздела «Инструменты: из Maven и Gradle в Cargo»Большинство разработчиков на Java быстро влюбляются в Cargo. Один инструмент, один формат конфигурации и никакого XML для плагинов. Сборка, зависимости, тесты, документация, форматирование и проверки — всё из одного места.
| Java | Rust | Примечания |
|---|---|---|
| Maven / Gradle | cargo |
Собирает, запускает, тестирует, добавляет зависимости, публикует. См. Cargo и крейты |
pom.xml / build.gradle.kts |
Cargo.toml |
Короткий, и для обычных проектов скрипты сборки не нужны |
| Блокировка зависимостей в Gradle | Cargo.lock |
Создаётся и обновляется автоматически |
| Многомодульная сборка | Рабочее пространство Cargo (workspace) | Один Cargo.toml наверху перечисляет крейты-участники |
| Maven Central | crates.io | Документация по 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 |
Отладочные сборки работают намного медленнее; замеряй производительность в release |
java -jar app.jar |
./target/release/app |
Один нативный исполняемый файл. Ставить JVM на сервер не нужно |
| Версии JDK, SDKMAN!, toolchains | rustup и файл rust-toolchain.toml |
См. Инструменты и установка |
javac |
rustc |
Cargo вызывает его за тебя; сам ты запускаешь его редко |
| JShell | Rust Playground в браузере или evcxr_repl |
|
| Checkstyle, SpotBugs, PMD, Error Prone | cargo clippy |
Сотни официальных проверок |
| google-java-format, Spotless | cargo fmt |
Один стандартный стиль; никто не спорит о скобках |
| IntelliJ IDEA, Eclipse | JetBrains RustRover или VS Code + rust-analyzer | rust-analyzer даёт автодополнение, переход к определению и ошибки прямо в коде |
| Javadoc | Doc-комментарии /// в Markdown + cargo doc |
Примеры кода в документации запускаются как тесты. См. Документация |
| JUnit 5 | Встроенные #[test] и cargo test (cargo nextest — популярный более быстрый раннер) |
См. Тестирование |
@ParameterizedTest |
rstest |
|
| Mockito | mockall или написанные вручную фейки, реализующие твой трейт |
См. Тестирование: дополнительно |
| 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"] }Библиотеки и фреймворки: «Что использовать вместо…?»
Заголовок раздела «Библиотеки и фреймворки: «Что использовать вместо…?»»Это первый вопрос, который задаёт любая команда на Java: «Мы сидим на Spring Boot и Hibernate. А что есть в Rust?» В Rust нет Spring — нет одного фреймворка, который приносит всё сразу. Вместо этого ты собираешь вместе несколько узкоспециализированных крейтов. Вот ближайшие соответствия.
Веб и сеть
Заголовок раздела «Веб и сеть»| Java | Rust | Примечания |
|---|---|---|
| Spring Boot (Spring MVC / WebFlux) | axum (мы делаем такой в проекте веб-API), actix-web, Rocket | axum и actix-web широко используются в продакшене |
@RestController + @GetMapping |
Маршруты axum, указывающие на обычные обработчики async fn |
Никаких аннотаций и сканирования classpath; маршруты подключаются в коде |
| Фильтры сервлетов, перехватчики Spring | Слои tower и крейт tower-http (CORS, сжатие, трассировка, таймауты) |
axum использует tower напрямую |
Внедрение зависимостей Spring (@Autowired) |
Общий State в axum, обычные конструкторы и трейты |
См. строку про DI ниже |
| springdoc-openapi | utoipa |
|
| Thymeleaf | askama (шаблоны проверяются при компиляции) или tera |
|
| Spring WebSocket | WebSocket в axum (фича ws) или tokio-tungstenite |
|
| Spring for GraphQL | async-graphql |
|
| gRPC Java | tonic (с prost для protobuf) |
|
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) |
|
| Java-клиент RabbitMQ | lapin |
|
| AWS SDK for Java | Официальный AWS SDK для Rust: aws-config, aws-sdk-s3, … |
Если тебе нравятся сущности JPA, начни с SeaORM. Если предпочитаешь писать SQL (как любители jOOQ и MyBatis) — попробуй sqlx. Если нужны самые сильные гарантии при компиляции — Diesel. В уроке Базы данных все три показаны на работающем коде.
Повседневные строительные блоки
Заголовок раздела «Повседневные строительные блоки»| Java | Rust | Примечания |
|---|---|---|
| Jackson / Gson | serde + serde_json |
Стандарт. Также умеет TOML, YAML и другое. См. JSON и Serde |
| JAXB, Jackson XML | quick-xml (с фичей serialize для serde) |
|
| SLF4J + Logback / Log4j 2 | tracing (+ tracing-subscriber) или log + env_logger |
См. Логирование |
| Lombok | Встроенные derive (Debug, Clone, PartialEq, Default, …); bon или derive_builder для билдеров |
Никаких трюков с байт-кодом; это всё обычные макросы |
| MapStruct | Реализации From / Into |
Пишутся вручную, проверяются компилятором. См. Преобразования |
| IoC-контейнер Spring | Обычно ничего: передавай зависимости в конструкторы, используй трейты для того, что хочешь подменять в тестах | Проекты на Rust редко используют DI-контейнеры |
Bean Validation (@NotNull, @Size) |
validator или garde (валидация через derive) |
@NotNull не нужен: используй Option только там, где значения может не быть |
| Resilience4j | backon или middleware повторов и таймаутов из tower |
|
| Caffeine (кеширование) | moka |
|
| Guava, Apache Commons | В основном стандартная библиотека плюс itertools |
|
java.time |
chrono или jiff |
См. Дата и время |
| picocli | clap |
См. Проект: CLI-утилита на clap |
@Scheduled, Quartz |
tokio-cron-scheduler |
|
| Micrometer | metrics или opentelemetry |
|
| Jakarta Mail | lettre |
Десктоп, игры, Android и взаимодействие
Заголовок раздела «Десктоп, игры, Android и взаимодействие»| Java | Rust | Примечания |
|---|---|---|
| JavaFX / Swing | Tauri (веб-интерфейс + бэкенд на Rust), Slint, egui, Iced |
Самые стабильные — Tauri 2 и Slint 1.x. См. GUI-приложения |
| LibGDX, jMonkeyEngine | Bevy, macroquad |
Bevy ещё до 1.0 и меняет API между релизами. См. Разработка игр |
| Код Android-приложения | Оставь Kotlin или Java для интерфейса; общую логику вынеси в библиотеку на Rust, собранную через Android NDK | uniffi от Mozilla генерирует привязки для Kotlin |
| Вызов нативного кода из Java | Открой функции extern "C" и вызывай их через FFM API (JDK 22+) или пиши JNI-функции с крейтом jni |
См. FFI: вызов C |
Бок о бок: абстрактный класс становится трейтом
Заголовок раздела «Бок о бок: абстрактный класс становится трейтом»Вот классическая иерархия на 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 получает 5000 в месяцLinus получает 6000 в месяцТрейт не может хранить поля, поэтому protected name превращается в небольшой метод name(), который реализует каждая структура. Поначалу это кажется лишним набором текста, но так исчезает целый класс проблем: нет хрупких базовых классов, нет вызовов super, о которых можно забыть, и нет глубоких иерархий, которые приходится листать. Если ты знаешь методы default в интерфейсах из Java 8, ты уже знаешь, как Rust делится поведением. А когда набор типов фиксирован (как у sealed interface в Java 17), перечисление с match часто проще трейта.
Бок о бок: Optional становится Option
Заголовок раздела «Бок о бок: Optional становится Option»Optional в Java был большим шагом вперёд, но используется он только для возвращаемых значений, а 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("email не указан"));}findEmail(1).ifPresent(email -> System.out.println("отправляем на " + email));У Option в Rust почти такой же 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(|| "email не указан".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:?}");}Вывод:
email не указанотправляем на [email protected]Some("example.com")Компилятор не даст вызвать .to_uppercase() у Option<String>, пока ты не разобрался с None, поэтому NullPointerException просто не существует. К тому же Option ничего не стоит: Option<&T> занимает столько же места, сколько обычная ссылка. Вспомогательные методы описаны в уроке Option.
Бок о бок: проверяемые исключения становятся Result
Заголовок раздела «Бок о бок: проверяемые исключения становятся 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, раскрутки стека, исключений, завёрнутых в RuntimeException только ради того, чтобы протащить их через лямбду. Ошибки — обычные значения, поэтому они спокойно работают внутри замыканий и цепочек итераторов. ? — видимая метка в каждом месте, где функция может вернуться раньше. Писать Display вручную быстро надоедает; крейт thiserror генерирует его за тебя, а anyhow даёт приложениям общий тип ошибки на все случаи (см. anyhow и thiserror). Для настоящих ошибок в коде, о которых ты сообщил бы непроверяемым исключением, в Rust есть panic!.
Бок о бок: стримы становятся итераторами
Заголовок раздела «Бок о бок: стримы становятся итераторами»Конвейер стримов с коллектором 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));В Rust роль .stream() играет .iter(), а у адаптеров знакомые имена:
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 сортирует ключи по алфавиту, и с русскими названиями порядок в выводе был бы другим.
Как и стримы, итераторы в Rust ленивые и работают, только когда их что-то потребляет (collect, sum, цикл for). В отличие от стримов, класса Collectors нет: collect() смотрит на тип, который ты просишь (Vec, HashMap, String, …), и строит его. Для группировки обычный ответ — простой цикл с entry API; если тебе не хватает groupingBy, крейт itertools добавляет помощники. А цепочки итераторов компилируются в тот же машинный код, что и написанный вручную цикл, без всякой упаковки int по дороге. Подробнее — в уроках Итераторы и Сортировка и поиск.
Бок о бок: стирание типов против мономорфизации
Заголовок раздела «Бок о бок: стирание типов против мономорфизации»Обобщения 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 компилирует отдельную копию каждой обобщённой функции для каждого типа, с которым ты её используешь (это называется мономорфизацией). Поэтому функция точно знает, что такое T, и может вызвать T::default():
use std::mem::size_of;
// «У T должно быть значение по умолчанию, и 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>. Ограничения после двоеточия (T: Default + Clone) — это версия Rust для <T extends ...>. См. Обобщения и Продвинутые обобщения.
Бок о бок: synchronized становится Arc и Mutex
Заголовок раздела «Бок о бок: 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() возвращает guard; блокировка снимается, когда guard удаляется. *counter.lock().unwrap() += 1; } })); }
for handle in handles { handle.join().unwrap(); }
println!("счётчик = {}", *counter.lock().unwrap());}Вывод:
счётчик = 4000Вот что разработчики на Java особенно любят: если забыть synchronized в Java, код скомпилируется и время от времени будет выдавать неверные числа. В Rust, если попытаться поделить между потоками обычный i32 или Vec и менять его, программа не скомпилируется. Гонки данных ловит система типов (через трейты Send и Sync). Для одного счётчика AtomicUsize был бы легче мьютекса — так же, как AtomicInteger. См. Многопоточность и Общее состояние.
Обо что споткнётся разработчик на Java
Заголовок раздела «Обо что споткнётся разработчик на Java»Каждый разработчик на Java натыкается на одни и те же кочки. Если знать их заранее, они становятся меньше.
-
Передача значения в функцию перемещает его. В Java при передаче объекта передаётся ссылка, и переменной можно пользоваться дальше. В Rust передача
StringилиVecпо значению перемещает (move) его в функцию: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 moveКомпилятор говорит, что
nameуже перемещено при первом вызове. Обычное исправление — дать функции заимствовать:fn greet(name: &str)и вызовgreet(&name). Сначала прочитай урок Владение: он объясняет это правило, а E0382 подробно разбирает эту ошибку. -
Анализатор заимствований. Одновременно может быть много читателей (
&T) или один писатель (&mut T), но никогда и то и другое. Графы объектов, обычные для Java, — например, дочерний объект со ссылкой обратно на родителя или много объектов, которые держат и меняют один общий объект, — требуют умных указателей или другой архитектуры. Первые несколько недель могут раздражать. Потом действительно становится легче. -
Нет наследования, нет
protected, нетsuper. Будет соблазн воссоздать иерархии классов с помощью трейтов. Сначала попробуй перечисления и композицию: обычно они подходят лучше. -
==сравнивает значения. На этот раз хорошая новость:a == bдля двухStringсравнивает текст, так что классическая ошибка==вместо.equals()невозможна. Для своих структур==работает только после того, как ты сделаешь derive или реализуешьPartialEq. -
BinaryHeapвыдаёт наибольший элемент.PriorityQueueв Java сначала выдаёт наименьший. В Rust оберни элементы вstd::cmp::Reverse, чтобы получить поведение min-кучи. См. VecDeque и BinaryHeap. -
Переполнение целых чисел не молчит.
Integer.MAX_VALUE + 1в Java тихо переходит через край. В отладочной сборке Rust это паника. Говори, что имеешь в виду, черезchecked_add,wrapping_addилиsaturating_add. См. Переполнение целых чисел. -
Stringи&str, и нетcharAt. Строки в Rust хранятся в UTF-8, поэтомуs[0]не скомпилируется. Чтобы пройти по символам, используйs.chars(). Функции обычно принимают&str, структуры обычно хранятString. См. Строки. -
Нет перегрузки методов. Нельзя иметь два метода
printс разными параметрами. Используй разные имена (print,print_with_color) или обобщённый параметр. -
Никакой магии во время работы. Нет сканирования classpath, нет
@Autowired, нет прокси, которые оборачивают твои методы транзакциями. Всё связывается явно в коде, а макросы работают при компиляции. Печатать приходится больше, зато всегда видно, что происходит. -
Нет
nullи нет исключений. ПоначалуOptionиResultповсюду кажутся лишней работой. Через несколько недель заметишь, что давно не видел ниNullPointerException, ни неожиданного стек-трейса. Вызывать.unwrap()во время обучения нормально, но в настоящем коде это версия Rust для «пусть падает». -
Время компиляции. Чистая сборка скачивает и компилирует все зависимости из исходников, поэтому она медленнее
javac. Инкрементальные сборки намного быстрее, а чаще всего во время редактирования ты будешь запускатьcargo check(он пропускает генерацию кода). Хорошая новость с другой стороны: программа запускается мгновенно, без прогрева JVM. -
Для async нужна среда выполнения. Виртуальных потоков нет.
async fnничего не делает, пока её не запустит исполнитель, поэтому веб-проекты добавляют tokio и пишут#[tokio::main]надmain. См. Async/Await.
Путь обучения для разработчиков на Java
Заголовок раздела «Путь обучения для разработчиков на Java»Этот сайт не обязательно читать по порядку. Этот маршрут пропускает то, чему тебя уже научила Java, и тратит время там, где Rust отличается:
- Первые шаги и Инструменты и установка: установи Rust, выбери редактор, запусти
cargo new. - Переменные и типы данных: пробеги глазами, в основном чтобы узнать
let mutи числовые типы. - Владение, затем заимствование, затем срезы: самые важные для тебя страницы. Не торопись.
- Строки:
Stringи&str. - Структуры и перечисления, затем match и сопоставление с образцом.
- Option и Result, затем свои типы ошибок и anyhow и thiserror.
- Трейты, обобщения, популярные трейты и трейт-объекты: твои интерфейсы и абстрактные классы.
- Замыкания и итераторы: твои лямбды и стримы.
- Векторы, HashMap и VecDeque и BinaryHeap.
- Модули, Cargo и крейты и тестирование.
- Времена жизни и умные указатели.
- Многопоточность, общее состояние, каналы и async/await.
- JSON и Serde, логирование и базы данных, а потом собери проект веб-API.
- Хочешь ускорить существующее Java-приложение? Прочитай урок FFI: вызов C, чтобы увидеть, как Rust открывает функции
extern "C".
Практика
Заголовок раздела «Практика»В обоих упражнениях дан код на Java. Перенеси его на Rust.
1. Перенеси record и конвейер стримов
Заголовок раздела «1. Перенеси record и конвейер стримов»Этот код выводит студентов, которые сдали экзамен, есть ли у кого-то идеальный результат, и лучший балл. Подсказка: у итераторов есть 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("есть 100 баллов: " + 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!("есть 100 баллов: {perfect}"); println!("лучший балл: {best}");}Вывод:
сдали: Ada, Grace, Kenесть 100 баллов: trueлучший балл: 100Атрибуты derive дают то, что record в Java даёт бесплатно: отладочную строку ({:?}), копирование (.clone()) и сравнение по значению (==). Вспомогательная функция student играет роль конструктора record. В отличие от стрима в Java, students.iter() можно вызывать сколько угодно раз, потому что каждый вызов создаёт новый итератор по тому же Vec.
2. Перенеси проверяемое исключение с причиной
Заголовок раздела «2. Перенеси проверяемое исключение с причиной»Этот метод читает строку port=<число> из файла конфигурации. Он бросает проверяемое ConfigException и оборачивает NumberFormatException, которое получает от Integer.parseInt. Перенеси его так, чтобы read_port возвращала Result<u16, String>. Подсказка: line.split_once('=') возвращает Option<(&str, &str)>; преврати его в Result с помощью .ok_or_else(...). .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, try/catch вокруг parseInt — одним map_err, а throw — значением Err(...). u16 для порта — небольшой бонус: число больше 65535 отвергает сам parse, чего int в Java не сделал бы. Маленькое замыкание expected строит сообщение об ошибке, только когда оно действительно нужно, как поставщик в orElseThrow(() -> ...).
- Многое из Java переносится: обобщения, интерфейсы (в виде трейтов), лямбды (в виде замыканий) и конвейеры стримов (в виде итераторов).
- Главные сдвиги: владение вместо сборщика мусора,
Optionвместоnull,Resultи?вместо исключений, трейты плюс композиция вместо наследования. - Обобщения компилируются для каждого типа отдельно (мономорфизация), поэтому нет ни стирания типов, ни упаковки чисел.
- Общие данные между потоками живут внутри
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. PriorityQueue в Java сначала возвращает наименьший элемент. Что возвращает BinaryHeap::pop в Rust?
3. Почему обобщённая функция в Rust может вызвать T::default(), а в Java нельзя написать new T()?
4. Ты делишь обычный Vec между потоками и пытаешься добавлять в него элементы без Mutex. Что произойдёт в Rust?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Язык программирования Rust: официальная книга (перевод на русский). Для разработчиков на Java важнее всего главы 4 (владение), 9 (обработка ошибок), 10 (обобщения и трейты) и 16 (многопоточность).
- std::collections (на английском): собственное руководство стандартной библиотеки по выбору между
Vec,VecDeque,HashMap,BTreeMapи остальными. - JEP 454: Foreign Function & Memory API (на английском): возможность JDK 22 для вызова нативных библиотек, в том числе на Rust, из Java без JNI-кода.
- Крейт jni на docs.rs (на английском): привязки Rust к JNI, если нужно вызывать Rust из старых JDK или обращаться обратно к Java.
- Are We Web Yet? (на английском): обзор веб-фреймворков, библиотек для баз данных и не только от сообщества — если ты ищешь замену своему стеку Spring Boot.