Lewati ke konten

Rust untuk Developer Java: Panduan Praktis

Kalau kamu sudah bertahun-tahun bekerja dengan Java, kamu sudah punya sebagian besar bekal untuk belajar Rust. Kamu terbiasa berpikir dengan tipe. Kamu mengenal generics, interface, lambda, dan pipeline stream. Rust punya semuanya: trait terlihat sangat mirip interface dengan method default, iterator terbaca seperti stream, dan Cargo mengerjakan tugas Maven atau Gradle, hanya saja dengan konfigurasi yang jauh lebih sedikit.

Yang baru adalah semua hal yang dulu dikerjakan JVM untukmu. Rust tidak punya garbage collector dan tidak punya virtual machine. Kodemu di-compile menjadi binary native, dan compiler menentukan kapan memori dibebaskan dengan mengecek ownership (kepemilikan) dan borrowing (peminjaman) sebelum program berjalan. Rust juga tidak punya null (kamu memakai Option), tidak punya exception (kamu memakai Result), dan tidak punya class inheritance. Empat perubahan inilah yang akan menyita sebagian besar waktu belajarmu.

Kenapa developer Java belajar Rust? Biasanya karena salah satu alasan ini: layanan yang butuh pemakaian memori rendah dan stabil tanpa jeda GC; tool command-line dan serverless function yang harus langsung menyala tanpa JVM; bagian paling sibuk di aplikasi Java yang ingin kamu pindahkan ke kode native (Rust bisa dipanggil dari Java lewat JNI atau Foreign Function & Memory API yang lebih baru); atau sekadar penasaran kenapa Rust terus menduduki puncak survei “bahasa yang paling dikagumi”. Halaman ini memetakan apa yang sudah kamu kenal dari Java ke Rust, supaya kamu bisa fokus pada bagian yang benar-benar berbeda.

Kalau kamu mencari perbandingan umum dengan C++, Go, dan Python, lihat Rust vs C++, Go, dan Python. Kalau kamu juga mengenal .NET, ada halaman terpisah: Rust untuk Developer C#.

Pakai tabel ini seperti kamus percakapan. Kolom terakhir berisi link ke pelajaran di situs ini yang menjelaskan sisi Rust-nya.

Java Rust Pelajari lebih lanjut
class dengan field dan method struct untuk datanya + blok impl untuk method-nya Struct
Constructor dan new Fungsi biasa, yang menurut kebiasaan bernama new, yang mengembalikan Self Struct
interface trait Trait
Method default di interface Method default di trait (cara kerjanya sama) Trait
abstract class, extends, super Tidak ada inheritance. Pakai trait dengan method default untuk perilaku bersama, dan komposisi (struct di dalam struct) untuk data bersama Trait, Design pattern
List<Shape> berisi class yang berbeda-beda Vec<Box<dyn Shape>> (sebuah trait object) Trait object
sealed interface + record + switch dengan pattern (Java 17/21) enum yang varian-nya membawa data, plus match Enum, Match
enum dengan field dan constructor enum yang setiap varian-nya bisa menyimpan data yang berbeda Enum
record Point(int x, int y) #[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } Struct, Trait yang sering dipakai
toString(), equals(), hashCode(), compareTo() Display / Debug, PartialEq / Eq, Hash, PartialOrd / Ord (biasanya lewat derive) Trait yang sering dipakai
== (objek yang sama) vs .equals() (nilai yang sama) == selalu membandingkan nilai. Tidak ada jebakan “reference equals” Operator
null dan Optional<T> Option<T>: bisa Some(value) atau 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 exception dan throws Result<T, E> di tipe kembalian, plus operator ? Result, Error kustom
Unchecked exception untuk bug sungguhan (IllegalStateException) panic! (menghentikan thread) Panic
Exception chaining (new ConfigException(msg, cause)) map_err, atau #[source] dari thiserror dan .context(...) dari anyhow anyhow dan thiserror
try-with-resources dan AutoCloseable Drop, yang berjalan otomatis saat pemiliknya keluar dari scope Scope, Smart pointer
Garbage collector Ownership dan borrowing, dicek saat compile Ownership, Borrowing
Variabel lokal final let biasa (immutable secara default); tulis let mut supaya bisa diubah Variabel
Konstanta static final const Variabel
Field static yang bisa diubah, singleton static dengan LazyLock atau OnceLock, dibungkus Mutex kalau harus bisa berubah Shared state
Generics dengan <T extends Comparable<T>> Generics dengan trait bound: T: Ord atau where T: Ord Generik
Type erasure, List<Integer> alih-alih List<int> Monomorphization: kode generik di-compile untuk setiap tipe, dan Vec<i32> menyimpan angka biasa Generik, Generik lanjutan
Lambda, Function<T, R>, Supplier<T>, Consumer<T> Closure, yang dijelaskan oleh trait Fn, FnMut, FnOnce Closure, Fn, FnMut, dan FnOnce
Stream: filter, map, reduce, collect Iterator adapter: filter, map, fold, sum, collect Iterator
Iterable<T> dan Iterator<T> IntoIterator dan Iterator (satu method: next, yang mengembalikan Option) Iterator kustom
parallelStream() par_iter() dari crate rayon Konkurensi
Thread, ExecutorService std::thread::spawn dan std::thread::scope; rayon atau tokio untuk pool Konkurensi
synchronized, ReentrantLock Mutex<T>, yang membungkus data yang dilindunginya Shared state
AtomicInteger, AtomicLong AtomicI32, AtomicI64, AtomicUsize Shared state
BlockingQueue antar-thread Channel (std::sync::mpsc) Channel
CompletableFuture, virtual thread async fn dan .await, dijalankan oleh runtime seperti tokio Async/Await
String (immutable) dan StringBuilder &str (tampilan teks yang dipinjam) dan String (dimiliki, bisa bertambah panjang) String
char (satu unit UTF-16) char (karakter Unicode apa pun, 4 byte); string berformat UTF-8 String
int, long, double dan Integer yang di-box i32, i64, f64, plus unsigned u8 sampai u128. Tidak pernah di-box Tipe data
Integer overflow yang diam-diam Panic di build debug; pakai checked_add, wrapping_add, saturating_add untuk memilih perilakunya Integer overflow
Cast seperti (int) x as untuk angka, From / TryFrom untuk konversi yang aman Konversi tipe, Konversi
BigDecimal, BigInteger Crate rust_decimal dan num-bigint Tipe data
Package dan import Modul (mod) dan use Modul
public, private, protected, package-private Private secara default; pub dan pub(crate). Tidak ada protected Modul
JAR / artifact Maven Crate (library atau binary) Cargo dan crate
Annotation (@Override, @Data dari Lombok) Attribute seperti #[derive(Debug)] dan #[test] Makro
Reflection, pemrosesan annotation saat runtime Hampir tidak ada saat runtime. Sebagai gantinya, makro membuat kode saat compile Makro prosedural
Method overloading Tidak ada overloading. Pakai nama yang berbeda, atau parameter generik Fungsi
Pengecekan instanceof dan downcast match pada enum (downcast dengan Any memang ada, tapi jarang dipakai) Pattern matching
JNI, Foreign Function & Memory API Fungsi extern "C" FFI: memanggil C

Java Collections Framework terpetakan dengan rapi ke std::collections. Perbedaan terbesarnya: koleksi Rust tidak pernah mengembalikan null. Pencarian memberimu Option.

Java Rust Pelajari lebih lanjut
ArrayList<T> Vec<T> Vektor
int[], String[] [T; N] kalau panjangnya diketahui saat compile, selain itu Vec<T>. Berikan keduanya sebagai slice &[T] Array, Slice
List.of(...) (tidak bisa diubah) Array, atau Vec yang hanya kamu bagikan sebagai &Vec<T> / &[T] (tidak ada yang bisa mengubahnya lewat shared reference) Borrowing
HashMap<K, V> HashMap<K, V> (get mengembalikan Option<&V>) HashMap
map.merge(word, 1, Integer::sum) *map.entry(word).or_insert(0) += 1 HashMap
TreeMap<K, V> BTreeMap<K, V> (key-nya tetap terurut) HashMap
LinkedHashMap<K, V> (urutan penyisipan) IndexMap dari crate indexmap
HashSet<T>, TreeSet<T> HashSet<T>, BTreeSet<T> HashSet
ArrayDeque<T> VecDeque<T> VecDeque dan BinaryHeap
Stack<T>, atau Deque yang dipakai sebagai stack Vec<T> dengan push / pop Vektor
PriorityQueue<T> (yang terkecil duluan) BinaryHeap<T> (yang terbesar duluan; bungkus dengan Reverse agar yang terkecil duluan) VecDeque dan BinaryHeap
LinkedList<T> LinkedList<T> ada, tapi Vec atau VecDeque hampir selalu lebih cepat VecDeque dan BinaryHeap
ConcurrentHashMap<K, V> Mutex<HashMap<K, V>>, atau crate dashmap Shared state
list.sort(Comparator.comparing(...)) v.sort_by_key(...) atau v.sort_by(...) Mengurutkan dan mencari
list.removeIf(...) v.retain(...) (menyimpan item yang cocok) Vektor

Seperti HashMap di Java, HashMap di Rust tidak punya urutan tetap, dan urutannya bisa berubah dari satu kali jalan ke kali berikutnya. Kalau kamu butuh output yang terurut, pakai BTreeMap atau urutkan key-nya dulu.

Kebanyakan developer Java cepat jatuh cinta pada Cargo. Hanya ada satu tool, satu format konfigurasi, dan tidak ada XML plugin. Build, dependency, test, dokumentasi, format, dan lint semuanya datang dari tempat yang sama.

Java Rust Catatan
Maven / Gradle cargo Mem-build, menjalankan, mengetes, menambah dependency, mem-publish. Lihat Cargo dan crate
pom.xml / build.gradle.kts Cargo.toml Pendek, dan proyek biasa tidak butuh build script
Dependency locking Gradle Cargo.lock Dibuat dan diperbarui otomatis untukmu
Build multi-module Cargo workspace Satu Cargo.toml di level teratas mendaftar crate-crate anggotanya
Maven Central crates.io Dokumentasi API setiap crate yang dipublikasikan ada di docs.rs
Menambahkan <dependency> cargo add serde
mvn archetype:generate / gradle init cargo new my_app (cargo new --lib untuk library)
mvn compile / mvn test / mvn package cargo build / cargo test / cargo build --release Build debug jauh lebih lambat saat dijalankan; ukur performa di build release
java -jar app.jar ./target/release/app Satu binary native. Tidak perlu menginstal JVM di server
Versi JDK, SDKMAN!, toolchain rustup dan file rust-toolchain.toml Lihat Tools dan setup Rust
javac rustc Cargo memanggilnya untukmu; kamu jarang menjalankannya sendiri
JShell Rust Playground di browser-mu, atau evcxr_repl
Checkstyle, SpotBugs, PMD, Error Prone cargo clippy Ratusan lint resmi
google-java-format, Spotless cargo fmt Satu gaya standar; tidak ada yang berdebat soal kurung kurawal
IntelliJ IDEA, Eclipse JetBrains RustRover, atau VS Code + rust-analyzer rust-analyzer memberi completion, go-to-definition, dan error langsung di baris kode
Javadoc Doc comment /// dalam Markdown + cargo doc Contoh kode di dokumentasi dijalankan sebagai test. Lihat Dokumentasi
JUnit 5 #[test] bawaan dan cargo test (cargo nextest adalah runner populer yang lebih cepat) Lihat Testing
@ParameterizedTest rstest
Mockito mockall, atau fake buatan sendiri yang mengimplementasikan trait-mu Lihat Pengujian lanjutan
AssertJ / Hamcrest assert!, assert_eq!, dan pretty_assertions untuk diff yang mudah dibaca
WireMock, Testcontainers wiremock, testcontainers Ide yang sama, versi Rust
JaCoCo cargo llvm-cov atau cargo tarpaulin
JMH criterion Lihat Performa
JFR, async-profiler, VisualVM cargo flamegraph (diinstal dengan cargo install flamegraph), perf Tidak ada GC atau JIT yang perlu di-tuning
GraalVM native-image Tidak perlu Setiap build Rust sudah berupa kode native

Ini contoh Cargo.toml kecil. Bandingkan dengan pom.xml yang kamu butuhkan untuk tiga dependency yang sama:

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

Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?”

Section titled “Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?””

Inilah pertanyaan pertama setiap tim Java: “Kami pakai Spring Boot dan Hibernate. Di Rust ada apa?” Rust tidak punya Spring, tidak ada satu framework yang membawa segalanya. Sebagai gantinya, kamu menggabungkan beberapa crate yang fokus pada satu tugas. Berikut padanan yang paling dekat.

Java Rust Catatan
Spring Boot (Spring MVC / WebFlux) axum (kita membuatnya di Proyek: Web API dengan Axum), actix-web, Rocket axum dan actix-web banyak dipakai di production
@RestController + @GetMapping Route axum yang mengarah ke handler async fn biasa Tanpa annotation atau classpath scanning; kamu menyambungkan route di kode
Servlet filter, interceptor Spring Layer tower dan crate tower-http (CORS, kompresi, tracing, timeout) axum memakai tower secara langsung
Dependency injection Spring (@Autowired) State bersama di axum, constructor biasa, dan trait Lihat baris DI di bawah
springdoc-openapi utoipa
Thymeleaf askama (template dicek saat compile) atau tera
Spring WebSocket WebSocket di axum (fitur ws), atau tokio-tungstenite
Spring for GraphQL async-graphql
gRPC Java tonic (dengan prost untuk protobuf)
java.net.http.HttpClient, OkHttp, WebClient reqwest Async secara default, dengan client blocking opsional
Netty tokio + hyper
Spring Security (JWT, hashing password) jsonwebtoken, argon2 Tidak ada framework keamanan yang serba ada
Java Rust Catatan
Hibernate / JPA / Spring Data JPA SeaORM: ORM async dengan entity, relasi, dan migrasi Paling terasa mirip JPA; dibangun di atas sqlx
Hibernate dengan fokus pada tipe yang ketat Diesel: ORM dan query builder; query-mu dicek tipenya saat compile Matang (2.x), synchronous secara default (ada diesel-async)
JDBC, JdbcTemplate, jOOQ, MyBatis sqlx: SQL biasa, dan makro query! mengeceknya terhadap database sungguhan saat compile Async, sangat populer, bukan ORM
Flyway / Liquibase sqlx migrate (sqlx-cli), diesel migration, atau refinery
Jedis / Lettuce (Redis) redis
Client Kafka, Spring Kafka rdkafka (dibangun di atas librdkafka)
Client Java RabbitMQ lapin
AWS SDK for Java AWS SDK for Rust yang resmi: aws-config, aws-sdk-s3, …

Kalau kamu suka entity JPA, coba SeaORM dulu. Kalau kamu lebih suka menulis SQL (kubu jOOQ dan MyBatis), coba sqlx. Kalau kamu menginginkan jaminan compile-time yang paling kuat, coba Diesel. Database di Rust menunjukkan ketiganya dengan kode yang benar-benar jalan.

Java Rust Catatan
Jackson / Gson serde + serde_json Standarnya. Juga bisa TOML, YAML, dan lainnya. Lihat JSON dengan Serde
JAXB, Jackson XML quick-xml (dengan fitur serialize untuk serde)
SLF4J + Logback / Log4j 2 tracing (+ tracing-subscriber), atau log + env_logger Lihat Logging
Lombok Derive bawaan (Debug, Clone, PartialEq, Default, …); bon atau derive_builder untuk builder Tanpa trik bytecode; semuanya makro biasa
MapStruct Implementasi From / Into Ditulis manual, dicek oleh compiler. Lihat Konversi
IoC container Spring Biasanya tidak ada: berikan dependency lewat constructor, pakai trait untuk hal yang ingin kamu tukar saat test Proyek Rust jarang memakai DI container
Bean Validation (@NotNull, @Size) validator atau garde (validasi berbasis derive) @NotNull tidak diperlukan: pakai Option hanya di tempat yang nilainya bisa tidak ada
Resilience4j backon, atau middleware retry dan timeout dari tower
Caffeine (caching) moka
Guava, Apache Commons Sebagian besar standard library, plus itertools
java.time chrono atau jiff Lihat Tanggal dan waktu
picocli clap Lihat Proyek: CLI dengan clap
@Scheduled, Quartz tokio-cron-scheduler
Micrometer metrics, atau opentelemetry
Jakarta Mail lettre
Java Rust Catatan
JavaFX / Swing Tauri (UI web + backend Rust), Slint, egui, Iced Tauri 2 dan Slint 1.x yang paling stabil. Lihat Aplikasi GUI
LibGDX, jMonkeyEngine Bevy, macroquad Bevy belum 1.0 dan API-nya berubah di setiap rilis. Lihat Pengembangan game
Kode aplikasi Android Tetap pakai Kotlin atau Java untuk UI; taruh logika bersama di library Rust yang di-build dengan Android NDK uniffi dari Mozilla membuat binding Kotlin
Memanggil kode native dari Java Ekspos fungsi extern "C" dan panggil lewat FFM API (JDK 22+), atau tulis fungsi JNI dengan crate jni Lihat FFI: memanggil C

Berdampingan: abstract class menjadi trait

Section titled “Berdampingan: abstract class menjadi trait”

Ini hierarki Java klasik: abstract base class dengan satu method abstrak dan satu method bersama.

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 tidak punya extends. Method bersamanya pindah ke sebuah trait sebagai method default, dan setiap struct menyimpan datanya sendiri:

trait Employee {
fn name(&self) -> &str;
fn monthly_pay(&self) -> u32;
// Method default: seperti payslip() yang konkret di abstract class.
fn payslip(&self) -> String {
format!("{} digaji {} per bulan", 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> berperan sebagai "referensi ke suatu 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());
}
}

Output:

Ada digaji 5000 per bulan
Linus digaji 6000 per bulan
▶ Coba sendiri

Trait tidak bisa menyimpan field, jadi protected name berubah menjadi method kecil name() yang diimplementasikan oleh setiap struct. Awalnya terasa seperti mengetik lebih banyak, tapi ini menghilangkan sekelompok masalah: tidak ada fragile base class, tidak ada pemanggilan super yang terlupa, dan tidak ada hierarki dalam yang harus kamu telusuri. Kalau kamu mengenal method default di interface sejak Java 8, kamu sudah tahu cara Rust berbagi perilaku. Dan kalau himpunan tipenya tetap (seperti sealed interface di Java 17), sebuah enum dengan match sering lebih sederhana daripada trait.

Optional di Java adalah langkah maju yang besar, tapi ia hanya dipakai untuk nilai kembalian, dan map.get() atau field apa pun tetap bisa berisi 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));

Option di Rust punya API yang hampir sama, tapi ia dipakai di mana-mana sebuah nilai bisa tidak ada, termasuk pencarian di koleksi. Dan tidak ada null yang bersembunyi di belakangnya:

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(|| "email tidak terdaftar".to_string());
println!("{shown}");
}
// ifPresent
if let Some(email) = find_email(1) {
println!("mengirim ke {email}");
}
// flatMap di sini namanya and_then
let domain = find_email(1).and_then(|email| email.split_once('@').map(|(_, d)| d.to_string()));
println!("{domain:?}");
}

Output:

email tidak terdaftar
mengirim ke [email protected]
Some("example.com")
▶ Coba sendiri

Compiler tidak akan membiarkanmu memanggil .to_uppercase() pada Option<String> sebelum kamu menangani None, jadi NullPointerException memang tidak ada. Option juga tidak menambah biaya apa pun: Option<&T> berukuran sama dengan referensi biasa. Pelajari method-method pembantunya di halaman Option.

Berdampingan: checked exception menjadi Result

Section titled “Berdampingan: checked exception menjadi Result”

Pada dasarnya, checked exception punya ide yang bagus: signature-nya memberitahumu apa saja yang bisa salah. Ini versi Java yang biasa:

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 mempertahankan ide bagus itu dan membuang sisanya. Error-nya menjadi bagian dari tipe kembalian (Result<u32, BankError>), dan ? mengerjakan tugas throws: “kalau ini gagal, teruskan error-nya ke pemanggilku”. Error biasanya berupa enum, jadi satu tipe bisa mendaftar semua cara sebuah operasi bisa gagal:

use std::fmt;
#[derive(Debug)]
enum BankError {
InvalidAmount,
InsufficientFunds { needed: u32 },
}
// Display berperan sebagai getMessage().
impl fmt::Display for BankError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BankError::InvalidAmount => write!(f, "jumlahnya harus lebih dari nol"),
BankError::InsufficientFunds { needed } => write!(f, "kurang {needed} lagi"),
}
}
}
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)?; // seperti memanggil method yang "throws"
println!("dibayar {amount}, sisa {left}");
Ok(left)
}
fn main() {
for (balance, amount) in [(100, 80), (50, 80), (100, 0)] {
if let Err(e) = pay(balance, amount) {
println!("error: {e}");
}
}
}

Output:

dibayar 80, sisa 20
error: kurang 30 lagi
error: jumlahnya harus lebih dari nol
▶ Coba sendiri

Perhatikan apa yang hilang: tidak ada blok try, tidak ada stack unwinding, tidak ada exception yang dibungkus RuntimeException hanya supaya bisa lewat sebuah lambda. Error adalah nilai biasa, jadi ia berfungsi dengan baik di dalam closure dan rantai iterator. Tanda ? adalah penanda yang terlihat di setiap titik di mana fungsi bisa kembali lebih awal. Menulis Display secara manual cepat membosankan; crate thiserror membuatkannya untukmu, dan anyhow memberi aplikasi tipe error serba-tangkap (lihat anyhow dan thiserror). Untuk bug sungguhan, jenis yang di Java kamu tandai dengan unchecked exception, Rust punya panic!.

Pipeline stream dengan 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));

Di Rust, .iter() berperan sebagai .stream(), dan adapter-nya punya nama yang akrab:

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()): pengurutan dilakukan pada Vec, bukan di dalam iterator.
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, Ada
Helsinki: 200
London: 120
New York: 230
▶ Coba sendiri

Seperti stream, iterator Rust bersifat lazy dan hanya berjalan saat ada yang mengonsumsinya (collect, sum, perulangan for). Berbeda dengan stream, tidak ada class Collectors: collect() melihat tipe yang kamu minta (Vec, HashMap, String, …) lalu membuatnya. Untuk pengelompokan, perulangan biasa dengan entry API adalah jawaban yang umum; crate itertools menambahkan fungsi pembantu kalau kamu merindukan groupingBy. Dan rantai iterator di-compile menjadi kode mesin yang sama dengan perulangan yang ditulis manual, tanpa boxing int di sepanjang jalan. Baca selengkapnya di Iterator dan Mengurutkan dan mencari.

Berdampingan: type erasure vs monomorphization

Section titled “Berdampingan: type erasure vs monomorphization”

Generics di Java hanya ada saat compile. Saat runtime, T dihapus, jadi kamu tidak bisa menulis new T(), dan kamu tidak bisa memasukkan int ke List tanpa mem-box-nya:

static <T> List<T> filled(int n) {
List<T> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(new T()); // tidak bisa di-compile: T sudah dihapus
return list;
}
List<int> ids; // ini juga tidak bisa di-compile: kamu butuh List<Integer>

Rust meng-compile salinan terpisah dari setiap fungsi generik untuk setiap tipe yang kamu pakai (ini disebut monomorphization). Jadi fungsinya tahu persis apa T itu, dan bisa memanggil T::default():

use std::mem::size_of;
// "T harus punya nilai default dan bisa di-clone"
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:?}");
// Tanpa boxing: Vec<i32> menyimpan angka 4 byte biasa secara berdampingan.
println!("sebuah i32 memakan {} byte", size_of::<i32>());
println!("sebuah bool memakan {} byte", size_of::<bool>());
}

Output:

[0, 0, 0] ["", ""] [false, false]
sebuah i32 memakan 4 byte
sebuah bool memakan 1 byte
▶ Coba sendiri

Untung ruginya: lebih banyak salinan kode berarti binary yang lebih besar dan waktu compile yang lebih lama, tapi setiap pemanggilan bersifat langsung dan bisa di-inline. Kalau kamu memang ingin satu salinan bersama (seperti perilaku Java), pakai trait object seperti Box<dyn Trait>. Bound setelah titik dua (T: Default + Clone) adalah versi Rust dari <T extends ...>. Lihat Generik dan Generik lanjutan.

Berdampingan: synchronized menjadi Arc dan Mutex

Section titled “Berdampingan: synchronized menjadi Arc dan Mutex”

Empat thread menaikkan sebuah counter bersama. Di Java, lock-nya menempel pada objek, dan kamulah yang harus ingat menulis 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());

Di Rust, Mutex memiliki angkanya. Tidak ada cara untuk menyentuhnya tanpa mengunci lebih dulu. Arc (pointer dengan reference counting atomik) memungkinkan beberapa thread berbagi mutex yang sama:

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 {
// Setiap thread mendapat handle-nya sendiri ke Mutex yang sama.
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() mengembalikan guard; lock dilepas saat guard-nya di-drop.
*counter.lock().unwrap() += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("count = {}", *counter.lock().unwrap());
}

Output:

count = 4000
▶ Coba sendiri

Inilah bagian yang disukai developer Java: kalau kamu lupa synchronized di Java, kodenya tetap bisa di-compile dan sesekali memberi angka yang salah. Di Rust, kalau kamu mencoba berbagi i32 atau Vec biasa antar-thread lalu mengubahnya, programnya tidak bisa di-compile. Data race ditangkap oleh sistem tipe (lewat trait Send dan Sync). Untuk satu counter saja, AtomicUsize lebih ringan daripada mutex, sama seperti AtomicInteger. Lihat Konkurensi dan Shared state.

Hal-hal yang akan membuatmu tersandung sebagai developer Java

Section titled “Hal-hal yang akan membuatmu tersandung sebagai developer Java”

Setiap developer Java menabrak polisi tidur yang sama. Mengetahuinya lebih awal membuatnya terasa lebih kecil.

  • Memberikan nilai ke fungsi berarti memindahkannya. Di Java, memberikan objek berarti memberikan referensi, dan kamu masih bisa memakai variabelmu. Di Rust, memberikan String atau Vec secara by value memindahkan (move) nilai itu ke dalam fungsi:

    fn greet(name: String) {
    println!("Halo, {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

    Compiler memberitahumu bahwa name sudah dipindahkan ke greet di baris 7 (“value moved here”), jadi pemanggilan kedua di baris 8 tidak bisa memakainya lagi. Solusi yang biasa adalah membiarkan fungsinya meminjam: fn greet(name: &str) dan panggil dengan greet(&name). Baca Ownership dulu; halaman itu menjelaskan aturan ini, dan E0382 membahas error ini secara rinci.

  • Borrow checker. Pada satu waktu kamu boleh punya banyak pembaca (&T) atau satu penulis (&mut T), tidak pernah keduanya. Graf objek yang biasa di Java, seperti child yang menunjuk balik ke parent-nya, atau banyak objek yang memegang dan mengubah satu objek bersama, butuh smart pointer atau desain yang berbeda. Beberapa minggu pertama bisa bikin frustrasi. Setelahnya memang jadi lebih mudah.

  • Tidak ada inheritance, tidak ada protected, tidak ada super. Kamu akan tergoda membuat ulang hierarki class dengan trait. Coba enum dan komposisi dulu; biasanya keduanya lebih cocok.

  • == membandingkan nilai. Kali ini kabar baik: a == b pada dua String membandingkan teksnya, jadi bug klasik == vs .equals() tidak mungkin terjadi. Untuk struct buatanmu sendiri, == hanya bisa dipakai setelah kamu men-derive atau mengimplementasikan PartialEq.

  • BinaryHeap mengeluarkan item terbesar. PriorityQueue di Java memberimu yang terkecil duluan. Di Rust, bungkus item dengan std::cmp::Reverse untuk mendapatkan perilaku min-heap. Lihat VecDeque dan BinaryHeap.

  • Integer overflow tidak diam-diam. Integer.MAX_VALUE + 1 diam-diam berputar di Java. Di build debug Rust, ia panic. Nyatakan maksudmu dengan checked_add, wrapping_add, atau saturating_add. Lihat Integer overflow.

  • String vs &str, dan tidak ada charAt. String di Rust berformat UTF-8, jadi s[0] tidak bisa di-compile. Pakai s.chars() untuk menelusuri karakter. Fungsi biasanya menerima &str; struct biasanya menyimpan String. Lihat String.

  • Tidak ada method overloading. Kamu tidak bisa punya dua method print dengan parameter berbeda. Pakai nama yang berbeda (print, print_with_color), atau parameter generik.

  • Tidak ada sihir saat runtime. Tidak ada classpath scanning, tidak ada @Autowired, tidak ada proxy yang menambahkan transaksi di sekitar method-mu. Semuanya disambungkan secara eksplisit di kode, dan makro berjalan saat compile. Memang lebih banyak mengetik, tapi kamu selalu bisa melihat apa yang terjadi.

  • Tidak ada null dan tidak ada exception. Awalnya, Option dan Result di mana-mana terasa seperti kerja tambahan. Setelah beberapa minggu, kamu akan sadar sudah lama tidak melihat NullPointerException atau stack trace yang mengejutkan. Memanggil .unwrap() tidak apa-apa selama belajar, tapi di kode sungguhan itu versi Rust dari “biarkan saja crash”.

  • Waktu compile. Clean build menarik dan meng-compile semua dependency dari source, jadi lebih lambat daripada javac. Incremental build jauh lebih cepat, dan cargo check (yang melewati tahap pembuatan kode) adalah perintah yang paling sering kamu jalankan selagi mengedit. Kabar baiknya di sisi lain: programnya langsung menyala, tanpa pemanasan JVM.

  • Async butuh runtime. Tidak ada virtual thread. Sebuah async fn tidak melakukan apa-apa sampai ada executor yang menjalankannya, jadi proyek web menambahkan tokio dan memasang #[tokio::main] pada main. Lihat Async/Await.

Kamu tidak perlu membaca situs ini berurutan. Rute ini melewati apa yang sudah diajarkan Java kepadamu dan memberi waktu lebih untuk bagian Rust yang berbeda:

  1. Mulai dengan Rust dan Tools dan setup Rust: instal Rust, pilih editor, jalankan cargo new.
  2. Variabel dan Tipe data: baca sekilas saja, terutama untuk mengenal let mut dan tipe-tipe angka.
  3. Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
  4. String: String vs &str.
  5. Struct dan Enum, lalu Match dan Pattern matching.
  6. Option dan Result, lalu Error kustom dan anyhow dan thiserror.
  7. Trait, Generik, Trait yang sering dipakai, dan Trait object: interface dan abstract class versimu.
  8. Closure dan Iterator: lambda dan stream versimu.
  9. Vektor, HashMap, dan VecDeque dan BinaryHeap.
  10. Modul, Cargo dan crate, dan Testing.
  11. Lifetime dan Smart pointer.
  12. Konkurensi, Shared state, Channel, dan Async/Await.
  13. JSON dengan Serde, Logging, dan Database di Rust, lalu bangun proyek Web API.
  14. Ingin mempercepat aplikasi Java yang sudah ada? Baca FFI: memanggil C untuk melihat cara Rust mengekspos fungsi extern "C".

Kedua latihan memberimu sedikit kode Java. Porting kode itu ke Rust.

Kode ini mencetak siswa yang lulus, apakah ada yang mendapat nilai sempurna, dan nilai tertinggi. Petunjuk: iterator punya any (seperti anyMatch) dan max (yang mengembalikan Option, seperti OptionalInt). Untuk Collectors.joining, kumpulkan ke dalam Vec<&str> lalu panggil .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);
Lihat solusi
#[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!("lulus: {}", passed.join(", "));
println!("nilai sempurna: {perfect}");
println!("tertinggi: {best}");
}

Output:

lulus: Ada, Grace, Ken
nilai sempurna: true
tertinggi: 100
▶ Coba sendiri

Derive-nya memberimu apa yang diberikan record Java secara gratis: string debug ({:?}), menyalin (.clone()), dan kesetaraan nilai (==). Fungsi pembantu student berperan sebagai constructor milik record. Berbeda dengan stream Java, students.iter() bisa dipanggil berapa kali pun, karena setiap pemanggilan membuat iterator baru atas Vec yang sama.

Method ini membaca baris port=<number> dari file konfigurasi. Ia melempar checked ConfigException, dan membungkus NumberFormatException yang didapatnya dari Integer.parseInt. Porting kode ini supaya read_port mengembalikan Result<u16, String>. Petunjuk: line.split_once('=') mengembalikan Option<(&str, &str)>; ubah menjadi Result dengan .ok_or_else(...). .parse::<u16>() mengembalikan Result, dan .map_err(...) menukar error-nya dengan pesanmu sendiri, persis seperti membungkus satu exception di dalam exception lain.

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()); }
}
Lihat solusi
fn read_port(line: &str) -> Result<u16, String> {
let expected = || format!("seharusnya port=<angka>, tapi dapat '{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!("nomor port tidak valid: {value}"))
}
fn main() {
for line in ["port = 8080", "port=delapan puluh", "host=localhost"] {
match read_port(line) {
Ok(port) => println!("port-nya {port}"),
Err(e) => println!("error konfigurasi: {e}"),
}
}
}

Output:

port-nya 8080
error konfigurasi: nomor port tidak valid: delapan puluh
error konfigurasi: seharusnya port=<angka>, tapi dapat 'host=localhost'
▶ Coba sendiri

Klausa throws ConfigException menjadi tipe kembalian Result, try/catch di sekitar parseInt menjadi satu map_err, dan throw menjadi Err(...). Memakai u16 untuk port adalah bonus kecil: angka di atas 65535 sudah ditolak oleh parse sendiri, sesuatu yang tidak dilakukan int di Java. Closure kecil expected hanya membuat pesan error saat dibutuhkan, seperti supplier di orElseThrow(() -> ...).

  • Banyak hal dari Java tetap terpakai: generics, interface (sebagai trait), lambda (sebagai closure), dan pipeline stream (sebagai iterator).
  • Perubahan besarnya: ownership menggantikan garbage collector, Option menggantikan null, Result dan ? menggantikan exception, dan trait plus komposisi menggantikan inheritance.
  • Generics di-compile untuk setiap tipe (monomorphization), jadi tidak ada type erasure dan tidak ada boxing angka.
  • Data bersama antar-thread tinggal di dalam Mutex (dibungkus Arc), dan lupa mengunci adalah error compile, bukan race.
  • Cargo menggantikan Maven atau Gradle; clippy dan rustfmt menggantikan Checkstyle, SpotBugs, dan formatter; testing sudah bawaan.
  • Sebagai pengganti Spring Boot, lihat axum atau actix-web; untuk JPA, lihat SeaORM atau Diesel; untuk JDBC dan jOOQ, lihat sqlx; dan untuk Jackson, pakai serde.

Kuis singkat

1. Sebuah abstract class Java punya satu method abstrak dan satu method konkret yang memanggilnya. Apa pengganti yang biasa di Rust?

2. PriorityQueue di Java mengembalikan item terkecil duluan. Apa yang dikembalikan BinaryHeap::pop di Rust?

3. Kenapa fungsi generik di Rust bisa memanggil T::default(), sementara Java tidak bisa menulis new T()?

4. Kamu berbagi Vec biasa antar-thread dan mencoba menambahkan item ke dalamnya tanpa Mutex. Apa yang terjadi di Rust?

Skor: 0 / 4

  • The Rust Programming Language (bahasa Inggris): buku resmi Rust. Bab 4 (ownership), 9 (penanganan error), 10 (generics dan trait), dan 16 (konkurensi) yang paling penting untuk developer Java.
  • std::collections (bahasa Inggris): panduan dari standard library sendiri untuk memilih di antara Vec, VecDeque, HashMap, BTreeMap, dan lainnya.
  • JEP 454: Foreign Function & Memory API (bahasa Inggris): fitur JDK 22 untuk memanggil library native, termasuk library Rust, dari Java tanpa menulis kode JNI.
  • Crate jni di docs.rs (bahasa Inggris): binding Rust untuk JNI, kalau kamu perlu memanggil Rust dari JDK lama atau memanggil balik ke Java.
  • Are We Web Yet? (bahasa Inggris): ringkasan dari komunitas tentang framework web, library database, dan lainnya di Rust, kalau kamu sedang mencari pengganti stack Spring Boot-mu.