Pular para o conteúdo

Rust para desenvolvedores Java: guia prático

Se você passou anos com Java, já tem quase tudo de que precisa para aprender Rust. Você pensa em tipos. Você conhece genéricos, interfaces, lambdas e pipelines de streams. O Rust tem tudo isso: os traits lembram muito interfaces com métodos padrão, os iteradores se leem como streams, e o Cargo faz o trabalho do Maven ou do Gradle, só que com bem menos configuração.

O que é novo é tudo aquilo que a JVM fazia por você. O Rust não tem garbage collector (coletor de lixo) nem máquina virtual. O seu código é compilado para um binário nativo, e o compilador decide quando a memória é liberada verificando o ownership (posse) e o borrowing (empréstimo) antes de o programa rodar. O Rust também não tem null (você usa Option), não tem exceções (você usa Result) e não tem herança de classes. É nessas quatro mudanças que vai a maior parte do seu tempo de estudo.

Por que desenvolvedores Java aprendem Rust? Normalmente por um destes motivos: serviços que precisam de um uso de memória baixo e estável, sem pausas do GC; ferramentas de linha de comando e funções serverless que devem iniciar na hora, sem JVM; um trecho crítico de uma aplicação Java que você quer levar para código nativo (o Rust pode ser chamado do Java por JNI ou pela nova Foreign Function & Memory API); ou simplesmente curiosidade sobre por que o Rust vive no topo das pesquisas de “linguagem mais admirada”. Esta página mostra o equivalente em Rust do que você já conhece do Java, para você focar nas partes que são realmente diferentes.

Se você prefere uma comparação geral com C++, Go e Python, veja Rust vs outras linguagens. Se você também conhece .NET, há uma página separada: Rust para desenvolvedores C#.

Use esta tabela como um guia de conversação. A última coluna leva à lição deste site que explica o lado do Rust.

Java Rust Saiba mais
class com campos e métodos struct para os dados + um bloco impl para os métodos Structs
Construtores e new Uma função comum, chamada new por convenção, que devolve Self Structs
interface trait Traits
Métodos default numa interface Métodos padrão num trait (funcionam do mesmo jeito) Traits
abstract class, extends, super Não existe herança. Use traits com métodos padrão para o comportamento compartilhado e composição (uma struct dentro de outra) para os dados compartilhados Traits, Padrões de projeto
List<Shape> com classes diferentes Vec<Box<dyn Shape>> (um trait object) Trait objects
sealed interface + records + switch com padrões (Java 17/21) Um enum cujas variantes carregam dados, mais match Enums, Match
enum com campos e construtores enum em que cada variante pode guardar dados diferentes Enums
record Point(int x, int y) #[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } Structs, Traits comuns
toString(), equals(), hashCode(), compareTo() Display / Debug, PartialEq / Eq, Hash, PartialOrd / Ord (normalmente com derive) Traits comuns
== (mesmo objeto) vs .equals() (mesmo valor) == sempre compara valores. Não existe a armadilha da “igualdade de referência” Operadores
null e Optional<T> Option<T>: ou Some(valor) ou 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 exceptions e throws Result<T, E> no tipo de retorno, mais o operador ? Result, Erros personalizados
Unchecked exceptions para bugs de verdade (IllegalStateException) panic! (encerra a thread) Panic
Encadeamento de exceções (new ConfigException(msg, cause)) map_err, ou o #[source] do thiserror e o .context(...) do anyhow anyhow e thiserror
try-with-resources e AutoCloseable Drop, que roda sozinho quando o dono sai de escopo Escopo, Ponteiros inteligentes
Garbage collector Ownership e borrowing, verificados em tempo de compilação Ownership, Borrowing
Variáveis locais final O let simples (imutável por padrão); escreva let mut para permitir mudanças Variáveis
Constantes static final const Variáveis
Campos static mutáveis, singletons static com LazyLock ou OnceLock, dentro de um Mutex se precisar mudar Estado compartilhado
Genéricos com <T extends Comparable<T>> Genéricos com trait bounds: T: Ord ou where T: Ord Genéricos
Type erasure, List<Integer> em vez de List<int> Monomorfização: o código genérico é compilado para cada tipo, e um Vec<i32> guarda números simples Genéricos, Genéricos avançados
Lambdas, Function<T, R>, Supplier<T>, Consumer<T> Closures, descritas pelos traits Fn, FnMut, FnOnce Closures, Fn, FnMut e FnOnce
Streams: filter, map, reduce, collect Adaptadores de iterador: filter, map, fold, sum, collect Iteradores
Iterable<T> e Iterator<T> IntoIterator e Iterator (um método só: next, que devolve um Option) Iteradores personalizados
parallelStream() par_iter() do crate rayon Concorrência
Thread, ExecutorService std::thread::spawn e std::thread::scope; rayon ou tokio para pools Concorrência
synchronized, ReentrantLock Mutex<T>, que envolve os dados que protege Estado compartilhado
AtomicInteger, AtomicLong AtomicI32, AtomicI64, AtomicUsize Estado compartilhado
BlockingQueue entre threads Canais (std::sync::mpsc) Canais (channels)
CompletableFuture, virtual threads async fn e .await, executados por um runtime como o tokio Async/Await
String (imutável) e StringBuilder &str (uma visão emprestada de um texto) e String (com dono, pode crescer) Strings
char (uma unidade UTF-16) char (qualquer caractere Unicode, 4 bytes); as strings são UTF-8 Strings
int, long, double e o Integer em box i32, i64, f64, mais os sem sinal de u8 a u128. Nunca em box Tipos de dados
Overflow de inteiros silencioso Um panic nos builds de debug; use checked_add, wrapping_add, saturating_add para escolher Overflow de inteiros
Casts como (int) x as para números, From / TryFrom para conversões seguras Conversão de tipos, Conversões
BigDecimal, BigInteger Os crates rust_decimal e num-bigint Tipos de dados
Pacotes e import Módulos (mod) e use Módulos
public, private, protected, package-private Privado por padrão; pub e pub(crate). Não existe protected Módulos
Um JAR / artefato Maven Um crate (uma biblioteca ou um binário) Cargo e crates
Anotações (@Override, o @Data do Lombok) Atributos como #[derive(Debug)] e #[test] Macros
Reflection, processamento de anotações em tempo de execução Quase nada em tempo de execução. As macros geram código em tempo de compilação Macros procedurais
Sobrecarga de métodos Não existe sobrecarga. Use nomes diferentes, ou um parâmetro genérico Funções
Verificações com instanceof e downcasts match sobre um enum (o downcast com Any existe, mas é raro) Pattern matching
JNI, Foreign Function & Memory API Funções extern "C" FFI: chamando C

O Java Collections Framework tem correspondência direta com std::collections. A maior diferença: as coleções do Rust nunca devolvem null. Uma busca devolve um Option.

Java Rust Saiba mais
ArrayList<T> Vec<T> Vetores
int[], String[] [T; N] quando o tamanho é conhecido em tempo de compilação; senão, Vec<T>. Passe qualquer um dos dois como um slice &[T] Arrays, Slices
List.of(...) (não modificável) Um array, ou um Vec que você só entrega como &Vec<T> / &[T] (ninguém consegue alterá-lo por uma referência compartilhada) Borrowing
HashMap<K, V> HashMap<K, V> (o get devolve Option<&V>) HashMap
map.merge(word, 1, Integer::sum) *map.entry(word).or_insert(0) += 1 HashMap
TreeMap<K, V> BTreeMap<K, V> (as chaves ficam ordenadas) HashMap
LinkedHashMap<K, V> (ordem de inserção) IndexMap do crate indexmap
HashSet<T>, TreeSet<T> HashSet<T>, BTreeSet<T> HashSet
ArrayDeque<T> VecDeque<T> VecDeque e BinaryHeap
Stack<T>, ou um Deque usado como pilha Vec<T> com push / pop Vetores
PriorityQueue<T> (o menor primeiro) BinaryHeap<T> (o maior primeiro; envolva os itens em Reverse para o menor primeiro) VecDeque e BinaryHeap
LinkedList<T> LinkedList<T> existe, mas Vec ou VecDeque quase sempre são mais rápidos VecDeque e BinaryHeap
ConcurrentHashMap<K, V> Mutex<HashMap<K, V>>, ou o crate dashmap Estado compartilhado
list.sort(Comparator.comparing(...)) v.sort_by_key(...) ou v.sort_by(...) Ordenação e busca
list.removeIf(...) v.retain(...) (mantém os itens que batem com a condição) Vetores

Assim como o HashMap do Java, um HashMap do Rust não tem ordem fixa, e a ordem pode mudar de uma execução para outra. Se você precisa de uma saída ordenada, use um BTreeMap ou ordene as chaves antes.

A maioria dos desenvolvedores Java se apaixona pelo Cargo rapidinho. É uma ferramenta só, um formato de configuração só, e nenhum XML de plugin. Build, dependências, testes, documentação, formatação e lints, tudo vem do mesmo lugar.

Java Rust Observações
Maven / Gradle cargo Compila, executa, testa, adiciona dependências, publica. Veja Cargo e crates
pom.xml / build.gradle.kts Cargo.toml Curto, e sem scripts de build em projetos normais
Dependency locking do Gradle Cargo.lock Criado e atualizado para você
Build multimódulo Um workspace do Cargo Um Cargo.toml no topo lista os crates membros
Maven Central crates.io A documentação da API de todo crate publicado fica no docs.rs
Adicionar uma <dependency> cargo add serde
mvn archetype:generate / gradle init cargo new my_app (cargo new --lib para uma biblioteca)
mvn compile / mvn test / mvn package cargo build / cargo test / cargo build --release Builds de debug são bem mais lentos na execução; meça o desempenho em release
java -jar app.jar ./target/release/app Um único binário nativo. Nenhuma JVM para instalar no servidor
Versões do JDK, SDKMAN!, toolchains rustup e um arquivo rust-toolchain.toml Veja Ferramentas do Rust
javac rustc O Cargo chama para você; raramente você roda o rustc direto
JShell O Rust Playground no navegador, ou evcxr_repl
Checkstyle, SpotBugs, PMD, Error Prone cargo clippy Centenas de lints oficiais
google-java-format, Spotless cargo fmt Um estilo padrão; ninguém discute sobre chaves
IntelliJ IDEA, Eclipse JetBrains RustRover, ou VS Code + rust-analyzer O rust-analyzer oferece autocompletar, ir para a definição e erros na linha
Javadoc Comentários de documentação /// em Markdown + cargo doc Os exemplos de código na documentação rodam como testes. Veja Documentação
JUnit 5 #[test] embutido e cargo test (o cargo nextest é um executor popular e mais rápido) Veja Testes em Rust
@ParameterizedTest rstest
Mockito mockall, ou fakes escritos à mão que implementam o seu trait Veja Testes avançados
AssertJ / Hamcrest assert!, assert_eq! e pretty_assertions para diffs legíveis
WireMock, Testcontainers wiremock, testcontainers As mesmas ideias, em versão Rust
JaCoCo cargo llvm-cov ou cargo tarpaulin
JMH criterion Veja Desempenho
JFR, async-profiler, VisualVM cargo flamegraph (instalado com cargo install flamegraph), perf Nenhum GC ou JIT para ajustar
GraalVM native-image Não é preciso Todo build do Rust já é código nativo

Aqui está um Cargo.toml pequeno. Compare com o pom.xml de que você precisaria para as mesmas três dependências:

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

Bibliotecas e frameworks: “O que usamos no lugar de…?”

Seção intitulada “Bibliotecas e frameworks: “O que usamos no lugar de…?””

Essa é a primeira pergunta de todo time Java: “A gente usa Spring Boot e Hibernate. Qual é a versão em Rust?” O Rust não tem um Spring, um único framework que traz tudo. Em vez disso, você combina alguns crates focados. Aqui estão os equivalentes mais próximos.

Java Rust Observações
Spring Boot (Spring MVC / WebFlux) axum (construímos uma API com ele em Projeto: API web com Axum), actix-web, Rocket axum e actix-web são muito usados em produção
@RestController + @GetMapping Rotas do axum apontando para handlers async fn simples Sem anotações nem varredura do classpath; você liga as rotas no código
Filtros de servlet, interceptors do Spring Camadas do tower e o crate tower-http (CORS, compressão, tracing, timeouts) O axum usa o tower diretamente
Injeção de dependência do Spring (@Autowired) State compartilhado no axum, construtores simples e traits Veja a linha sobre DI mais abaixo
springdoc-openapi utoipa
Thymeleaf askama (templates verificados em tempo de compilação) ou tera
Spring WebSocket WebSockets no axum (a feature ws), ou tokio-tungstenite
Spring for GraphQL async-graphql
gRPC Java tonic (com prost para protobuf)
java.net.http.HttpClient, OkHttp, WebClient reqwest Assíncrono por padrão, com um cliente bloqueante opcional
Netty tokio + hyper
Spring Security (JWT, hash de senhas) jsonwebtoken, argon2 Não existe um framework de segurança completo
Java Rust Observações
Hibernate / JPA / Spring Data JPA SeaORM: ORM assíncrono com entidades, relacionamentos e migrations É o que mais lembra o JPA; construído em cima do sqlx
Hibernate, pensando em tipagem rígida Diesel: ORM e query builder; as consultas têm os tipos verificados em tempo de compilação Maduro (2.x), síncrono por padrão (existe o diesel-async)
JDBC, JdbcTemplate, jOOQ, MyBatis sqlx: SQL puro, e a macro query! confere a consulta com o seu banco de dados real em tempo de compilação Assíncrono, muito popular, não é um ORM
Flyway / Liquibase sqlx migrate (sqlx-cli), diesel migration ou refinery
Jedis / Lettuce (Redis) redis
Clientes Kafka, Spring Kafka rdkafka (construído sobre a librdkafka)
Cliente Java do RabbitMQ lapin
AWS SDK for Java O AWS SDK oficial para Rust: aws-config, aws-sdk-s3, …

Se você gosta das entidades do JPA, experimente primeiro o SeaORM. Se você prefere escrever SQL (a turma do jOOQ e do MyBatis), experimente o sqlx. Se você quer as garantias mais fortes em tempo de compilação, experimente o Diesel. Bancos de dados em Rust mostra os três com código que funciona.

Java Rust Observações
Jackson / Gson serde + serde_json O padrão. Também lida com TOML, YAML e mais. Veja JSON com Serde
JAXB, Jackson XML quick-xml (com a feature serialize para o serde)
SLF4J + Logback / Log4j 2 tracing (+ tracing-subscriber), ou log + env_logger Veja Logging
Lombok Derives embutidos (Debug, Clone, PartialEq, Default, …); bon ou derive_builder para builders Sem truques de bytecode; são só macros normais
MapStruct Implementações de From / Into Escritas à mão, verificadas pelo compilador. Veja Conversões
Contêiner IoC do Spring Normalmente nada: passe as dependências nos construtores e use traits para o que você troca nos testes Projetos Rust raramente usam um contêiner de DI
Bean Validation (@NotNull, @Size) validator ou garde (validação com derive) O @NotNull não é necessário: use Option só onde um valor pode faltar
Resilience4j backon, ou o middleware de retry e timeout do tower
Caffeine (cache) moka
Guava, Apache Commons Quase sempre a biblioteca padrão, mais o itertools
java.time chrono ou jiff Veja Datas e horas
picocli clap Veja Projeto: CLI com clap
@Scheduled, Quartz tokio-cron-scheduler
Micrometer metrics, ou opentelemetry
Jakarta Mail lettre
Java Rust Observações
JavaFX / Swing Tauri (interface web + backend em Rust), Slint, egui, Iced Tauri 2 e Slint 1.x são os mais estáveis. Veja Apps com interface gráfica
LibGDX, jMonkeyEngine Bevy, macroquad O Bevy está antes da 1.0 e muda a API entre versões. Veja Desenvolvimento de jogos
Código de apps Android Mantenha Kotlin ou Java na interface; coloque a lógica compartilhada numa biblioteca Rust compilada com o Android NDK O uniffi, da Mozilla, gera bindings para Kotlin
Chamar código nativo a partir do Java Exponha funções extern "C" e chame-as pela API FFM (JDK 22+), ou escreva funções JNI com o crate jni Veja FFI: chamando C

Aqui está uma hierarquia clássica em Java: uma classe base abstrata com um método abstrato e um método compartilhado.

abstract class Employee {
protected final String name;
Employee(String name) { this.name = name; }
abstract int monthlyPay();
String payslip() { return name + " earns " + monthlyPay() + " a month"; }
}
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());

O Rust não tem extends. O método compartilhado vai para um trait como um método padrão, e cada struct guarda os próprios dados:

trait Employee {
fn name(&self) -> &str;
fn monthly_pay(&self) -> u32;
// Um método padrão: como o payslip() concreto da classe abstrata.
fn payslip(&self) -> String {
format!("{} ganha {} por mês", 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> faz o papel de "uma referência para algum 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());
}
}

Saída:

Ada ganha 5000 por mês
Linus ganha 6000 por mês
▶ Experimente você mesmo

Um trait não pode ter campos, então o protected name vira um pequeno método name() que cada struct implementa. No começo, isso parece digitação a mais, mas elimina uma classe inteira de problemas: nada de classes base frágeis, nada de chamadas a super para esquecer, nada de hierarquias profundas para percorrer. Se você conhece os métodos default das interfaces do Java 8, já sabe como o Rust compartilha comportamento. E quando o conjunto de tipos é fechado (como uma sealed interface no Java 17), um enum com match muitas vezes é mais simples que um trait.

O Optional do Java foi um grande avanço, mas ele só é usado em valores de retorno, e o map.get() ou qualquer campo ainda podem ser null:

static Optional<String> findEmail(int userId) {
return userId == 1 ? Optional.of("[email protected]") : Optional.empty();
}
for (int id : new int[] {1, 2}) {
System.out.println(findEmail(id).map(String::toUpperCase).orElse("no email on file"));
}
findEmail(1).ifPresent(email -> System.out.println("sending to " + email));

O Option do Rust tem quase a mesma API, mas ele é usado em todo lugar onde um valor pode faltar, inclusive nas buscas em coleções. E não existe um null escondido atrás dele:

fn find_email(user_id: u32) -> Option<String> {
if user_id == 1 {
Some("[email protected]".to_string())
} else {
None
}
}
fn main() {
for id in [1, 2] {
// map + orElseGet
let shown = find_email(id)
.map(|email| email.to_uppercase())
.unwrap_or_else(|| "nenhum e-mail cadastrado".to_string());
println!("{shown}");
}
// ifPresent
if let Some(email) = find_email(1) {
println!("enviando para {email}");
}
// O flatMap se chama and_then
let domain = find_email(1).and_then(|email| email.split_once('@').map(|(_, d)| d.to_string()));
println!("{domain:?}");
}

Saída:

nenhum e-mail cadastrado
enviando para [email protected]
Some("example.com")
▶ Experimente você mesmo

O compilador não deixa você chamar .to_uppercase() em um Option<String> antes de tratar o None, então a NullPointerException simplesmente não existe. O Option também não custa nada a mais: um Option<&T> tem o mesmo tamanho de uma referência simples. Aprenda os métodos auxiliares na página de Option.

As checked exceptions tinham uma boa ideia no fundo: a assinatura conta o que pode dar errado. Aqui está uma versão típica em Java:

class InsufficientFundsException extends Exception {
InsufficientFundsException(int needed) { super("need " + needed + " more"); }
}
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("paid " + amount + ", " + left + " left");
return left;
}
try { pay(100, 80); pay(50, 80); }
catch (InsufficientFundsException e) { System.out.println("error: " + e.getMessage()); }

O Rust fica com essa boa ideia e descarta o resto. O erro faz parte do tipo de retorno (Result<u32, BankError>), e o ? faz o que o throws faz: “se isto falhou, repasse o erro para quem me chamou”. Os erros normalmente são um enum, então um único tipo pode listar todas as formas como uma operação pode falhar:

use std::fmt;
#[derive(Debug)]
enum BankError {
InvalidAmount,
InsufficientFunds { needed: u32 },
}
// Display faz o papel do getMessage().
impl fmt::Display for BankError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BankError::InvalidAmount => write!(f, "o valor precisa ser maior que zero"),
BankError::InsufficientFunds { needed } => write!(f, "faltam {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)?; // como chamar um método que "lança" exceção
println!("pago {amount}, restam {left}");
Ok(left)
}
fn main() {
for (balance, amount) in [(100, 80), (50, 80), (100, 0)] {
if let Err(e) = pay(balance, amount) {
println!("erro: {e}");
}
}
}

Saída:

pago 80, restam 20
erro: faltam 30
erro: o valor precisa ser maior que zero
▶ Experimente você mesmo

Repare no que sumiu: nada de blocos try, nada de desenrolar a pilha, nada de exceções embrulhadas numa RuntimeException só para passarem por uma lambda. Os erros são valores comuns, então funcionam bem dentro de closures e de cadeias de iteradores. O ? é uma marca visível em cada ponto em que a função pode retornar mais cedo. Escrever o Display à mão cansa rápido; o crate thiserror gera isso para você, e o anyhow dá às aplicações um tipo de erro que serve para tudo (veja anyhow e thiserror). Para bugs de verdade, do tipo que você sinalizaria com uma unchecked exception, o Rust tem o panic!.

Um pipeline de stream com um coletor 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));

Em Rust, o .iter() faz o papel do .stream(), e os adaptadores têm nomes conhecidos:

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()): a ordenação acontece num Vec, não dentro do iterador.
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}");
}
}

Saída:

Linus, Ken, Ada
Helsinki: 200
London: 120
New York: 230
▶ Experimente você mesmo

Os nomes das cidades ficaram em inglês porque o BTreeMap os ordena alfabeticamente. Como os streams, os iteradores do Rust são preguiçosos (lazy) e só rodam quando algo os consome (collect, sum, um laço for). Diferente dos streams, não existe uma classe Collectors: o collect() olha para o tipo que você pediu (Vec, HashMap, String, …) e monta esse tipo. Para agrupar, a resposta comum é um laço simples com a entry API; o crate itertools traz funções auxiliares se você sentir falta do groupingBy. E as cadeias de iteradores são compiladas para o mesmo código de máquina de um laço escrito à mão, sem colocar ints em box pelo caminho. Leia mais em Iteradores e Ordenação e busca.

Os genéricos do Java só existem em tempo de compilação. Em tempo de execução, o T é apagado, então você não pode escrever new T(), nem colocar um int numa List sem fazer boxing:

static <T> List<T> filled(int n) {
List<T> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(new T()); // não compila: T foi apagado
return list;
}
List<int> ids; // também não compila: você precisa de List<Integer>

O Rust compila uma cópia separada de cada função genérica para cada tipo com que você a usa (isso se chama monomorfização). Assim, a função sabe exatamente o que é T, e pode chamar T::default():

use std::mem::size_of;
// "T precisa ter um valor padrão e poder ser clonado"
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:?}");
// Sem boxing: um Vec<i32> guarda números simples de 4 bytes, lado a lado.
println!("um i32 ocupa {} bytes", size_of::<i32>());
println!("um bool ocupa {} byte", size_of::<bool>());
}

Saída:

[0, 0, 0] ["", ""] [false, false]
um i32 ocupa 4 bytes
um bool ocupa 1 byte
▶ Experimente você mesmo

A troca: mais cópias do código significam binários maiores e compilação mais demorada, mas cada chamada é direta e pode virar inline. Quando você quer uma única cópia compartilhada (como no Java), use um trait object como Box<dyn Trait>. Os bounds depois dos dois-pontos (T: Default + Clone) são a versão Rust do <T extends ...>. Veja Genéricos e Genéricos avançados.

Quatro threads incrementam um contador compartilhado. Em Java, o lock fica no objeto, e cabe a você lembrar do 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("count = " + counter.get());

Em Rust, o Mutex é dono do número. Não há como chegar até ele sem travar antes. O Arc (um ponteiro com contagem de referências atômica) permite que várias threads compartilhem o 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 {
// Cada thread recebe o próprio handle para o mesmo Mutex.
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() devolve um guard; o lock é liberado quando o guard é descartado.
*counter.lock().unwrap() += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("contador = {}", *counter.lock().unwrap());
}

Saída:

contador = 4000
▶ Experimente você mesmo

Esta é a parte que os desenvolvedores Java adoram: se você esquece o synchronized no Java, o código compila e de vez em quando dá números errados. No Rust, se você tenta compartilhar um i32 ou um Vec simples entre threads e alterá-lo, o programa não compila. As data races são barradas pelo sistema de tipos (por meio dos traits Send e Sync). Para um único contador, um AtomicUsize seria mais leve que um mutex, exatamente como o AtomicInteger. Veja Concorrência e Estado compartilhado.

Todo desenvolvedor Java passa pelos mesmos buracos. Conhecer todos com antecedência deixa a queda menor.

  • Passar um valor para uma função o move. Em Java, passar um objeto passa uma referência, e você continua podendo usar a sua variável. Em Rust, passar uma String ou um Vec por valor o move para dentro da função:

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

    O compilador diz (em inglês) que name foi movido na linha 7 (“value moved here”) e usado de novo na linha 8 (“value used here after move”). A correção mais comum é deixar a função pegar emprestado: fn greet(name: &str), chamando com greet(&name). Leia primeiro Ownership; essa página explica essa regra, e a página do E0382 cobre esse erro em detalhes.

  • O borrow checker. Você pode ter muitos leitores (&T) ou um único escritor (&mut T) ao mesmo tempo, nunca os dois. Grafos de objetos normais no Java, como um filho que aponta de volta para o pai, ou muitos objetos guardando e alterando um mesmo objeto compartilhado, precisam de smart pointers (ponteiros inteligentes) ou de outro design. As primeiras semanas podem ser frustrantes. Fica mais fácil, de verdade.

  • Sem herança, sem protected, sem super. Você vai ficar tentado a recriar hierarquias de classes com traits. Experimente primeiro enums e composição; normalmente eles se encaixam melhor.

  • O == compara valores. Uma boa notícia, para variar: a == b com duas Strings compara o texto, então o clássico bug do == vs .equals() não tem como acontecer. Nas suas próprias structs, o == só funciona depois que você deriva ou implementa PartialEq.

  • O BinaryHeap tira primeiro o maior item. A PriorityQueue do Java dá o menor primeiro. No Rust, envolva os itens em std::cmp::Reverse para ter o comportamento de min-heap. Veja VecDeque e BinaryHeap.

  • O overflow de inteiros não é silencioso. Integer.MAX_VALUE + 1 dá a volta quietinho no Java. Num build de debug do Rust, ele causa um panic. Diga o que você quer com checked_add, wrapping_add ou saturating_add. Veja Overflow de inteiros.

  • String vs &str, e nada de charAt. As strings do Rust são UTF-8, então s[0] não compila. Use s.chars() para percorrer os caracteres. Funções normalmente recebem &str; structs normalmente guardam String. Veja Strings.

  • Sem sobrecarga de métodos. Você não pode ter dois métodos print com parâmetros diferentes. Use nomes diferentes (print, print_with_color), ou um parâmetro genérico.

  • Sem mágica em tempo de execução. Não há varredura do classpath, nem @Autowired, nem proxies colocando transações em volta dos seus métodos. Tudo é ligado explicitamente no código, e as macros rodam em tempo de compilação. Dá mais trabalho de digitar, mas você sempre consegue ver o que acontece.

  • Sem null e sem exceções. No começo, Option e Result em todo lugar parecem trabalho extra. Depois de algumas semanas, você percebe que não viu nenhuma NullPointerException nem um stack trace surpresa. Chamar .unwrap() é tranquilo enquanto você aprende, mas em código de verdade ele é a versão Rust de “deixa quebrar”.

  • Tempo de compilação. Um build limpo baixa e compila todas as dependências a partir do código-fonte, então é mais lento que o javac. Builds incrementais são bem mais rápidos, e o cargo check (que pula a geração de código) é o que você mais vai rodar enquanto edita. A boa notícia do outro lado: o programa inicia na hora, sem o aquecimento da JVM.

  • Async precisa de um runtime. Não existem virtual threads. Uma async fn não faz nada até que um executor a rode, então projetos web adicionam o tokio e colocam #[tokio::main] no main. Veja Async/Await.

Você não precisa ler este site na ordem. Este caminho pula o que o Java já ensinou a você e investe tempo onde o Rust é diferente:

  1. Começando com Rust e Ferramentas do Rust: instale o Rust, escolha um editor e rode cargo new.
  2. Variáveis e Tipos de dados: passe os olhos, principalmente para aprender let mut e os tipos numéricos.
  3. Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
  4. Strings: String vs &str.
  5. Structs e Enums, depois Match e Pattern matching.
  6. Option e Result, depois Erros personalizados e anyhow e thiserror.
  7. Traits, Genéricos, Traits comuns e Trait objects: as suas interfaces e classes abstratas.
  8. Closures e Iteradores: as suas lambdas e streams.
  9. Vetores, HashMap e VecDeque e BinaryHeap.
  10. Módulos, Cargo e crates e Testes em Rust.
  11. Lifetimes e Ponteiros inteligentes.
  12. Concorrência, Estado compartilhado, Canais (channels) e Async/Await.
  13. JSON com Serde, Logging e Bancos de dados em Rust, e depois construa o projeto de API web.
  14. Quer acelerar uma aplicação Java que já existe? Leia FFI: chamando C para ver como o Rust expõe funções extern "C".

Os dois exercícios trazem um pouco de Java. Converta para Rust.

Este código imprime os alunos aprovados, se alguém tirou a nota máxima e a melhor nota. Dica: os iteradores têm any (como o anyMatch) e max (que devolve um Option, como o OptionalInt). No lugar do Collectors.joining, colete num Vec<&str> e chame .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: " + passed);
System.out.println("perfect score: " + perfect);
System.out.println("best: " + best);
Ver solução
#[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!("aprovados: {}", passed.join(", "));
println!("nota máxima: {perfect}");
println!("melhor nota: {best}");
}

Saída:

aprovados: Ada, Grace, Ken
nota máxima: true
melhor nota: 100
▶ Experimente você mesmo

Os derives dão o que um record do Java dá de graça: uma string de depuração ({:?}), cópia (.clone()) e igualdade por valor (==). A função auxiliar student faz o papel do construtor do record. Diferente de um stream do Java, students.iter() pode ser chamado quantas vezes você quiser, porque cada chamada cria um iterador novo sobre o mesmo Vec.

Este método lê uma linha port=<número> de um arquivo de configuração. Ele lança uma ConfigException checked e embrulha a NumberFormatException que recebe do Integer.parseInt. Converta para que read_port devolva um Result<u16, String>. Dica: line.split_once('=') devolve um Option<(&str, &str)>; transforme em um Result com .ok_or_else(...). O .parse::<u16>() devolve um Result, e o .map_err(...) troca o erro dele pela sua própria mensagem, exatamente como embrulhar uma exceção em outra.

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("expected port=<number>, got '" + line + "'");
try {
return Integer.parseInt(parts[1].trim());
} catch (NumberFormatException e) {
throw new ConfigException("bad port number: " + parts[1].trim());
}
}
for (String line : List.of("port = 8080", "port=eighty", "host=localhost")) {
try { System.out.println("port is " + readPort(line)); }
catch (ConfigException e) { System.out.println("config error: " + e.getMessage()); }
}
Ver solução
fn read_port(line: &str) -> Result<u16, String> {
let expected = || format!("esperado port=<número>, recebido '{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!("número de porta inválido: {value}"))
}
fn main() {
for line in ["port = 8080", "port=eighty", "host=localhost"] {
match read_port(line) {
Ok(port) => println!("a porta é {port}"),
Err(e) => println!("erro de configuração: {e}"),
}
}
}

Saída:

a porta é 8080
erro de configuração: número de porta inválido: eighty
erro de configuração: esperado port=<número>, recebido 'host=localhost'
▶ Experimente você mesmo

A cláusula throws ConfigException virou o tipo de retorno Result, o try/catch em volta do parseInt virou um único map_err, e o throw virou Err(...). Usar u16 para a porta é um pequeno bônus: um número acima de 65535 é rejeitado pelo próprio parse, coisa que o int do Java não faria. A pequena closure expected só monta a mensagem de erro quando ela é necessária, como um supplier no orElseThrow(() -> ...).

  • Muita coisa do Java vem junto: genéricos, interfaces (como traits), lambdas (como closures) e pipelines de streams (como iteradores).
  • As grandes mudanças são ownership no lugar de um garbage collector, Option no lugar de null, Result e ? no lugar de exceções, e traits mais composição no lugar de herança.
  • Os genéricos são compilados para cada tipo (monomorfização), então não há type erasure nem boxing de números.
  • Dados compartilhados entre threads ficam dentro de um Mutex (envolvido num Arc), e esquecer de travar é um erro de compilação, não uma race.
  • O Cargo substitui o Maven ou o Gradle; o clippy e o rustfmt substituem o Checkstyle, o SpotBugs e os formatadores; os testes já vêm embutidos.
  • No lugar do Spring Boot, veja o axum ou o actix-web; no lugar do JPA, o SeaORM ou o Diesel; no lugar do JDBC e do jOOQ, o sqlx; e no lugar do Jackson, use o serde.

Teste o que aprendeu

1. Uma classe abstrata do Java tem um método abstrato e um método concreto que o chama. Qual é o substituto mais comum no Rust?

2. A PriorityQueue do Java devolve primeiro o menor item. O que o BinaryHeap::pop devolve no Rust?

3. Por que uma função genérica do Rust consegue chamar T::default(), enquanto o Java não consegue escrever new T()?

4. Você compartilha um Vec simples entre threads e tenta fazer push nele sem um Mutex. O que acontece no Rust?

Pontuação: 0 / 4

  • The Rust Programming Language (em inglês): o livro oficial. Os capítulos 4 (ownership), 9 (tratamento de erros), 10 (genéricos e traits) e 16 (concorrência) são os que mais importam para quem vem do Java.
  • std::collections (em inglês): o guia da própria biblioteca padrão para escolher entre Vec, VecDeque, HashMap, BTreeMap e as demais coleções.
  • JEP 454: Foreign Function & Memory API (em inglês): o recurso do JDK 22 para chamar bibliotecas nativas, inclusive as escritas em Rust, a partir do Java sem escrever código JNI.
  • O crate jni no docs.rs (em inglês): bindings Rust para o JNI, caso você precise chamar Rust a partir de JDKs mais antigos ou chamar o Java de volta.
  • Are We Web Yet? (em inglês): um panorama da comunidade sobre os frameworks web, as bibliotecas de banco de dados e mais do Rust, se você está procurando a sua stack no lugar do Spring Boot.