Migrate to Rust from Java
Moving to Rust from Java can honestly be a daunting task. The two languages are quite different in terms of syntax, paradigms, and tooling. However, the transition can be made easier by understanding the similarities and differences between the two languages.
The Java language is a high-level, object-oriented programming language that is designed to be platform-independent. It used garbage collection to manage memory so developers don't have to worry about memory management.
Rust, on the other hand, is a systems programming language that is designed to be fast, but safe. It does not have a garbage collector, and instead uses a system of ownership, borrowing, and lifetimes to manage memory. Rust also has no concept of classes or inheritance, and instead uses traits and generics to achieve similar levels of code reuse.
main.java -> main.rs
Let's start by comparing a simple "Hello, World!" program in Java and Rust.
Java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Rust
fn main() { println!("Hello, World!"); }
As you can see, the Rust version is a bit more concise and does not require a class definition. The main
function is
the entry point of the program, and it does not require any arguments. So far, so good. 😄
Building and Running
In Java, you would compile the program using the javac
command and run it using the java
command.
javac Main.java
java Main
In Rust, you would use the rustc
command to compile the program and then run the resulting binary.
rustc main.rs
./main
Well... In reality, you would use something like Maven or Gradle to build your Java project, and Cargo to build your Rust project. But you get the idea.
Another thing to note is that Rust is a compiled language, while Java is an interpreted language. This means that Rust code is compiled into machine code that can be run directly on the target platform, while Java code is compiled into bytecode that is run on the Java Virtual Machine (JVM).
Building with cargo
Cargo is the build system and package manager for Rust. It makes it easy to build, test, and run Rust projects. It also handles dependencies and provides a consistent way to build and package Rust code.
To create a new Rust project, you can use the cargo new
command.
cargo new hello_world
cd hello_world
This will create a new directory called hello_world
with a basic Rust project structure. You can then edit the
src/main.rs
file to add your code, and use the cargo build
and cargo run
commands to build and run your project.
The binary that is produced by cargo build
is a debug binary by default. To build a release binary, you would use
the --release
flag. Debug binaries include debug information that can be useful for debugging, while release binaries
are optimized for size and/or speed and do not include debug information.
cargo build --release
Classes and Objects
In Java, classes are the building blocks of the language. They define the structure and behavior of objects, and can be used to create instances of objects.
In Rust, there are no classes. Instead, Rust uses structs to define data structures and traits to define behavior. Structs are similar to classes in that they can have properties and methods, but they do not support inheritance.
Here is an example of a simple class in Java and its equivalent in Rust:
Java
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Rust
#![allow(unused)] fn main() { struct Person { name: String, } impl Person { fn new(name: &str) -> Person { Person { name: name.to_string() } } fn get_name(&self) -> &str { &self.name } } }
Unit tests
In Java, unit tests are typically written using JUnit or TestNG. These frameworks provide annotations and utilities
for writing and running tests. Rust has built-in support for unit tests using the #[test]
attribute. Unlike Java,
where tests are typically written in separate classes, Rust tests are written in the same file as the code they are
testing.
Let's look at an example of a unit test in Java and its equivalent in Rust:
Java
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class PersonTest {
@Test
public void testGetName() {
Person person = new Person("Alice");
assertEquals("Alice", person.getName());
}
}
Rust
#![allow(unused)] fn main() { struct Person { name: String, } impl Person { fn new(name: &str) -> Person { Person { name: name.to_string() } } fn get_name(&self) -> &str { &self.name } } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_name() { let person = Person::new("Alice"); assert_eq!(person.get_name(), "Alice"); } } }
The tests
module is conditionally compiled only when running tests. The #[cfg(test)]
ensures that the subsequent
module is only compiled when running tests. This allows you to separate your tests from your production code, and
ensures that your tests do not end up in the final binary.