Rust per sviluppatori Java: guida pratica
Se hai passato anni con Java, hai già quasi tutto quello che ti serve per imparare Rust. Ragioni per tipi. Conosci i generici, le interfacce, le lambda e le pipeline di stream. Rust ha tutto questo: i trait assomigliano molto alle interfacce con metodi predefiniti, gli iteratori si leggono come gli stream, e Cargo fa il lavoro di Maven o Gradle, solo con molta meno configurazione.
La novità è tutto quello che prima faceva la JVM al posto tuo. Rust non ha garbage collector né macchina virtuale. Il tuo codice viene compilato in un binario nativo, e il compilatore decide quando liberare la memoria controllando l’ownership (possesso) e il prestito (borrowing) prima che il programma parta. Rust inoltre non ha null (usi Option), non ha eccezioni (usi Result) e non ha ereditarietà tra classi. È su questi quattro cambiamenti che passerai la maggior parte del tempo di studio.
Perché gli sviluppatori Java si avvicinano a Rust? Di solito per uno di questi motivi: servizi che devono usare poca memoria, in modo stabile e senza pause del GC; strumenti da riga di comando e funzioni serverless che devono partire all’istante senza JVM; un percorso critico di un’app Java che vuoi spostare in codice nativo (Rust si può chiamare da Java tramite JNI o la più recente Foreign Function & Memory API); o semplicemente curiosità sul perché Rust continui a vincere i sondaggi sul «linguaggio più amato». Questa pagina mette in corrispondenza quello che conosci di Java con Rust, così puoi concentrarti sulle parti davvero diverse.
Se invece cerchi un confronto generale con C++, Go e Python, guarda Rust vs C++, Go e Python. Se conosci anche .NET, c’è una pagina a parte: Rust per sviluppatori C#.
Concetti del linguaggio: da Java a Rust
Sezione intitolata “Concetti del linguaggio: da Java a Rust”Usa questa tabella come un frasario. L’ultima colonna rimanda alla lezione di questo sito che spiega il lato Rust.
| Java | Rust | Per saperne di più |
|---|---|---|
class con campi e metodi |
struct per i dati + un blocco impl per i metodi |
Struct |
Costruttori e new |
Una funzione normale, per convenzione chiamata new, che restituisce Self |
Struct |
interface |
trait |
Trait |
Metodi default in un’interfaccia |
Metodi predefiniti in un trait (funzionano allo stesso modo) | Trait |
abstract class, extends, super |
Niente ereditarietà. Usa trait con metodi predefiniti per il comportamento condiviso e la composizione (una struct dentro una struct) per i dati condivisi | Trait, Design pattern |
List<Shape> con classi diverse |
Vec<Box<dyn Shape>> (un trait object) |
Trait object |
sealed interface + record + switch con pattern (Java 17/21) |
Un enum le cui varianti portano dati, più match |
Enum, match |
enum con campi e costruttori |
enum in cui ogni variante può contenere dati diversi |
Enum |
record Point(int x, int y) |
#[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } |
Struct, Trait comuni |
toString(), equals(), hashCode(), compareTo() |
Display / Debug, PartialEq / Eq, Hash, PartialOrd / Ord (di solito con derive) |
Trait comuni |
== (stesso oggetto) contro .equals() (stesso valore) |
== confronta sempre i valori. Non c’è la trappola dell’«uguaglianza di riferimento» |
Operatori |
null e Optional<T> |
Option<T>: o Some(valore) o 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 |
Eccezioni checked e throws |
Result<T, E> nel tipo di ritorno, più l’operatore ? |
Result, Errori personalizzati |
Eccezioni unchecked per veri bug (IllegalStateException) |
panic! (ferma il thread) |
Panic |
Concatenazione di eccezioni (new ConfigException(msg, cause)) |
map_err, o #[source] di thiserror e .context(...) di anyhow |
anyhow e thiserror |
try-with-resources e AutoCloseable |
Drop, che viene eseguito da solo quando il proprietario esce dallo scope |
Scope, Smart pointer |
| Garbage collector | Ownership e prestito, controllati in compilazione | Ownership, Prestito |
Variabili locali final |
Un semplice let (immutabile per default); scrivi let mut per permettere modifiche |
Variabili |
Costanti static final |
const |
Variabili |
Campi static modificabili, singleton |
static con LazyLock o OnceLock, avvolto in un Mutex se deve cambiare |
Stato condiviso |
Generici con <T extends Comparable<T>> |
Generici con vincoli di trait (trait bound): T: Ord o where T: Ord |
Generici |
Type erasure, List<Integer> invece di List<int> |
Monomorfizzazione: il codice generico viene compilato per ogni tipo, e Vec<i32> contiene numeri semplici |
Generici, Generici avanzati |
Lambda, Function<T, R>, Supplier<T>, Consumer<T> |
Closure, descritte dai trait Fn, FnMut, FnOnce |
Closure, Fn, FnMut e FnOnce |
Stream: filter, map, reduce, collect |
Adattatori di iteratori: filter, map, fold, sum, collect |
Iteratori |
Iterable<T> e Iterator<T> |
IntoIterator e Iterator (un solo metodo: next, che restituisce un Option) |
Iteratori personalizzati |
parallelStream() |
par_iter() del crate rayon |
Concorrenza |
Thread, ExecutorService |
std::thread::spawn e std::thread::scope; rayon o tokio per i pool |
Concorrenza |
synchronized, ReentrantLock |
Mutex<T>, che avvolge i dati che protegge |
Stato condiviso |
AtomicInteger, AtomicLong |
AtomicI32, AtomicI64, AtomicUsize |
Stato condiviso |
BlockingQueue tra thread |
Canali (std::sync::mpsc) |
Canali |
CompletableFuture, virtual thread |
async fn e .await, eseguiti da un runtime come tokio |
Async/await |
String (immutabile) e StringBuilder |
&str (una vista presa in prestito su un testo) e String (posseduta, può crescere) |
Stringhe |
char (un’unità UTF-16) |
char (qualsiasi carattere Unicode, 4 byte); le stringhe sono UTF-8 |
Stringhe |
int, long, double e Integer in un box |
i32, i64, f64, più i tipi senza segno da u8 a u128. Mai in un box |
Tipi di dati |
| Overflow degli interi silenzioso | Un panic nelle build di debug; scegli tu con checked_add, wrapping_add, saturating_add |
Overflow degli interi |
Cast come (int) x |
as per i numeri, From / TryFrom per le conversioni sicure |
Conversione di tipo, Conversioni |
BigDecimal, BigInteger |
I crate rust_decimal e num-bigint |
Tipi di dati |
Package e import |
Moduli (mod) e use |
Moduli |
public, private, protected, visibilità di package |
Privato per default; pub e pub(crate). Non esiste protected |
Moduli |
| Un JAR / artefatto Maven | Un crate (una libreria o un binario) | Cargo e crate |
Annotazioni (@Override, @Data di Lombok) |
Attributi come #[derive(Debug)] e #[test] |
Macro |
| Reflection, elaborazione delle annotazioni durante l’esecuzione | Quasi niente durante l’esecuzione. Al suo posto, le macro generano codice in compilazione | Macro procedurali |
| Overloading dei metodi | Niente overloading. Usa nomi diversi, o un parametro generico | Funzioni |
Controlli con instanceof e downcast |
match su un enum (il downcast con Any esiste, ma è raro) |
Pattern matching |
| JNI, Foreign Function & Memory API | Funzioni extern "C" |
FFI |
Collezioni
Sezione intitolata “Collezioni”Il Java Collections Framework corrisponde in modo pulito a std::collections. La differenza più grande: le collezioni di Rust non restituiscono mai null. Una ricerca ti dà un Option.
| Java | Rust | Per saperne di più |
|---|---|---|
ArrayList<T> |
Vec<T> |
Vettori |
int[], String[] |
[T; N] quando la lunghezza è nota in compilazione, altrimenti Vec<T>. Passa l’uno o l’altro come slice &[T] |
Array, Slice |
List.of(...) (non modificabile) |
Un array, o un Vec che passi in giro solo come &Vec<T> / &[T] (nessuno può modificarlo tramite un riferimento condiviso) |
Prestito |
HashMap<K, V> |
HashMap<K, V> (get restituisce Option<&V>) |
HashMap |
map.merge(word, 1, Integer::sum) |
*map.entry(word).or_insert(0) += 1 |
HashMap |
TreeMap<K, V> |
BTreeMap<K, V> (le chiavi restano ordinate) |
HashMap |
LinkedHashMap<K, V> (ordine di inserimento) |
IndexMap del crate indexmap |
|
HashSet<T>, TreeSet<T> |
HashSet<T>, BTreeSet<T> |
HashSet |
ArrayDeque<T> |
VecDeque<T> |
VecDeque e BinaryHeap |
Stack<T>, o un Deque usato come pila |
Vec<T> con push / pop |
Vettori |
PriorityQueue<T> (prima il più piccolo) |
BinaryHeap<T> (prima il più grande; avvolgi gli elementi in Reverse per avere prima il più piccolo) |
VecDeque e BinaryHeap |
LinkedList<T> |
LinkedList<T> esiste, ma Vec o VecDeque sono quasi sempre più veloci |
VecDeque e BinaryHeap |
ConcurrentHashMap<K, V> |
Mutex<HashMap<K, V>>, o il crate dashmap |
Stato condiviso |
list.sort(Comparator.comparing(...)) |
v.sort_by_key(...) o v.sort_by(...) |
Ordinamento e ricerca |
list.removeIf(...) |
v.retain(...) (tiene gli elementi che corrispondono) |
Vettori |
Come la HashMap di Java, anche una HashMap di Rust non ha un ordine fisso, e l’ordine può cambiare da un’esecuzione all’altra. Se ti serve un risultato ordinato, usa una BTreeMap o ordina prima le chiavi.
Strumenti: da Maven e Gradle a Cargo
Sezione intitolata “Strumenti: da Maven e Gradle a Cargo”La maggior parte degli sviluppatori Java si innamora presto di Cargo. C’è un solo strumento, un solo formato di configurazione e nessun XML di plugin. Build, dipendenze, test, documentazione, formattazione e lint arrivano tutti dallo stesso posto.
| Java | Rust | Note |
|---|---|---|
| Maven / Gradle | cargo |
Compila, esegue, testa, aggiunge dipendenze, pubblica. Vedi Cargo e crate |
pom.xml / build.gradle.kts |
Cargo.toml |
Breve, e per i progetti normali non servono script di build |
| Blocco delle dipendenze di Gradle | Cargo.lock |
Viene creato e aggiornato per te |
| Build multi-modulo | Un workspace Cargo | Un Cargo.toml in cima elenca i crate membri |
| Maven Central | crates.io | La documentazione delle API di ogni crate pubblicato è su docs.rs (in inglese) |
Aggiungere una <dependency> |
cargo add serde |
|
mvn archetype:generate / gradle init |
cargo new my_app (cargo new --lib per una libreria) |
|
mvn compile / mvn test / mvn package |
cargo build / cargo test / cargo build --release |
Le build di debug sono molto più lente in esecuzione; misura le prestazioni in release |
java -jar app.jar |
./target/release/app |
Un unico binario nativo. Nessuna JVM da installare sul server |
| Versioni del JDK, SDKMAN!, toolchain | rustup e un file rust-toolchain.toml |
Vedi Strumenti e configurazione |
javac |
rustc |
Lo chiama Cargo per te; raramente lo esegui a mano |
| JShell | Il Rust Playground nel browser, o evcxr_repl |
|
| Checkstyle, SpotBugs, PMD, Error Prone | cargo clippy |
Centinaia di lint ufficiali |
| google-java-format, Spotless | cargo fmt |
Un solo stile standard; nessuno discute sulle graffe |
| IntelliJ IDEA, Eclipse | JetBrains RustRover, o VS Code + rust-analyzer | rust-analyzer offre completamento, vai-alla-definizione ed errori in linea |
| Javadoc | Commenti di documentazione /// in Markdown + cargo doc |
Gli esempi di codice nella documentazione vengono eseguiti come test. Vedi Documentazione |
| JUnit 5 | #[test] integrato e cargo test (cargo nextest è un runner più veloce molto usato) |
Vedi Test |
@ParameterizedTest |
rstest |
|
| Mockito | mockall, o finti oggetti scritti a mano che implementano il tuo trait |
Vedi Test avanzati |
| AssertJ / Hamcrest | assert!, assert_eq! e pretty_assertions per differenze leggibili |
|
| WireMock, Testcontainers | wiremock, testcontainers |
Stesse idee, versioni Rust |
| JaCoCo | cargo llvm-cov o cargo tarpaulin |
|
| JMH | criterion |
Vedi Prestazioni |
| JFR, async-profiler, VisualVM | cargo flamegraph (si installa con cargo install flamegraph), perf |
Nessun GC o JIT da regolare |
| GraalVM native-image | Non serve | Ogni build di Rust è già codice nativo |
Ecco un piccolo Cargo.toml. Confrontalo con il pom.xml che ti servirebbe per le stesse tre dipendenze:
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Librerie e framework: «cosa usiamo al posto di…?»
Sezione intitolata “Librerie e framework: «cosa usiamo al posto di…?»”È la prima domanda che fa ogni team Java: «Usiamo Spring Boot e Hibernate. Qual è la versione Rust?» Rust non ha uno Spring, un unico framework che porta tutto con sé. Combini invece alcuni crate specializzati. Ecco gli equivalenti più vicini.
Web e rete
Sezione intitolata “Web e rete”| Java | Rust | Note |
|---|---|---|
| Spring Boot (Spring MVC / WebFlux) | axum (ne costruiamo una in Progetto: API web), actix-web, Rocket | axum e actix-web sono molto usati in produzione |
@RestController + @GetMapping |
Rotte di axum che puntano a semplici handler async fn |
Niente annotazioni né scansione del classpath; colleghi le rotte nel codice |
| Filtri delle servlet, interceptor di Spring | Layer di tower e il crate tower-http (CORS, compressione, tracing, timeout) |
axum usa tower direttamente |
Dependency injection di Spring (@Autowired) |
State condiviso in axum, costruttori semplici e trait |
Vedi la riga sulla DI più sotto |
| springdoc-openapi | utoipa |
|
| Thymeleaf | askama (template controllati in compilazione) o tera |
|
| Spring WebSocket | WebSocket in axum (la feature ws), o tokio-tungstenite |
|
| Spring for GraphQL | async-graphql |
|
| gRPC Java | tonic (con prost per protobuf) |
|
java.net.http.HttpClient, OkHttp, WebClient |
reqwest |
Asincrono per default, con un client bloccante opzionale |
| Netty | tokio + hyper |
|
| Spring Security (JWT, hashing delle password) | jsonwebtoken, argon2 |
Nessun framework di sicurezza tutto-in-uno |
Dati e messaggistica
Sezione intitolata “Dati e messaggistica”| Java | Rust | Note |
|---|---|---|
| Hibernate / JPA / Spring Data JPA | SeaORM: ORM asincrono con entità, relazioni e migrazioni | È il più simile a JPA; costruito sopra sqlx |
| Hibernate, se ti interessa una tipizzazione rigorosa | Diesel: ORM e query builder; le query vengono controllate sui tipi in compilazione | Maturo (2.x), sincrono per default (esiste diesel-async) |
JDBC, JdbcTemplate, jOOQ, MyBatis |
sqlx: SQL semplice, e la macro query! lo controlla sul tuo vero database in compilazione |
Asincrono, molto popolare, non è un ORM |
| Flyway / Liquibase | sqlx migrate (sqlx-cli), diesel migration, o refinery |
|
| Jedis / Lettuce (Redis) | redis |
|
| Client Kafka, Spring Kafka | rdkafka (basato su librdkafka) |
|
| Client Java di RabbitMQ | lapin |
|
| AWS SDK for Java | L’AWS SDK ufficiale per Rust: aws-config, aws-sdk-s3, … |
Se ti piacciono le entità JPA, prova prima SeaORM. Se preferisci scrivere SQL (il popolo di jOOQ e MyBatis), prova sqlx. Se vuoi le garanzie più forti in compilazione, prova Diesel. Database in Rust li mostra tutti e tre con codice funzionante.
I mattoni di tutti i giorni
Sezione intitolata “I mattoni di tutti i giorni”| Java | Rust | Note |
|---|---|---|
| Jackson / Gson | serde + serde_json |
Lo standard. Gestisce anche TOML, YAML e altro. Vedi JSON con Serde |
| JAXB, Jackson XML | quick-xml (con la sua feature serialize per serde) |
|
| SLF4J + Logback / Log4j 2 | tracing (+ tracing-subscriber), o log + env_logger |
Vedi Logging |
| Lombok | Derive integrati (Debug, Clone, PartialEq, Default, …); bon o derive_builder per i builder |
Nessun trucco sul bytecode; sono tutte macro normali |
| MapStruct | Implementazioni di From / Into |
Scritte a mano, controllate dal compilatore. Vedi Conversioni |
| Container IoC di Spring | Di solito niente: passa le dipendenze ai costruttori, usa trait per le cose che vuoi sostituire nei test | I progetti Rust usano raramente un container DI |
Bean Validation (@NotNull, @Size) |
validator o garde (validazione basata su derive) |
@NotNull non serve: usa Option solo dove un valore può mancare |
| Resilience4j | backon, o il middleware di retry e timeout di tower |
|
| Caffeine (cache) | moka |
|
| Guava, Apache Commons | Quasi sempre la libreria standard, più itertools |
|
java.time |
chrono o jiff |
Vedi Date e orari |
| picocli | clap |
Vedi Progetto: tool CLI con clap |
@Scheduled, Quartz |
tokio-cron-scheduler |
|
| Micrometer | metrics, o opentelemetry |
|
| Jakarta Mail | lettre |
Desktop, giochi, Android e interoperabilità
Sezione intitolata “Desktop, giochi, Android e interoperabilità”| Java | Rust | Note |
|---|---|---|
| JavaFX / Swing | Tauri (interfaccia web + backend Rust), Slint, egui, Iced |
Tauri 2 e Slint 1.x sono i più stabili. Vedi App con interfaccia grafica |
| LibGDX, jMonkeyEngine | Bevy, macroquad |
Bevy è prima della 1.0 e cambia le sue API tra una release e l’altra. Vedi Sviluppo di giochi |
| Codice di app Android | Tieni Kotlin o Java per l’interfaccia; metti la logica condivisa in una libreria Rust compilata con l’Android NDK | uniffi di Mozilla genera i binding Kotlin |
| Chiamare codice nativo da Java | Esponi funzioni extern "C" e chiamale tramite la FFM API (JDK 22+), o scrivi funzioni JNI con il crate jni |
Vedi FFI |
Fianco a fianco: una classe astratta diventa un trait
Sezione intitolata “Fianco a fianco: una classe astratta diventa un trait”Ecco una classica gerarchia Java: una classe base astratta con un metodo astratto e un metodo condiviso.
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());Rust non ha extends. Il metodo condiviso si sposta in un trait come metodo predefinito, e ogni struct tiene i propri dati:
trait Employee { fn name(&self) -> &str; fn monthly_pay(&self) -> u32;
// Un metodo predefinito: come il payslip() concreto della classe astratta. fn payslip(&self) -> String { format!("{} guadagna {} al mese", 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> fa la parte di «un riferimento a un Employee qualsiasi». 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()); }}Output:
Ada guadagna 5000 al meseLinus guadagna 6000 al meseUn trait non può contenere campi, quindi il protected name diventa un piccolo metodo name() che ogni struct implementa. All’inizio sembra codice in più da scrivere, ma elimina un’intera categoria di problemi: niente classi base fragili, niente chiamate a super da dimenticare, niente gerarchie profonde da scorrere. Se conosci i metodi default delle interfacce di Java 8, sai già come Rust condivide il comportamento. E quando l’insieme dei tipi è fisso (come una sealed interface in Java 17), un enum con un match è spesso più semplice di un trait.
Fianco a fianco: Optional diventa Option
Sezione intitolata “Fianco a fianco: Optional diventa Option”Optional di Java è stato un grande passo avanti, ma si usa solo per i valori di ritorno, e map.get() o qualsiasi campo possono ancora essere 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));L’Option di Rust ha quasi la stessa API, ma si usa ovunque un valore possa mancare, comprese le ricerche nelle collezioni. E dietro non si nasconde nessun 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(|| "nessuna email registrata".to_string()); println!("{shown}"); }
// ifPresent if let Some(email) = find_email(1) { println!("invio a {email}"); }
// flatMap si chiama and_then let domain = find_email(1).and_then(|email| email.split_once('@').map(|(_, d)| d.to_string())); println!("{domain:?}");}Output:
nessuna email registratainvio a [email protected]Some("example.com")Il compilatore non ti lascia chiamare .to_uppercase() su un Option<String> finché non hai gestito None, quindi la NullPointerException semplicemente non esiste. E Option non costa niente in più: un Option<&T> ha la stessa dimensione di un semplice riferimento. Impara i metodi di supporto nella pagina Option.
Fianco a fianco: le eccezioni checked diventano Result
Sezione intitolata “Fianco a fianco: le eccezioni checked diventano Result”Le eccezioni checked partivano da una buona idea: la firma ti dice cosa può andare storto. Ecco una tipica versione 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()); }Rust tiene la buona idea e butta via il resto. L’errore fa parte del tipo di ritorno (Result<u32, BankError>), e ? fa quello che fa throws: «se questa cosa è fallita, passa l’errore a chi mi ha chiamato». Di solito gli errori sono un enum, così un solo tipo può elencare tutti i modi in cui un’operazione può fallire:
use std::fmt;
#[derive(Debug)]enum BankError { InvalidAmount, InsufficientFunds { needed: u32 },}
// Display fa la parte di getMessage().impl fmt::Display for BankError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { BankError::InvalidAmount => write!(f, "l'importo deve essere maggiore di zero"), BankError::InsufficientFunds { needed } => write!(f, "mancano {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)?; // come chiamare un metodo che «lancia» println!("pagati {amount}, ne restano {left}"); Ok(left)}
fn main() { for (balance, amount) in [(100, 80), (50, 80), (100, 0)] { if let Err(e) = pay(balance, amount) { println!("errore: {e}"); } }}Output:
pagati 80, ne restano 20errore: mancano 30errore: l'importo deve essere maggiore di zeroNota cosa è sparito: niente blocchi try, niente srotolamento dello stack, niente eccezioni avvolte in RuntimeException solo per farle passare attraverso una lambda. Gli errori sono valori normali, quindi funzionano bene dentro le closure e le catene di iteratori. Il ? è un segno visibile in ogni punto in cui la funzione può uscire in anticipo. Scrivere Display a mano stanca presto; il crate thiserror lo genera per te, e anyhow dà alle applicazioni un tipo di errore pigliatutto (vedi anyhow e thiserror). Per i veri bug, quelli che segnaleresti con un’eccezione unchecked, Rust ha panic!.
Fianco a fianco: gli stream diventano iteratori
Sezione intitolata “Fianco a fianco: gli stream diventano iteratori”Una pipeline di stream con un collector 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));In Rust .iter() fa la parte di .stream(), e gli adattatori hanno nomi familiari:
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()): l'ordinamento si fa su un Vec, non dentro l'iteratore. 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}"); }}Output:
Linus, Ken, AdaHelsinki: 200London: 120New York: 230Come gli stream, anche gli iteratori di Rust sono pigri (lazy) e partono solo quando qualcosa li consuma (collect, sum, un ciclo for). A differenza degli stream, non c’è una classe Collectors: collect() guarda il tipo che chiedi (Vec, HashMap, String, …) e lo costruisce. Per raggruppare, la risposta abituale è un semplice ciclo con l’API entry; il crate itertools aggiunge funzioni di supporto se ti manca groupingBy. E le catene di iteratori vengono compilate nello stesso codice macchina di un ciclo scritto a mano, senza mettere int in un box lungo la strada. Leggi di più in Iteratori e Ordinamento e ricerca.
Fianco a fianco: type erasure contro monomorfizzazione
Sezione intitolata “Fianco a fianco: type erasure contro monomorfizzazione”I generici di Java esistono solo in compilazione. Durante l’esecuzione T viene cancellato, quindi non puoi scrivere new T(), e non puoi mettere un int in una List senza metterlo in un box:
static <T> List<T> filled(int n) { List<T> list = new ArrayList<>(); for (int i = 0; i < n; i++) list.add(new T()); // non compila: T viene cancellato return list;}
List<int> ids; // non compila nemmeno questo: serve List<Integer>Rust compila una copia separata di ogni funzione generica per ogni tipo con cui la usi (si chiama monomorfizzazione). Così la funzione sa esattamente cos’è T, e può chiamare T::default():
use std::mem::size_of;
// «T deve avere un valore predefinito ed essere clonabile»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:?}");
// Niente box: un Vec<i32> contiene semplici numeri da 4 byte, uno accanto all'altro. println!("un i32 occupa {} byte", size_of::<i32>()); println!("un bool occupa {} byte", size_of::<bool>());}Output:
[0, 0, 0] ["", ""] [false, false]un i32 occupa 4 byteun bool occupa 1 byteIl compromesso: più copie del codice significano binari più grandi e compilazioni più lunghe, ma ogni chiamata è diretta e può essere resa inline. Quando vuoi una sola copia condivisa (come in Java), usa un trait object come Box<dyn Trait>. I vincoli dopo i due punti (T: Default + Clone) sono la versione Rust di <T extends ...>. Vedi Generici e Generici avanzati.
Fianco a fianco: synchronized diventa Arc e Mutex
Sezione intitolata “Fianco a fianco: synchronized diventa Arc e Mutex”Quattro thread incrementano un contatore condiviso. In Java il lock sta sull’oggetto, e tocca a te ricordarti 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());In Rust il Mutex possiede il numero. Non c’è modo di raggiungerlo senza prima acquisire il lock. Arc (un puntatore con conteggio atomico dei riferimenti) permette a più thread di condividere il 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 { // Ogni thread riceve il suo handle allo stesso Mutex. let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { for _ in 0..1000 { // lock() restituisce una guardia; il lock viene rilasciato quando la guardia viene eliminata. *counter.lock().unwrap() += 1; } })); }
for handle in handles { handle.join().unwrap(); }
println!("conteggio = {}", *counter.lock().unwrap());}Output:
conteggio = 4000Ecco la parte che gli sviluppatori Java adorano: se in Java dimentichi synchronized, il codice compila e ogni tanto dà numeri sbagliati. In Rust, se provi a condividere tra thread un semplice i32 o un Vec e a modificarlo, il programma non compila. Le data race vengono intercettate dal sistema dei tipi (tramite i trait Send e Sync). Per un singolo contatore, un AtomicUsize sarebbe più leggero di un mutex, proprio come AtomicInteger. Vedi Concorrenza e Stato condiviso.
Le cose che ti faranno inciampare se vieni da Java
Sezione intitolata “Le cose che ti faranno inciampare se vieni da Java”Ogni sviluppatore Java prende le stesse buche. Conoscerle in anticipo le rende più piccole.
-
Passare un valore a una funzione lo sposta. In Java, passare un oggetto passa un riferimento, e puoi continuare a usare la tua variabile. In Rust, passare per valore una
Stringo unVeclo sposta dentro la funzione:fn greet(name: String) {println!("Ciao, {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 moveIl compilatore dice che
nameè stato spostato con la prima chiamata e poi usato di nuovo. La soluzione abituale è lasciare che la funzione prenda in prestito:fn greet(name: &str)e chiamala congreet(&name). Leggi prima Ownership: spiega questa regola, e E0382 tratta questo errore in dettaglio. -
Il borrow checker. Puoi avere molti lettori (
&T) oppure un solo scrittore (&mut T) alla volta, mai entrambi. Grafi di oggetti normali in Java, come un figlio che punta al genitore, o tanti oggetti che contengono e modificano un unico oggetto condiviso, richiedono smart pointer o un design diverso. Le prime settimane possono essere frustranti. Poi diventa davvero più facile. -
Niente ereditarietà, niente
protected, nientesuper. Sarai tentato di ricreare le gerarchie di classi con i trait. Prova prima con enum e composizione; di solito si adattano meglio. -
==confronta i valori. Una volta tanto, una buona notizia:a == bsu dueStringconfronta il testo, quindi il classico bug==contro.equals()non può capitare. Per le tue struct,==funziona solo dopo che hai fatto derive o implementatoPartialEq. -
BinaryHeapestrae l’elemento più grande. LaPriorityQueuedi Java ti dà prima il più piccolo. In Rust, avvolgi gli elementi instd::cmp::Reverseper avere il comportamento di un min-heap. Vedi VecDeque e BinaryHeap. -
L’overflow degli interi non è silenzioso.
Integer.MAX_VALUE + 1in Java ricomincia da capo senza dire niente. In una build di debug di Rust provoca un panic. Dichiara cosa vuoi conchecked_add,wrapping_addosaturating_add. Vedi Overflow degli interi. -
Stringcontro&str, e nientecharAt. Le stringhe di Rust sono UTF-8, quindis[0]non compila. Usas.chars()per scorrere i caratteri. Di solito le funzioni accettano&stre le struct contengonoString. Vedi Stringhe. -
Niente overloading dei metodi. Non puoi avere due metodi
printcon parametri diversi. Usa nomi diversi (print,print_with_color), o un parametro generico. -
Niente magia durante l’esecuzione. Niente scansione del classpath, niente
@Autowired, niente proxy che aggiungono transazioni attorno ai tuoi metodi. Tutto viene collegato esplicitamente nel codice, e le macro girano in compilazione. È più codice da scrivere, ma vedi sempre cosa succede. -
Niente
nulle niente eccezioni. All’inizioOptioneResultdappertutto sembrano lavoro in più. Dopo qualche settimana ti accorgerai che non vedi più unaNullPointerExceptiono uno stack trace a sorpresa. Chiamare.unwrap()va bene mentre impari, ma nel codice vero è la versione Rust di «lascia che vada in crash». -
Tempi di compilazione. Una build pulita scarica e compila tutte le dipendenze dal sorgente, quindi è più lenta di
javac. Le build incrementali sono molto più rapide, ecargo check(che salta la generazione del codice) è il comando che userai di più mentre scrivi. La buona notizia dall’altra parte: il programma parte all’istante, senza riscaldamento della JVM. -
L’async ha bisogno di un runtime. Non ci sono i virtual thread. Una
async fnnon fa niente finché un executor non la esegue, quindi i progetti web aggiungono tokio e mettono#[tokio::main]sumain. Vedi Async/await.
Un percorso di studio per sviluppatori Java
Sezione intitolata “Un percorso di studio per sviluppatori Java”Non devi leggere questo sito in ordine. Questo percorso salta quello che Java ti ha già insegnato e si sofferma dove Rust è diverso:
- Primi passi e Strumenti e configurazione: installa Rust, scegli un editor, esegui
cargo new. - Variabili e Tipi di dati: scorrile velocemente, soprattutto per imparare
let mute i tipi numerici. - Ownership, poi Prestito, poi Slice: le pagine più importanti per te. Prenditi il tuo tempo.
- Stringhe:
Stringcontro&str. - Struct ed Enum, poi match e Pattern matching.
- Option e Result, poi Errori personalizzati e anyhow e thiserror.
- Trait, Generici, Trait comuni e Trait object: le tue interfacce e classi astratte.
- Closure e Iteratori: le tue lambda e i tuoi stream.
- Vettori, HashMap e VecDeque e BinaryHeap.
- Moduli, Cargo e crate e Test.
- Lifetime e Smart pointer.
- Concorrenza, Stato condiviso, Canali e Async/await.
- JSON con Serde, Logging e Database in Rust, poi costruisci il progetto API web.
- Vuoi velocizzare un’app Java esistente? Leggi FFI per vedere come Rust espone funzioni
extern "C".
Pratica
Sezione intitolata “Pratica”Entrambi gli esercizi ti danno un po’ di Java. Portalo in Rust.
1. Porta un record e una pipeline di stream
Sezione intitolata “1. Porta un record e una pipeline di stream”Questo codice stampa gli studenti promossi, se qualcuno ha preso il punteggio pieno, e il punteggio migliore. Suggerimento: gli iteratori hanno any (come anyMatch) e max (che restituisce un Option, come OptionalInt). Al posto di Collectors.joining, raccogli in un Vec<&str> e chiama .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);Mostra la soluzione
#[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!("promossi: {}", passed.join(", ")); println!("punteggio pieno: {perfect}"); println!("migliore: {best}");}Output:
promossi: Ada, Grace, Kenpunteggio pieno: truemigliore: 100I derive ti danno quello che un record Java ti dà gratis: una stringa di debug ({:?}), la copia (.clone()) e l’uguaglianza per valore (==). La funzione di supporto student fa la parte del costruttore del record. A differenza di uno stream Java, puoi chiamare students.iter() tutte le volte che vuoi, perché ogni chiamata crea un nuovo iteratore sullo stesso Vec.
2. Porta un’eccezione checked con una causa
Sezione intitolata “2. Porta un’eccezione checked con una causa”Questo metodo legge una riga port=<numero> da un file di configurazione. Lancia una ConfigException checked, e avvolge la NumberFormatException che riceve da Integer.parseInt. Portalo in modo che read_port restituisca un Result<u16, String>. Suggerimento: line.split_once('=') restituisce un Option<(&str, &str)>; trasformalo in un Result con .ok_or_else(...). .parse::<u16>() restituisce un Result, e .map_err(...) sostituisce il suo errore con un tuo messaggio, proprio come avvolgere un’eccezione in un’altra.
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()); }}Mostra la soluzione
fn read_port(line: &str) -> Result<u16, String> { let expected = || format!("atteso port=<numero>, trovato '{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!("numero di porta non valido: {value}"))}
fn main() { for line in ["port = 8080", "port=eighty", "host=localhost"] { match read_port(line) { Ok(port) => println!("la porta è {port}"), Err(e) => println!("errore di configurazione: {e}"), } }}Output:
la porta è 8080errore di configurazione: numero di porta non valido: eightyerrore di configurazione: atteso port=<numero>, trovato 'host=localhost'La clausola throws ConfigException è diventata il tipo di ritorno Result, il try/catch attorno a parseInt è diventato un solo map_err, e il throw è diventato Err(...). Usare u16 per la porta è un piccolo regalo in più: un numero sopra 65535 viene rifiutato da parse stesso, cosa che un int in Java non farebbe. La piccola closure expected costruisce il messaggio di errore solo quando serve, come il supplier di un orElseThrow(() -> ...).
Riepilogo
Sezione intitolata “Riepilogo”- Molto di Java si porta dietro: generici, interfacce (come trait), lambda (come closure) e pipeline di stream (come iteratori).
- I grandi cambiamenti sono l’ownership al posto del garbage collector,
Optional posto dinull,Resulte?al posto delle eccezioni, e trait più composizione al posto dell’ereditarietà. - I generici vengono compilati per ogni tipo (monomorfizzazione), quindi non c’è type erasure e i numeri non finiscono in un box.
- I dati condivisi tra thread vivono dentro un
Mutex(avvolto in unArc), e dimenticare il lock è un errore di compilazione, non una race. - Cargo sostituisce Maven o Gradle; clippy e rustfmt sostituiscono Checkstyle, SpotBugs e i formattatori; i test sono integrati.
- Al posto di Spring Boot guarda axum o actix-web, al posto di JPA guarda SeaORM o Diesel, al posto di JDBC e jOOQ guarda sqlx, e al posto di Jackson usa serde.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Una classe astratta Java ha un metodo astratto e un metodo concreto che lo chiama. Qual è il sostituto abituale in Rust?
2. La PriorityQueue di Java restituisce prima l'elemento più piccolo. Cosa restituisce BinaryHeap::pop in Rust?
3. Perché una funzione generica di Rust può chiamare T::default(), mentre in Java non puoi scrivere new T()?
4. Condividi un semplice Vec tra thread e provi ad aggiungere elementi senza un Mutex. Cosa succede in Rust?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- The Rust Programming Language (in inglese): il libro ufficiale. I capitoli 4 (ownership), 9 (gestione degli errori), 10 (generici e trait) e 16 (concorrenza) sono i più importanti per chi viene da Java.
- std::collections (in inglese): la guida della libreria standard per scegliere tra
Vec,VecDeque,HashMap,BTreeMape le altre. - JEP 454: Foreign Function & Memory API (in inglese): la funzionalità del JDK 22 per chiamare da Java librerie native, comprese quelle in Rust, senza scrivere codice JNI.
- Il crate jni su docs.rs (in inglese): binding Rust per JNI, se devi chiamare Rust da JDK più vecchi o richiamare Java da Rust.
- Are We Web Yet? (in inglese): una panoramica della comunità su framework web, librerie per database e altro in Rust, se cerchi il tuo stack al posto di Spring Boot.