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.