Rust for Java developers, part 2

Where did we get so far?

In the first installment of this now series of Rust for Java developers, we saw some basic syntax, some pattern matching, briefly touched on enum and, most importantly, tried to reason about where the data in our programs resides.

When we quickly tried to save a line of code, we inadvertently changed the scope of an important resource because of RAII and the borrow checker gave us an error when we tried compiling the code:

$ rustc greeter.rs
error[E0716]: temporary value dropped while borrowed
 --> greeter.rs:6:43
  |
6 |     let anybody_at_all = env::args().collect::<Vec<String>>().get(1);
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^       - temporary value is freed at the end of this statement
  |                          |
  |                          creates a temporary which is freed while still in use
7 |
8 |     let who: &str = if let Some(one) = anybody_at_all {
  |                                        -------------- borrow later used here
  |

The dreaded borrow checker with borrow later used here is effectively telling us a reference (a borrow) to a value owned by, in this case, a temporary is used while the owner was freed. This is an error message too well known to developers trying out Rust coming from managed languages. Chances are that if you started playing with Rust yourself, you’ve came across such a message sooner or later on your adventure.

Java not only has the garbage collector to free memory only when it isn’t used, it also has the compiler that can optimize heap allocations away entirely transparently. This frees you from thinking about memory, about freeing it or where it is allocated: on the heap or the stack. Rust, on the other hand, pushes that decision of where and how to allocate memory on the developer. Which comes with great power, but also… yeah, responsibility. At least the compiler will not let you produce unsafe code. Which sometimes makes it feel as if the compiler won’t let you get your job done. This generally comes from the coding patterns not accounting for how the data "flows" through your program, because the JVM has spoiled us in not having to think about those things and create a large sea of object where any one can point to, well…​ everything. So what does Rust do?

Ownership and moves

We’ll start from where we left off in the previous post, i.e. a brand new Rust project, as created with cargo new. We’ll change the code a bit, in not too many surprising ways again, to get the first argument passed, if any, and greet:

use std::env;

fn main() {
    let who = env::args().nth(1).unwrap_or(String::from("World"));
    println!("Hello, {who}!");
}

Wait a second! This is nothing like we’ve seen previously? Why is all we did before now a one-liner? Well… yes. We could have made it easier to read. Or at least I hope this is easier to read. All we need now to get the second, .nth(1), item from the Args iterator, which still is an Option<String>; we then .unwrap to move the String out of the Option if there was Some argument provided to our program. On the other hand, _or if there was None, we create a heap allocated String instance from the argument provided to the method: "World": &str instead.

There is one new bit of syntax in this code above: String::from(), which is a function from() invoked on the String type. This would be the equivalent of a static method call. The from() we refer to here is to be found on the struct String. A struct holds related data in named fields, like a class in Java does. But we’ll get back to that in a bit.

So what’s this business with moving the String out of the Option::Some? As we saw in the previous post, the String has to live somewhere. That’s what Rust calls ownership. And there can only be one owner of any piece of data. If there was an argument passed in to our program, the call to env::args().nth(1) would return that String in an Option::Some variant, which is the owner of the said String. Which means that as soon as it goes out of scope, its content would be Drop -ped as well and the String would be freed. But we don’t want to deal with the Option indirection throughout our little program. We only care about having a String, whether it’s the argument passed in, or "World", we don’t really care beyond getting who to point the proper String for us to print. Which is what this .unwrap_or() does for us.

Let’s break the two individual bits down, first we’ll only deal with the Option<String> for simplicity’s sake, so let’s refactor our code to first get that:

let anybody_at_all: Option<String> = env::args().nth(1);
let who: String = anybody_at_all.unwrap_or(String::from("World"));

Now that we have our anybody_at_all: Option<String>, let’s assume that there is always an argument provided to our program. We can replace the .unwrap_or method call, by a straight .unwrap(), like this:

let who: String = anybody_at_all.unwrap();

This code would now panic in the absence of an argument actually being provided. Let’s try this out, first providing an argument, then omitting it:

$ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running `target/debug/rusty-java-2 Jane`
Hello, Jane!

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rusty-java-2`
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:5:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Right, so that’s bad, but let’s ignore this for now, we’ll get back to our .unwrap_or version soon enough. Focusing on the path that actually succeeds, calling anybody_at_all.unwrap() transfers the ownership of the String it had to the who: String variable. The String from the argument provided, i.e. "Jane" in this example, moved from the Option::Some that was our anybody_at_all to who. At which point you probably wonder what happened to anybody_at_all and you’d be right asking. Trial and error remains our approach here again: let’s try to .unwrap() the value again:

let anybody_at_all: Option<String> = env::args().nth(1);
let who: String = anybody_at_all.unwrap();
let _again: String = anybody_at_all.unwrap();

What would _again be? (starting a variable name with underscore tells the compiler we don’t plan on using it). It’s not a reference. It is also a String, as that’s what the Option contains. So would it be a copy of the original string? Let’s try and run this:

$ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
error[E0382]: use of moved value: `anybody_at_all`
   --> src/main.rs:6:26
    |
4   |     let anybody_at_all: Option<String> = env::args().nth(1);
    |         -------------- move occurs because `anybody_at_all` has type `Option<String>`, which does not implement the `Copy` trait
5   |     let who: String = anybody_at_all.unwrap();
    |                       -------------- -------- `anybody_at_all` moved due to this method call
    |                       |
    |                       help: consider calling `.as_ref()` or `.as_mut()` to borrow the type's contents
6   |     let _again: String = anybody_at_all.unwrap();
    |                          ^^^^^^^^^^^^^^ value used here after move
    |
note: this function takes ownership of the receiver `self`, which moves `anybody_at_all`
   --> .../lib/rustlib/src/rust/library/core/src/option.rs:775:25
    |
775 |     pub const fn unwrap(self) -> T {
    |                         ^^^^

For more information about this error, try `rustc --explain E0382`.
error: could not compile `rusty-java-2` due to previous error

Ok, that’s a scary error message: move occurs because …​ does not implement the Copy trait?! Well, no copy of the original String apparently. So much on that hypothesis. But we do see something did move! anybody_at_all moved says the error message! The Option itself did. Because of some fn unwrap(self) method we did indeed invoke. We’ll get back to the syntax later. But what happened is that the call did result in anybody_at_all being consumed, while it transferred ownership of the String it contained to who. So there is nothing we can do with that anybody_at_all, it "vanished" and can’t be used beyond line 5, where we did invoke .unwrap().

Ownership is transferred from one variable to another within the same scope in a very similar fashion:

let who: String = anybody_at_all.unwrap();
let moved: String = who;
println!("Hello, {who}!");
println!("Hello, {moved}!");

This would not compile, as we transfer the ownership of our String from the who variable to the moved one. As such who can not be used there after and println! can’t make use of it anymore.

What about the .unwrap_or(String::from("World")) call? It behaves exactly the same, other than it doesn’t panic if we’re dealing with a Option::None variant. In that case, it returns the argument provided instead. This is a generic type Option<T> and its unwrap_or takes a T as its default: T argument. Which means we have to pass it a String in this case. That’s why we have to instantiate a string on the heap, unlike what we did in the previous post and where we dealt with an &str instead. This program allocates a string on the heap for "World" always. This is an argument passed in to a method, and as such needs to be evaluated, which means the function String::from is always invoked. There is an alternative, running cargo clippy will actually recommend you use it, which makes use of Rust’s equivalent of Java’s lambda expression to lazily evaluate the default String to use and avoids unnecessarily allocate "World" on the heap if an argument was already provided. We’ll keep that for a later post though - feel free to try it though!

struct and borrowing

Now that we have a fairly good understanding of ownership and move, it’s time we look a little closer at borrowing. As we said quickly earlier a reference is a borrow of a value. We’ve been borrowing a fair amount in our previous post, as we dealt with a reference to a str from the loaded binary code, or a reference to the String that we got from the arguments passed to the executable. All those borrows were "read-only" ones. Meaning that we couldn’t ever mutate what the reference was pointing to. They come in opposition to &mut references, which are mutable references.

Here comes an important rule, that rustc will impose on us:

At any given time, you can have either one mutable reference or any number of immutable references.
— The Rust Book
The Rules of References

Which means that there can be zero or more immutable references "alive" at the same time, but there can only be zero immutable references if there is one (and only one!) mutable reference. This guarantees that no one reads a value, while it is being mutated. While this is useful for concurrent code, we’ll see in a bit that even single threaded code benefits from that guarantee.

But before we look into how this helps us eliminate an entire category of bugs from our code at compile time, rather than a random RuntimeException thrown at us in production, let us first look into how references and borrowing comes into play with struct and their impl blocks. First, how is a struct declared in Rust:

struct Greeter {
    person: String,
}

A struct in Rust is the equivalent of a class in Java. You first declare it using the struct keyword, followed by the name you want to give it. The fields it declares follow the Rust syntax we’ve seen so far: <name>: <type>. In the example above, the line declaring the person field is ended by a comma (,). This isn’t strictly necessary as it is the last (well the only) field in our Greeter struct. But fields that’d be declared before it would need the comma. It is considered good practice to always end field declaration with comma, so should you ever add a new one, the diff in your version control system would be cleaner.

For now our struct looks more like a C struct than a Java class. At this stage you probably expect a constructor and some methods. So let’s start with the former. In this particular case, you wouldn’t need a constructor per se, as since we put all this within the same file, visibility doesn’t matter. Which, as a matter of fact, is implicit in our examples, as the struct isn’t explicitly declared as pub. Neither is the field person. Which means that at any point in our main function, we could instantiate such as Greeter with the following syntax:

let greeter = Greeter {
    person: String::from("Jane"),
};

And, well, there is no constructor as you know them from Java in Rust neither. You could define a function on our struct Greeter that performs some argument manipulation for instance. Adding a function to a struct is done with an impl block for that same name. Below is an example of a Greeter::new function that creates a Person and returns it (the struct itself, not a reference to it):

impl Greeter {
    fn new(person: String) -> Self {
        Self {
            person,
        }
    }
}

Again, the function would be declared public with pub fn new, if we needed it exposed outside of this scope. We can see that the function takes ownership of a String, which means the caller would move the String ownership to this function. And that the function returns, denoted with the arrow a… Self?

Self is a placeholder for the type of the impl block. This is useful when refactoring, or when the type is somewhat verbose. But you could just as well have the method signature be fn new(person: String) → Greeter. Follows the block, within curly braces, that is the function’s body. When creating the Greeter this time though, we are not explicitly telling that the field person is to own the variable of the same name. Unlike when we previously did associate it with a freshly created String: person: String::from("Jane"). Them having the same name, is the reason we don’t have to prefix it with person:. It’s considered redundant and as such not necessary. Finally, the single line of the body again misses a ; at the end, as we are returning the freshly created Greeter instance to the caller, i.e. moving ownership of it to the caller. It is also worth mentioning that this freshly created struct was created on the stack. Even if the bytes of the person 's name are eventually on the heap, as this is a String.

So Greeter::new is again the equivalent of a static method in Java. How would you add an instance method then? First, let consider what an instance method even is. An instance method is one that acts on the instance of the object itself. What happens at runtime is that a reference to the instance is implicitly passed to a plain old function. Rust makes this a little more explicit. Below is the code that adds a .hi() method to the Greeter struct:

impl Greeter {
    fn new(person: String) -> Self {
        Self {
            person,
        }
    }

    fn hi(&self) {
        let who = self.person.as_str();
        println!("Hello, {who}!");
    }
}

A reference to… self? Lowercase self this time? Yes. That’s how Rust defines an instance method. The first argument being &self, which actually is a shorthand for self: &Self, so a binding named self that is a reference to a type of Self, i.e. Greeter. That first argument can either be a &self, a &mut self or a self. We’ve seen the self variant with our Option.unwrap() at the beginning, it’s an instance method that consumes the instance. So once invoked the binding used to invoke the method becomes useless. &mut self tells the compiler that we expect a mutable reference, because we’ll be mutating state related to this struct. One example would be a setter in Java, or e.g. a set_person method on our struct Greeter. In order to reassign a new provided String to our person field, the reference to would need to be mutable:

fn set_person(&mut self, person: String) {
    self.person = person;
}

If we’d declare it with &self, the compiler would complain as we’re trying to mutate self when assigning the new value to the person field.

$ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
error[E0594]: cannot assign to `self.person`, which is behind a `&` reference
  --> src/main.rs:32:9
   |
31 |     fn set_person(&self, person: String) {
   |                   ----- help: consider changing this to be a mutable reference: `&mut self`
32 |         self.person = person;
   |         ^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written

For more information about this error, try `rustc --explain E0594`.
error: could not compile `rusty-java-2` due to previous error

Below is our entire main.rs as it stands now with all these changes… and a few little ones more:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use std::env;

fn main() {
    let who = env::args().nth(1).unwrap_or(String::from("World"));

    let greeter = Greeter::new(who);
    greeter.hi();
    greeter.bye();
}

struct Greeter {
    person: String,
}

impl Greeter {
    fn new(person: String) -> Self {
        Self {
            person,
        }
    }

    fn hi(&self) {
        let who = self.person.as_str();
        println!("Hello, {who}!");
    }
    
    fn bye(&self) {
        println!("... and goodbye, {}!", &self.person);
    }    

    fn person(&self) -> &String {
        &self.person
    }

    fn set_person(&mut self, person: String) {
        self.person = person;
    }
}

We now also have a getter to the person field, that returns a reference as well as a bye() method. That method’s body is slightly different than from the hi() one that we saw before. They both do effectively the same: get a reference of the person to print their name. On line 23, in the hi() method, we get the self.person field and invoke the .as_str() which returns a &str for us to use, while on line 28 , within the println! macro invocation, we grab a reference & to the self.person, i.e. a &String. In both cases we do not move the self.person out of our struct. That move would be illegal, we only reference the field.

Which, when we run it, now yields this result:

❯ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target/debug/rusty-java-2 Jane`
Hello, Jane!
... and goodbye, Jane!

If we now use this struct to play a bit with borrows, we can test the rule of reference from before:

At any given time, you can have either one mutable reference or any number of immutable references.
— The Rust Book
The Rules of References

For instance what do you think would this do?

let mut greeter = Greeter::new(String::from("Jane"));

let reference = &greeter;
greeter.set_person(String::from("John"));
reference.hi();

Well, it would not compile, as it violates our simple rule:

let reference = &greeter;
                -------- immutable borrow occurs here
greeter.set_person(String::from("John"));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
reference.hi();
-------------- immutable borrow later used here

The issue becomes visible when you look at the method declaration of set_person(&mut self, person: Person). It takes a &mut self, so when invoking the method, we implicitly try to borrow greeter mutably. Which we cannot do because a immutable reference is still alive. If we would use greeter instead to invoke .hi() on, the immutable reference would be "dead" and the code would work…

What do you think would this other simple example does:

let mut greeter = Greeter::new(String::from("Jane"));

let person = greeter.person();
greeter.set_person(String::from("John"));
greeter.hi();
println!("Our previous person: {person}");

This one might be a little less obvious, as we are borrowing a reference to the person field of the greeter. But, thinking about it, you’ll probably not be surprised that the result if pretty much the same as before:

let person = greeter.person();
             ---------------- immutable borrow occurs here
greeter.set_person(String::from("John"));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
greeter.hi();
println!("Our previous person: {person}");
                                ------ immutable borrow later used here

The "transitivity" of the borrow of .person through greeter itself is tracked by the compiler and as such we get the same error back. We can’t have set_person replace the String within greeter with a new one, while we still have a reference to it. Replacing it would imply Drop -ping the old value while it is still being in use. Remember there is no garbage collector to only clean it up after the last reference is outlived.

You might want to use this very simple example to experiment with the lifecycle of references and see how the rule of references plays out for you. As a Java developer it does take some time to get used to this rule and think more of how your data is handled by your code. Rust requires a little more thinking as where and how to store data and references.

If now you’ve been wondering why this rule is there or might have felt that at times it is more constraining than actually helpful. Other than the reference to greeter.person being modified concurrently by another thread, there isn’t much added value to this guard than the freeing of the value… or is there? Let’s consider this simple Java code:

List<Integer> list = new ArrayList<Integer>();

list.add(1);
list.add(2);
list.add(3);

for (Integer i : list) {
    System.out.println(i);
    list.remove(0);
}

We’re iterating over a list, printing out the values and removing them as we go. An this keeps on happening in "real" code, unlike this idiotic example. If you’ve been doing Java for a while, this one is probably obvious to you already, but here is the output of running this:

1

Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
	at Program.main(Program.java:12)

Well, we didn’t get too far on this one! A ConcurrentModificationException, on a single thread! Now let’s try the same code in Rust:

let mut items = vec![1, 2, 3];

for i in &items {
    println!("{i}");
    items.remove(0);
}

Let’s first explain what is going on here. We’re creating a Vec<i32> from the handy vec! a function-like macro again. The type of the elements in the vector is i32, signed 32 bit integer - Java’s int, as this is what integer literal evaluate to in Rust.

We then iterate over the reference to our vec?! Well… no. The for loop there has some syntactic sugar added by the compiler. for i in items - without the ampersand - would be the equivalent of for i in items.into_iter(). Where into_iter() moves the ownership of the Vec from vec to the iterator. Eventually each item’s ownership is moved into the loop. While i in &items is the equivalent of i in items.iter(), which will provide us with references to each element of vec in our loop and leave the collection items untouched.

The loop block should be straightforward by now. So what happens if we run this?

for i in &items {
         ------
         |
         immutable borrow occurs here
         immutable borrow later used here
    println!("{i}");
    items.remove(0);
    ^^^^^^^^^^^^^^^ mutable borrow occurs here

Well, the rule of references will make it impossible for us to grab a mutable reference to our items, which we implicitly do by calling .remove which unsurprisingly takes a &mut self to the Vec to mutate it. All that while we are using an immutable reference to iterate over the elements within that Vec.

What about this code tho:

let mut items = vec![1, 2, 3];

for i in &items {
    for i in &items {
        println!("{i}");
    }
}

This does not violate our rule of references. While there is a keyword mut present, it is not on a reference, but on the variable which owns the Vec<i32>. We then go on to nest iterations, which results in at most two immutable references alive at the same time, which is not against our rule. If anything, this code will have the compiler produce warnings if, after these loops, we don’t mutate our items as then: variable does not need to be mutable and the first i isn’t used: if this is intentional, prefix it with an underscore.

So how are things Drop -ped then?

When an instance goes out of scope, the compiler will insert some code for us that is responsible for Drop -ping the instance. That means freeing up all resources it might hold on to: more instances, file descriptors, sockets, … and eventually free up the memory it used, like the memory on heap for our String instances.

Let’s start a little experiment in trying to understand how Rust deals with freeing up resources. We’ll build on this hi/bye "idiom" we’ve built with the Greeter, but change it slightly. We want it to only say "hi!" once and make sure it always says "bye". Let’s jump right into it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Scope {
    name: String,
}

struct TrackedScope {
    name: String,
}

impl Scope {
    fn new(name: &str) -> Self {
        Scope {
            name: String::from(name),
        }
    }

    fn start(self) -> TrackedScope {
        println!("Scope {} started", &self.name);
        TrackedScope {
            name: self.name,
        }
    }
}

impl Drop for TrackedScope {
    fn drop(&mut self) {
        println!("Scope {} ended!", &self.name);
    }
}

Ok, so what do we know already? We’re declaring two struct types: Scope and TrackedScope. They both have the same structure, containing a heap allocated String field called name.

The Scope field has one function and one method. The function is straight forward, but adds some nice usability to our previous Greeter, it takes a &str and converts it to the heap allocated String before creating the struct Scope for us. The method start is slightly more interesting though. It doesn’t take reference &self but consumes self instead! But before doing so, it prints that the scope is now started and then returns a TrackedScope moving the String instance from the Scope instance into the newly created TrackedScope one that is then returned. So the name is preserved, but the original Scope instance is dropped.

Talking about Drop! We also have an impl block for TrackedScope, but it implements the Drop trait. The trait, your Java interface equivalent, declares a single method fn drop(&mut self), which we implement here by printing that the scope has ended. That drop method invocation is what the compiler will add for us automatically in the right spot. You cannot invoke that method manually in your code yourself though, the compiler (again!) will prohibit that.

If you manually want to drop something, and as such invoke the .drop() on the instance, there is a handy utility function available in the standard library for you to use: std::mem::drop. You might be wondering how special that function is and what kind of magic it uses to drop the actual instance that you couldn’t possibly be trusted with?! Well, let’s take a look! Below it the full source code of this mysterious utility function:

pub fn drop<T>(_x: T) {}

Wait?! What? It’s… it’s empty!

Yes it is. The magic happens in the function signature again, it take ownership of the argument passed in! The method is generic over T, without any bounds, so it’d accept anything. And do absolutely nothing with it! But since, when invoking that function, the ownership of _x is moved to this scope and then… nothing, well… it gets dropped! The compiler sees it goes out of scope and insert the necessary cleanup code for you!

So let’s use our Scope now to see how the drop -ing occurs in code, considering these few cases below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let one = Scope::new("1");
let two = Scope::new("2");
let three = Scope::new("3");
let four = Scope::new("4");

let _one = one.start();
{
    two.start();
    let _three = three.start();
}
let _four = four.start();

Worth while to note is that we add a synthetic block on line 7, which ends on line 10. Also, the scope two is not bound to a variable when started. So what do you think the output should be? Let’s run this and see:

Scope 1 started
Scope 2 started
Scope 2 ended!
Scope 3 started
Scope 3 ended!
Scope 4 started
Scope 4 ended!
Scope 1 ended!

Probably unsurprisingly, three get dropped at the end of our block on line 10, right before four gets started. two though gets dropped right after it is started, as we never bind the returned TrackedScope to anything, so it can be cleaned up right away. While three only behaves similarly as it’s the last statement in that block. In the outer scope, we first started one then four, binding the result to a local variable and we can see that they get drop -ped in inverted order, first four, then one. Which makes sense. As a structure binding declared later could hold a reference to something declared before, hence dropping in reverse order cleans things up properly!

Let’s recap

That was quite some content again. We’ve by now explored the concept of ownership much deeper. How there is only one owner of any given piece of data. But that ownership can be transferred, otherwise known as move. We’ve then looked at how we can borrow a reference to data, according to the rule of references. And finally saw how the rust compiler frees resources by Drop -ping our struct instances.

We’ve indeed done most of this on struct by declaring our own new types and adding functions and methods to them. By doing this, we’ve seen how three different types of methods that can be declared on structs: &self, &mut self and self and how these differ in their behavior with regards to the borrow checker and move semantics.

Finally, we got a glimpse at how a for loop works in Rust and it differs from Java’s. We’ve also illustrated some of the usages of the above principles with code from the Rust standard library. Just like with Java, that source code is available for you to read and it is strongly encouraged that you spend some time reading some of it to get further acquainted with how the Rust standard library developer write code to leveraging the language solve some basic problems, like we did for std::mem::drop().

I can’t possibly end this, without reiterating one advice: run cargo clippy often on your code! Spend the time refactoring what you’ve got once it compiled. And re-run clippy! Spending some time exploring the language on your own terms is probably the best way to get a better handle on writing Rust! Coming from Java, chances are that becoming comfortable with where data should reside, i.e. which parts of your program own what data, and own to then efficiently share that data might be somewhat challenging initially. But I do believe that finding the path of least resistance to how data flows through your code will eventually lead to you writing better Java too.

There is much more for us to cover. So expect some new post some time this month with the next adventure into Rust coming from Java! I hope this was insightful and, again, please don’t hesitate to leave comments, remarks, suggestions, fixes, complains…​


This post is part of the series Rust for Java developers