But Java is still hugely popular and widely used in the enterprise, according to Oracle, which notes it is used by over 69% of full-time developers worldwide... It counts Arm, Amazon, IBM, Intel, NTT Data, Red Hat, SAP and Tencent among its list of notable contributors to JDK 15.
Oracle also gave a special mention to Microsoft and cloud system monitoring service Catalog for fixes... “In Media's opinion, the work Oracle began a few years ago in moving to a six-month update cycle and introducing a new level of modularity, puts the vendor in good stead with its constituency of approximately 12 million developers,” Oracle said in its report on Media's analysis.
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. There are two reasons for this: A struct can actually implement multiple traits with conflicting methods defined.
For (int i = 0; i < limit; i++) is a variant of the syntax we do not see on the Rust side. 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.
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.
Every scope cleans up after itself, and destroys all data that is no longer needed. 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.
Similarly, string1 moves out of the function in the return statement, and thus passes into the ownership of whoever called it. Clone is a similar trait, but does require you to explicitly “confirm” that you want a potentially expensive copy by calling a method.
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.
In exchange, we need to ensure these references do not dangle after the death of the object. 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. Java is fine with final Objects being altered internally.
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 Rust, the outer RC is destroyed, but the inner keeps the object alive. This is one of the exceptions to the “deep mutability” rule mentioned earlier.
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. Other than the decision to have a trait implementation being an independent block, they look almost exactly the same.
What about extends, the once-shining star of object-oriented programming that has fallen by the wayside over the years? This is called dynamic dispatch in Rust, and indicated by the Dan keyword.
The approach of “just passing a reference” works fine for dealing with parameters. 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. When you allocate a struct, you actually put that many bytes on the stack.
By using the type Box
The Box itself has a fixed size, and can clean up the heap-placed object properly. While boxing an object is very much in the style of Java, Rust is not eager to use much heap.
After all, keeping track of heap is comparatively slow and complex. 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. 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 in Java, the syntax is T extends S, Rust has a somewhat less wordy alternative: T: S. 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. If we do not need to know the type itself, we just write the trait bound as we usually would.
But if we do need that knowledge, we can peek beneath the hood, and treat the associated type like a parameter. They are essentially a shortcut to turn a function (method) into an object.
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.
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.
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. Java has decided to cut down the complexity by omitting the trickier cases of Input and Nonce.
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.
At the intro of this chapter, we mentioned that Rust does not like to produce backtracks or deal with “abrupt exits” of functions. 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.
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.