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 variable context 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
object mutable, 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
, y
, and z
, with 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 Tweet
, using 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.