As part of some of the coding interviews I’ve been doing recently, invariance issues sometimes come up. I’m not overly dogmatic myself, but whenever mutable state is not needed, I try to get rid of code that causes variability, which is usually most obvious in data structures. However, there seems to be some misunderstanding about the concept of immutability, and developers often assume that having a final reference, or val in Kotlin or Scala, is enough to make an object immutable. This blog post delves into immutable references and immutable data structures.
Benefits of immutable data structures
Immutable data structures have significant advantages, such as:
- There is no invalid state
- Thread safety
- Easy to understand code
- Easier to test code
- Can be used for value types
There is no invalid state
When an object is immutable, it’s hard to invalidate an object. The object can only be instantiated through its constructor, which enforces the validity of the object. In this way, the parameters required for a valid state can be enforced. An example:
Address address = new Address();
address.setCity("Sydney");
// address is inInvalid state now, since the country Hasn't been set.address Address = new Address("Sydney"."Australia");
// Address is valid and doesn’t have setters, so the address object is always valid.
Copy the code
Thread safety
Because an object cannot be changed, it can be shared between threads without race conditions or data mutations.
Easy to understand code
Similar to the code example for invalid state, it is usually easier to use constructors than initialization methods. This is because the constructor enforces the required arguments, while the setter or Initializer methods are not enforced at compile time.
More testable code
Because objects are more predictable, it is not necessary to test all permutations of the initialization methods, that is, whether the object is valid or invalid when the constructor of the class is called. The rest of the code that uses these classes becomes more predictable and has fewer opportunities for NullPointerexceptions. Sometimes, some methods may change the state of an object when it is passed. Such as:
public boolean isOverseas(Address address) {
if(address.getCountry().equals("Australia") = =false) {
address.setOverseas(true); // address has now been mutated!
return true;
} else {
return false; }}Copy the code
In general, the above code is bad practice. It returns a Boolean value and may change the state of the object. This makes the code harder to understand and test. A better solution is to remove the setter from the Address class and return a Boolean value by testing the country name. A better approach is to move this logic to the Address class itself (address.isoverseas ()). When you do need to set the state, make a copy of the original object without changing the input.
Can be used for value types
Imagine the amount, say $10. Ten dollars will always be ten dollars. In code, this might look like public Money(final BigInteger amount, final Currency Currency). As you can see in this code, it is not possible to change the value of $10 to anything else, so the above can safely be used for value types.
Final references do not make the object immutable
As mentioned earlier, one of the problems I often run into is that a large percentage of these developers don’t fully understand the difference between final references and immutable objects. It seems that the common understanding among these developers is that the moment variables become final, data structures become immutable. Unfortunately, it’s not that simple, and I want to take this misconception out of the world once and for all:
A final reference does not make your objects immutable!
In other words, the following code does not make the object immutable:
final Person person = new Person("John");
Copy the code
Why not? Well, although Person is the last field and cannot be reassigned, the Person class might have a setter method or other mutator method that does something like this:
person.setName("Cindy");
Copy the code
Regardless of the final modifier, this is a very easy thing to do. Alternatively, the Person class might expose such a list of addresses. Accessing this list allows you to add addresses to it, so change the Person object as follows:
person.getAddresses().add(new Address("Sydney"));
Copy the code
Ok, now that we’ve solved this problem, let’s take a closer look at how we make classes immutable. When designing our class, we need to keep a few things in mind:
- Do not expose the internal state in a variable manner
- Do not change state internally
- Ensure that subclasses do not override the above behavior
Let’s design a better version of The Person Class based on the following guidelines.
Public final class Person {// Final class, can't be overridden by subclasses private final String name; // finalfor safe publication in multithreaded applications
private final List<Address> addresses;
public Person(String name, List<Address> addresses) {
this.name = name;
this.addresses = List.copyOf(addresses); // makes a copy of the list to protect from outside mutations (Java 10+).
// Otherwise, use Collections.unmodifiableList(new ArrayList<>(addresses));
}
public String getName() {
return this.name; // String is immutable, okay to expose
}
public List<Address> getAddresses() {
return addresses; // Address list is immutable
}
}
public final class Address { // final class, can’t be overridden by subclasses
private final String city; // only immutable classes
private final String country;
public Address(String city, String country) {
this.city = city;
this.country = country;
}
public String getCity() {
return city;
}
public String getCountry() {
returncountry; }}Copy the code
Now, you can use the following code:
import java.util.List;
final Person person = new Person("John", the List of (new Address (" Sydney ","Australia"));
Copy the code
Now, the code above is immutable, but because of the design of the Person and Address classes, along with the final reference, there is no way to reassign the Person variable to anything else.
Update: As some people have mentioned, the above code is still mutable because I didn’t copy the address list in the constructor. Therefore, if you do not call new in the ArrayList() constructor, you can still do the following:
final List<Address> addresses = new ArrayList<>();
addresses.add(new Address("Sydney"."Australia"));
final Person person = new Person("John", addressList);
addresses.clear();
Copy the code
However, because a new copy is created in the constructor, the code above will no longer affect the address list that is copied in the class to reference Person, making the code safe.
I hope this helps to understand the difference between finality and immutability. If you have any comments or feedback, let me know in the comments below.
dzone.com/articles/im…
See more articles
Public id: Galaxy 1
Contact email: [email protected]
(Please do not reprint without permission)