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#.
Konsep bahasa: dari Java ke Rust
Section titled “Konsep bahasa: dari Java ke Rust”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 |
Koleksi
Section titled “Koleksi”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.
Tooling: dari Maven dan Gradle ke Cargo
Section titled “Tooling: dari Maven dan Gradle ke Cargo”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.
Web dan networking
Section titled “Web dan networking”| 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 |
Data dan messaging
Section titled “Data dan messaging”| 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.
Komponen sehari-hari
Section titled “Komponen sehari-hari”| 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 |
Desktop, game, Android, dan interop
Section titled “Desktop, game, Android, dan interop”| 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 bulanLinus digaji 6000 per bulanTrait 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.
Berdampingan: Optional menjadi Option
Section titled “Berdampingan: Optional menjadi Option”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) {}
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 { } 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 terdaftarmengirim ke [email protected]Some("example.com")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 20error: kurang 30 lagierror: jumlahnya harus lebih dari nolPerhatikan 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!.
Berdampingan: stream menjadi iterator
Section titled “Berdampingan: stream menjadi iterator”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, AdaHelsinki: 200London: 120New York: 230Seperti 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 bytesebuah bool memakan 1 byteUntung 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 = 4000Inilah 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
StringatauVecsecara 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` trait7 | greet(name);| ---- value moved here8 | greet(name);| ^^^^ value used here after moveCompiler memberitahumu bahwa
namesudah dipindahkan kegreetdi 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 dengangreet(&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 adasuper. 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 == bpada duaStringmembandingkan teksnya, jadi bug klasik==vs.equals()tidak mungkin terjadi. Untuk struct buatanmu sendiri,==hanya bisa dipakai setelah kamu men-derive atau mengimplementasikanPartialEq. -
BinaryHeapmengeluarkan item terbesar.PriorityQueuedi Java memberimu yang terkecil duluan. Di Rust, bungkus item denganstd::cmp::Reverseuntuk mendapatkan perilaku min-heap. Lihat VecDeque dan BinaryHeap. -
Integer overflow tidak diam-diam.
Integer.MAX_VALUE + 1diam-diam berputar di Java. Di build debug Rust, ia panic. Nyatakan maksudmu denganchecked_add,wrapping_add, atausaturating_add. Lihat Integer overflow. -
Stringvs&str, dan tidak adacharAt. String di Rust berformat UTF-8, jadis[0]tidak bisa di-compile. Pakais.chars()untuk menelusuri karakter. Fungsi biasanya menerima&str; struct biasanya menyimpanString. Lihat String. -
Tidak ada method overloading. Kamu tidak bisa punya dua method
printdengan 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
nulldan tidak ada exception. Awalnya,OptiondanResultdi mana-mana terasa seperti kerja tambahan. Setelah beberapa minggu, kamu akan sadar sudah lama tidak melihatNullPointerExceptionatau 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, dancargo 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 fntidak melakukan apa-apa sampai ada executor yang menjalankannya, jadi proyek web menambahkan tokio dan memasang#[tokio::main]padamain. Lihat Async/Await.
Jalur belajar untuk developer Java
Section titled “Jalur belajar untuk developer Java”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:
- Mulai dengan Rust dan Tools dan setup Rust: instal Rust, pilih editor, jalankan
cargo new. - Variabel dan Tipe data: baca sekilas saja, terutama untuk mengenal
let mutdan tipe-tipe angka. - Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
- String:
Stringvs&str. - Struct dan Enum, lalu Match dan Pattern matching.
- Option dan Result, lalu Error kustom dan anyhow dan thiserror.
- Trait, Generik, Trait yang sering dipakai, dan Trait object: interface dan abstract class versimu.
- Closure dan Iterator: lambda dan stream versimu.
- Vektor, HashMap, dan VecDeque dan BinaryHeap.
- Modul, Cargo dan crate, dan Testing.
- Lifetime dan Smart pointer.
- Konkurensi, Shared state, Channel, dan Async/Await.
- JSON dengan Serde, Logging, dan Database di Rust, lalu bangun proyek Web API.
- Ingin mempercepat aplikasi Java yang sudah ada? Baca FFI: memanggil C untuk melihat cara Rust mengekspos fungsi
extern "C".
Latihan
Section titled “Latihan”Kedua latihan memberimu sedikit kode Java. Porting kode itu ke Rust.
1. Porting record dan pipeline stream
Section titled “1. Porting record dan pipeline stream”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, Kennilai sempurna: truetertinggi: 100Derive-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.
2. Porting checked exception dengan cause
Section titled “2. Porting checked exception dengan cause”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 8080error konfigurasi: nomor port tidak valid: delapan puluherror konfigurasi: seharusnya port=<angka>, tapi dapat 'host=localhost'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(() -> ...).
Ringkasan
Section titled “Ringkasan”- Banyak hal dari Java tetap terpakai: generics, interface (sebagai trait), lambda (sebagai closure), dan pipeline stream (sebagai iterator).
- Perubahan besarnya: ownership menggantikan garbage collector,
Optionmenggantikannull,Resultdan?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(dibungkusArc), 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
Section titled “Kuis singkat”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
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- 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.