Skip to content

Rust programming for Java developers

Among the newer programming languages ​​growing in popularity is Rust. Rust was first introduced in 2010 and has quietly gained mindshare for its performance, syntax, and thread safety features. If you are a Java developer, you’ll find Rust relatively easy to get a grip on, thanks to the similarity of the two languages.

Rust has climbed the ladder of language popularity, or most commonly used languages, but most tellingly, Rust continually tops out as the the “most loved language” of all, according to the Stack Overflow survey. That is a testament to the great experience of using Rust.

Read on for a look at some of the main things to know about Rust if you’re coming from a Java background.

rust syntax

Like Java, Rust is compiled. It is compiled to the LLVM spec, similar in spirit to the JVM, allowing for output to a variety of target platforms.

And like Java, Rust descends from the C lineage. Its use of curly braces for blocks and semi-colons for line terminations is exactly the same as Java. For example, you can see a simple program here, like Listing 1.

Listing 1. Simple Rust code

fn main() {
println!("Hello, InfoWorld!");
}

Notice that there is a main()function, similar to the entry point in Java.

Functions in Rust

Functions stand alone in Rust, and they can be declared anywhere, including nested in other functions. This is unlike Java, where functions are always declared as methods on objects (except in the case of lambdas). Put another way, in Java everything is an object. Not so in Rust.

Listing 2. Using functions in Rust

fn main() {
println!("Hello, world!");

fn function2(){
println!("Hello InfoWorld");
}
function2();

function3();
}

fn function3() {
println!("Hello again.");
}

Implicit return values

Unlike Java, Rust allows you to skip the return keyword at the end of a function. The final statement in the function will automatically be evaluated as the return value. When doing this, you omit the semicolon from the final statement.

lambdas

Like Java, Rust supports lambdas for functional style coding. The syntax is different, but it’s not hard to understand if you are familiar with the Java streams API. Listing 3 shows the use of the map() function to make a set of strings uppercase. As you can see, it’s quite similar to Java.

Listing 3. Functional sorting with lambdas

// Rust
fn main() {
let animals = ["dog", "badger", "quokka"];

let result = animals.iter().map(|value| value.to_uppercase());

for animal in result {
println!("Uppercased: {}", animal);
}
}

the map() function takes a two-part argument. The first part is a variable inside the pipe characters, |value|, which will define the variable that is used as a handle on each item. The second part is the operation to execute. In this case, we call to_uppercase() on each element of the array.

Note that, like Java, Rust lambdas are closures that capture the state of the surrounding block. In other words, they have access to the context variable in which they execute.

Objects are structs in Rust

Have a look at Listing 4, which introduces the struct keyword. A struct, which is short for structure, allows you to define a container for data, just like the state part of a class in Java.

Listing 4. Using a Rust struct

struct Animal {
name: String
}
fn main() {
let dog = Animal{
name: String::from("Shiba")
};
println!("{}", dog.name);
}

You define the members of the struct inside the curly brace of the struct. These variables are analogous to public members.

Notice that in the line where you declare the dog variable, no call to a new keyword is necessary. Rust can deduce from the context that a new reference is in order.

Next, notice that the name variable is set at creation time to be a string with a value. This is done via calling the built-in String.from method using the double-colon reference operator.

Finally, notice that just like Java, Rust uses the dot operator to access the name field on the dog instance: dog.name.

methods

You can add functions to structs, and these functions behave in much the same way as methods in Java. For example, to add a speak() method to the Animal struct shown in Listing 4, you can use the impl keyword as seen in Listing 5.

Listing 5. Adding a method

impl Animal {
fn speak(&self) {
println!("{}", self.name);
}
}

Impl means implementation. Here in Listing 5, we are implementing the Animal struct. We define a single method, speak, that takes a single argument. This argument is the special &self pointer (the ampersand in Rust means the argument is a reference). This special pointer has very similar semantics to the this keyword in Java. It refers to the currently active object instance.

calling dog.speak() will output the name of the current instantiated object, which is "Shiba" in this example.

Mutability in Rust

One of the more curious things about Rust, if you’re coming from a Java background, is the default immutability of variables. In short, when you declare a variable in Rust, it is by default immutable, and attempts to alter it will result in an error.

To make a variable mutable, the mut keyword must be added, but mut can only be added by one reference at a time. Remember, Rust is highly concerned with keeping code thread-safe. This also avoids concurrent modification errors seen in Java.

Listing 6 shows how to make the dog mutable object, and then assign a new name to it.

Listing 6. A mutable string

let mut dog = Animal{
name: String::from("Shiba")
};
dog.name = String::from("Suki");
println!("{}", dog.name);

The key here is the mut keyword added to the declaration.

Type inference in Rust

In Rust, you don’t always have to tell the compiler what kind of variable you are declaring. This will seem odd for developers coming from Java, where there’s no facility for inferring the variable type. For example, in Listing 7, the compiler correctly infers the type to be integer.

Listing 7. Type inference example

let number1 = 10;
let number2 = 10;
println!("{}", number1 * number2);

Shadowing and variable names

Another Rust feature that may surprise a Java developer is what’s called variable shadowing. In essence, instead of declaring a variable as mutable, you can create a mask on top of it with the same name.

This is a kind of one-off mutability that creates a new space for the same variable name. In general, the ability to reuse the same variable name is different from Java. Listing 8 shows a simple example of variable shadowing.

Listing 8. Variable shadowing

fn main() {
    let x = 5;
    let x = x + 1;
    println!("The value of x is: {}", x); // outputs 6
}

The tuple type in Rust

Rust supports a tuple type, which is a kind of compound variable that doesn’t have a true analog in Java. Listing 9 shows you an example of a tuple in action.

Listing 9. Using the tuple type

fn main() {
    let myTuple = ("Sum", 10, 5);
    let (x, y) = myTuple ;
    println!("The {} is: {}", x, y + z);
}

Here you can see the myTuple variable is declared with the parentheses containing three values, a string and two integers. This is a tuple.

You can “destructure” the tuple into scalar variables as seen in the next line, where the let keyword is used to populate each variable, x, yand zwith the values ​​from the tuple.

You can also access the tuple members by index. For example, tup.0 references the first field on the tuple (the string "Sum").

Traits and generics in Rust

In Rust there is the concept of traits, which are similar to fine-grained interfaces in Java: They define what properties a type shares with other types. Put another way, traits abstract common functionality across different types.

Generics in Rust work similarly to those in Java, using a similar angle-bracket syntax, for addressing types in a general way based on their shared properties.

Take a look at Listing 10, which summarizes an example of using traits from the Rust manual.

Listing 10. Using a trait

pub trait Summary {
    fn summarize(&self) -> String;
}
pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn main() {
    let tweet = Tweet {
        username: String::from("dog_post"),
        content: String::from("A Shih Tzu is smaller than a Lurcher",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

here the trait keyword is used to define a Summary property, which is then implemented for each type, NewsArticle and Tweetusing the impl keyword. So this is very similar to an interface in Java, except that in Java an interface defines the surface of a whole class, instead of piecemeal defining methods.

A not so strange brew

Although this is a brief tour of some of the most salient points for a Java dev new to Rust, you can see that the language is not terribly hard to approach. It’s always good to keep an open mind about new technology, and Rust recommends itself with it’s consistent developer satisfaction ratings.

Copyright © 2022 IDG Communications, Inc.

Leave a Reply

Your email address will not be published.