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#.
Conceitos da linguagem: do Java para o Rust
Seção intitulada “Conceitos da linguagem: do Java para o Rust”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 |
Coleções
Seção intitulada “Coleções”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.
Ferramentas: do Maven e Gradle para o Cargo
Seção intitulada “Ferramentas: do Maven e Gradle para o Cargo”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.
Web e rede
Seção intitulada “Web e rede”| 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 |
Dados e mensageria
Seção intitulada “Dados e mensageria”| 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.
Blocos do dia a dia
Seção intitulada “Blocos do dia a dia”| 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 |
Desktop, jogos, Android e interoperabilidade
Seção intitulada “Desktop, jogos, Android e interoperabilidade”| 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 |
Lado a lado: uma classe abstrata vira um trait
Seção intitulada “Lado a lado: uma classe abstrata vira um trait”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êsLinus ganha 6000 por mêsUm 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.
Lado a lado: Optional vira Option
Seção intitulada “Lado a lado: Optional vira Option”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) {}
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 { } 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 cadastradoenviando para [email protected]Some("example.com")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.
Lado a lado: checked exceptions viram Result
Seção intitulada “Lado a lado: checked exceptions viram Result”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 20erro: faltam 30erro: o valor precisa ser maior que zeroRepare 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!.
Lado a lado: streams viram iteradores
Seção intitulada “Lado a lado: streams viram iteradores”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, AdaHelsinki: 200London: 120New York: 230Os 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.
Lado a lado: type erasure vs monomorfização
Seção intitulada “Lado a lado: type erasure vs monomorfização”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 bytesum bool ocupa 1 byteA 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.
Lado a lado: synchronized vira Arc e Mutex
Seção intitulada “Lado a lado: synchronized vira Arc e Mutex”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 = 4000Esta é 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.
O que vai fazer você tropeçar vindo do Java
Seção intitulada “O que vai fazer você tropeçar vindo do Java”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
Stringou umVecpor 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` trait7 | greet(name);| ---- value moved here8 | greet(name);| ^^^^ value used here after moveO compilador diz (em inglês) que
namefoi 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 comgreet(&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, semsuper. 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 == bcom duasStrings 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 implementaPartialEq. -
O
BinaryHeaptira primeiro o maior item. APriorityQueuedo Java dá o menor primeiro. No Rust, envolva os itens emstd::cmp::Reversepara ter o comportamento de min-heap. Veja VecDeque e BinaryHeap. -
O overflow de inteiros não é silencioso.
Integer.MAX_VALUE + 1dá a volta quietinho no Java. Num build de debug do Rust, ele causa um panic. Diga o que você quer comchecked_add,wrapping_addousaturating_add. Veja Overflow de inteiros. -
Stringvs&str, e nada decharAt. As strings do Rust são UTF-8, entãos[0]não compila. Uses.chars()para percorrer os caracteres. Funções normalmente recebem&str; structs normalmente guardamString. Veja Strings. -
Sem sobrecarga de métodos. Você não pode ter dois métodos
printcom 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
nulle sem exceções. No começo,OptioneResultem todo lugar parecem trabalho extra. Depois de algumas semanas, você percebe que não viu nenhumaNullPointerExceptionnem 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 ocargo 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 fnnão faz nada até que um executor a rode, então projetos web adicionam o tokio e colocam#[tokio::main]nomain. Veja Async/Await.
Uma trilha de estudo para desenvolvedores Java
Seção intitulada “Uma trilha de estudo para desenvolvedores Java”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:
- Começando com Rust e Ferramentas do Rust: instale o Rust, escolha um editor e rode
cargo new. - Variáveis e Tipos de dados: passe os olhos, principalmente para aprender
let mute os tipos numéricos. - Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
- Strings:
Stringvs&str. - Structs e Enums, depois Match e Pattern matching.
- Option e Result, depois Erros personalizados e anyhow e thiserror.
- Traits, Genéricos, Traits comuns e Trait objects: as suas interfaces e classes abstratas.
- Closures e Iteradores: as suas lambdas e streams.
- Vetores, HashMap e VecDeque e BinaryHeap.
- Módulos, Cargo e crates e Testes em Rust.
- Lifetimes e Ponteiros inteligentes.
- Concorrência, Estado compartilhado, Canais (channels) e Async/Await.
- JSON com Serde, Logging e Bancos de dados em Rust, e depois construa o projeto de API web.
- Quer acelerar uma aplicação Java que já existe? Leia FFI: chamando C para ver como o Rust expõe funções
extern "C".
Prática
Seção intitulada “Prática”Os dois exercícios trazem um pouco de Java. Converta para Rust.
1. Converta um record e um pipeline de stream
Seção intitulada “1. Converta um record e um pipeline de stream”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, Kennota máxima: truemelhor nota: 100Os 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.
2. Converta uma checked exception com causa
Seção intitulada “2. Converta uma checked exception com causa”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 é 8080erro de configuração: número de porta inválido: eightyerro de configuração: esperado port=<número>, recebido 'host=localhost'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,
Optionno lugar denull,Resulte?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 numArc), 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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- 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,BTreeMape 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.