It offers a step-by-step guide through several core features, and how many of the concepts of Java translate to Rust. Like any programming language intended for real-life production usage, Rust offers far more than a single blog post can teach.
This post aims at giving a first overview of Rust for Java developers. Those interested in the details and further reading can find more documentation in the Rust book.
After all, syntax determines what you look at all day long, and it will influence how you approach a problem in subtle ways. So at its most basic the syntax of Rust should feel familiar for a Java developer.
This block tells us the struct is public, and has two (implicitly private) fields. From this definition, the Rust compiler knows enough to be able to generate an instance of the struct.
Self is a special type that can come in handy sometimes, especially once we start writing generic code. So, we need to provide a number of methods in order to fulfill the Named contract.
Unlike Java, we do not write these methods mixed with the inherent ones. Instead, we create a new top-level block containing only the methods of a single trait.
This constructs a type called Range which provides the required Incinerator implementation. While this does not completely match up with all the capabilities of the “init-check-update for loop”, it does very elegantly cover the most common use.
Roughly analogous to the switch expression in Java, match offers that functionality and more. Like Java switch, they allow selecting different values in a single, concise statement.
The older ones among you might have flashbacks to mallow/free, while the younger ones might scratch their heads on how the program is supposed to ever reclaim memory. Fortunately, there is a simple and elegant solution to the problem of when to destroy data in Rust.
Actually, it means something every Java developer probably finds intuitive: Your program reclaims memory once it has become unreachable. The key difference is that Rust does so immediately, instead of delaying it until a garbage collection.
There may be other references to the same string, in almost arbitrary parts of the programs' memory. If you pass a String to another function, store it in a struct, or otherwise transfer it anywhere, you lose access to it yourself.
Clone is a similar trait, but does require you to explicitly “confirm” that you want a potentially expensive copy by calling a method. In effect, copy and clone provide functions similar to the Cloneable interface of the JDK.
The ownership scheme described in the previous section may seem simple and intuitive, but it has one major consequence: How would you write a function that does something to an object you want to use in the future, ideally without shuffling megabytes of data across your memory? That means, as you are probably aware, that once you allocate an object “somewhere” you can use it in arbitrary ways.
The object lives long enough to ensure the references always remain valid. As explained in the previous chapter, Rust maintains a clear ownership of the object.
This allows the language to clean up an object immediately when it becomes unused. A reference is introduced by the ref keyword, but can also be declared in the type of variable.
Many Java developers will have run into the bug illustrated in this code snippet. An unexpected endless loop is usually harder to debug than a relatively clean exception.
Note that this code again sneakily introduces a new concept: but. This modifier announces that a variable or reference can alter values.
If your DEC is not mutable, this is also includes altering its content (usually, some exceptions exist). While this means you need to think a little more deeply about mutability on occasion, it at least prevents an UnsupportedOperationException.
In Java, this is not a problem, the garbage collector can ignore such internal references. In Rust, the outer RC is destroyed, but the inner keeps the object alive.
RC may want to protect us from altering the shared value (by only allowing an immutable reference). Nevertheless, Recall stands ready to break this rule and allow us to shoot ourselves in the foot.
If you prefer to work with multiple threads sharing data, you should use its close cousin Arc instead. In this snippet, we see this capability in action: We define a single function, which can be invoked with references to any number of types that implement the trait AMREF
Unfortunately, while this looks like it “should work” from the point of view of a Java developer, Rust has some additional constraints. All Java objects live on a large heap, and their true size is actually pretty hard to determine.
By using the type Box
While boxing an object is very much in the style of Java, Rust is not eager to use much heap. Frequently, developers do not want to change the type depending on some parameters, but instead just not expose such an implementation detail.
It does not expose the implementation type, but instead just says “I return something that you can use as the trait”, without going into detail what that something is. It knows and can optimize for the actual type, up to and including not doing a dynamic call at all.
Pretty much all Java developers know at least the basics of generics: They are what makes Collection ET. Without generics (and PRE- Java 5), all these types operated solely on objects.
Under the hood, they still do by removing all generic types and replacing them with the “upper bound”. Rust does not have a common super type like Object, but still has generic types (you have seen a few of them in this article already).
Since Rust does not have a “common super type”, it stands to reason that its approach must be different. Where Java creates the same code for all potential type parameters, Rust instead emits special code for each actual type parameter combination.
Remember there is no way to “extend a struct” in Rust, so only traits can constrain a type. Multiple traits can be demanded by simply specifying Trait1 + Trait2, much like the Java Interface1 & Interface2 notation.
However, since Rust traits are frequently much narrower than Java interfaces tend to be, you will encounter the plus-notation a lot more often. This may seem unexpected even to seasoned Java developers, but it has a good reason: Type erasure.
In contrast, Rust allows two implementations of the same trait with different type parameters. In Rust that is an intentional choice you can make, rather than a constraint of the language’s history.
But if we do need that knowledge, we can peek beneath the hood, and treat the associated type like a parameter. Before Java 8 came along, that required a dedicated (often anonymous) class, and a lot of notation.
Other than some fine points in notation (lack of braces, …) the Rust code looks very similar to what we would write in Java. Things do get somewhat more interesting, when we look at the underpinnings of “functional style” code.
Effectively, any interface that only lacks a default implementation for a single method can serve as the target for a lambda expression. The chief reason for this might be that the function receives ownership of an object, and destroys it once it completes.
You can call them multiple times, and they do not capture any (mutable) state. The first one (defined in invoke_with_once_closure) actively takes ownership of a variable, and thus is forced to implement the weakest of the three traits, Nonce.
This added complexity serves a rather simple purpose: Keeping track of what lives for how long. Imagine referencing a local variable in a closure, and having the containing block exit, thus destroying the value.
Thus, the closure would reference a dead value, and bad things would happen. Also note that this function uses the imply Trait return type previously discussed.
Any language worth its salt needs a user-friendly error handling strategy. The then-novel concept of exceptions takes center stage in its error handling strategy.
Generally speaking, a method will throw an Exception to signal an error condition. That aborts the execution of the current method, and “skips back” on the stack to a matching handler.
This is a very convenient model for the developer, only slightly hampered by the overhead of doing throws declarations. So it stands to reason that Rust would favor another way to handle errors over raising exceptions: Encoding the success or failure of an operation into the returned value.
In essence, the above code fragment expresses the same thing as this Java signature: The key difference here is that the failure does not propagate automatically up the stack: There is no need for special logic to find an exception handler.
This innocuous operator effectively serves as a shortcut to the statements above. Error types may be a simple as “something went wrong” to relatively complex structures with lots of helpful error-handling information.
It is not the preferred way to handle things in Rust, and mostly used for cases when the error is on the level of a failed assertion. Panics are a debugging tool and not the proper way to handle errors.
You can actually “catch” a panic if you employ some functions in the standard library, but there is usually little benefit in doing so. Currently, there are two chief approaches to this: (Thread-based) parallel computation, and concurrent execution.
Both have close relatives in Rust, and both have one major constraint: the ability to share data securely between threads. With Java, this has always been an open issue: You can always share References freely.
Whole classes of common errors are completely eliminated by the language rules, which is no small feat. For those who regularly truck with sun.misc. Unsafe, a peek at the unsafe sub-language in the Rustonomicon might get the creative juices flowing.