How to implement immutable classes in Java

Time:2019-11-27

Preface

Object oriented programming constructs readable code by encapsulating changeable parts, while functional programming constructs readable code by minimizing changeable parts.
– Michael feathers, author of working with legacy code

In this section, I discuss one of the cornerstones of functional programming: immutability. The state of an immutable object is immutable after its construction, in other words, the constructor is the only place where you can change the state of the object. If you want to change an immutable object, you will not change it, but use the modified value to create a new object and point your reference to it. (string is a typical example of an immutable class built in the Java language kernel.) Invariance is the key to functional programming, because it is consistent with the goal of minimizing the changing parts, which makes it easier to infer these parts.

Implementing immutable classes in Java

Modern object-oriented languages such as Java, ruby, Perl, groovy, and C ා all have built-in convenience mechanisms that make it easy to change state in a controlled way. However, state is such basic information for computing that you never know where it will go wrong. For example, due to the existence of a large number of changeable mechanisms, it is difficult to write high-performance and correct multithreaded code in an object-oriented language. Because Java has been optimized for manipulating state, you have to bypass some of these mechanisms to get some of the benefits of immutability. But once you understand some of the pitfalls to avoid, building immutable classes in Java becomes very easy.

Defining immutable classes

To construct a Java class into an immutable class, you must do the following:

  • Declare all domains as final.

After fields are defined as final in Java, you must initialize them at declaration time or in the constructor. If your ide complains that you didn’t initialize them when you declared them, don’t worry; when you write the appropriate code in the constructor, they realize that you know what you’re doing.

  • Declare the class final so that it is not overridden.

If you can override a class, you can override the behavior of its methods, so your safest option is not to subclass the class. Note that this is the strategy used by Java’s string class.

  • Do not provide a parameterless constructor.

If you have an immutable object, you must set any state that the object will contain in the constructor. If there is no state to set, what do you want an object to do? Static methods of stateless classes work just as well; therefore, you should never provide a parameterless constructor for an immutable class. If the framework you are using requires such a constructor for some reason, you can see if you can meet this requirement by providing a private parameterless constructor (which is visible through reflection).

It should be noted that the absence of parameterless constructors violates the JavaBeans standard, which insists on a default constructor. However, JavaBeans cannot be immutable in any case, which is determined by the way the setXXX method works.

  • Provide at least one constructor.

If you don’t provide a parameterless constructor, this is your last chance to add some state to the object!

  • No more mutable methods are provided except for constructors.

Not only do you want to avoid the typical JavaBeans inspired setXXX method, you also need to be careful not to return variable object references. The object reference is declared final, which is true, but that doesn’t mean you can’t change what it points to. Therefore, you need to make sure that you have copied defensively any object references returned from the getxxx method.

“Traditional” immutable class

Listing 1 lists an immutable class that meets the above requirements:

Listing 1. Immutable address class in Java


public final class Address {
private final String name;
private final List<String> streets;
private final String city;
private final String state;
private final String zip;
public Address(String name, List<String> streets, 
String city, String state, String zip) {
this.name = name;
this.streets = streets;
this.city = city;
this.state = state;
this.zip = zip;
}
public String getName() {
return name;
}
public List<String> getStreets() {
return Collections.unmodifiableList(streets);
}
public String getCity() {
return city;
}
public String getState() {
return state;
}
public String getZip() {
return zip;
}
}

Note that you can use the collections. Unmodifiablelist () method in Listing 1 to make a defensive copy of the streets list. You should always use collections instead of arrays to create immutable lists, although defensive array copying is possible, but it can have some undesirable side effects. Consider the code in Listing 2:

Listing 2. Customer class using array instead of collection


public class Customer {
public final String name;
private final Address[] address;
public Customer(String name, Address[] address) {
this.name = name;
this.address = address;
}
public Address[] getAddress() {
return address.clone();
}
}

When you try to do anything on the clone array returned from the getaddress() method call, the code problem in Listing 2 is exposed, as shown in Listing 3:

Listing 3. Tests that show the correct but not intuitive results


public static List<String> streets(String... streets) {
return asList(streets);
}
public static Address address(List<String> streets, 
String city, String state, String zip) {
return new Address(streets, city, state, zip);
}
@Test public void immutability_of_array_references_issue() {
Address [] addresses = new Address[] {
address(streets("201 E Washington Ave", "Ste 600"), "Chicago", "IL", "60601")};
Customer c = new Customer("ACME", addresses);
assertEquals(c.getAddress()[0].city, addresses[0].city);
Address newAddress = new Address(
streets("HackerzRulz Ln"), "Hackerville", "LA", "00000");
// doesn't work, but fails invisibly
c.getAddress()[0] = newAddress;
// illustration that the above unable to change to Customer's address
assertNotSame(c.getAddress()[0].city, newAddress.city);
assertSame(c.getAddress()[0].city, addresses[0].city);
assertEquals(c.getAddress()[0].city, addresses[0].city);
}

When returning a clone array, you protect the underlying array, but the array you return looks like a normal array, that is to say, you can modify the contents of the array (even if the variable holding the array is final, because this only works on the array reference itself, and does not work on the non array contents). When you use collections. Unmodifiablelist() (and a series of methods in collections for other types), you receive an object reference that does not change the availability of the method.

Clearer immutable classes

You may often hear that you should also declare an immutable domain as a private domain. After hearing someone clarify some deep-rooted assumptions with a different but clear view, I no longer agree with that view. In an interview with rich Hickey, creator of clojure, Michael fogus talked about the lack of data hiding encapsulation in many core parts of clojure. Clojure has been bothering me in this area because I am so obsessed with state-based thinking.

But after that, I realized that if domains are immutable, there is no need to worry about them being exposed. Many of the safeguards we use in encapsulation are actually to prevent changes. Once we have sorted out these two concepts, a clearer Java implementation emerges.

Consider the address class version in Listing 4:

Listing 4. Address class with public immutable domain


public final class Address {
private final List<String> streets;
public final String city;
public final String state;
public final String zip;
public Address(List<String> streets, String city, String state, String zip) {
this.streets = streets;
this.city = city;
this.state = state;
this.zip = zip;
}
public final List<String> getStreets() {
return Collections.unmodifiableList(streets);
}
}

When you want to hide the underlying representation, there are some benefits only when you declare the public getxxx () method for the immutable domain, but there are some obvious benefits during the refactoring, such as the ability to easily discover subtle changes. By declaring domains public or immutable, you can access them directly in your code without worrying about accidentally changing them.

At the beginning, it seems unnatural to use immutable fields. If you have heard the story of angry monkeys, you will know that this difference is actually beneficial: you are not used to dealing with immutable classes in Java, which looks like a new type, as shown in Listing 5:

Listing 5. Unit test for the address class


@Test (expected = UnsupportedOperationException.class)
public void address_access_to_fields_but_enforces_immutability() {
Address a = new Address(
streets("201 E Randolph St", "Ste 25"), "Chicago", "IL", "60601");
assertEquals("Chicago", a.city);
assertEquals("IL", a.state);
assertEquals("60601", a.zip);
assertEquals("201 E Randolph St", a.getStreets().get(0));
assertEquals("Ste 25", a.getStreets().get(1));
// compiler disallows
//a.city = "New York";
a.getStreets().clear();
}

The access to the public immutable domain avoids the visible overhead caused by a series of getxxx() calls. Note also that the compiler will not allow you to assign any value to any of these primitive types. If you try to call the variable methods on the street collection, you will receive an unsupported operation exception (captured at the top of the test). The use of this code style gives a strong visual indication that the class is immutable.

Adverse aspects

One possible disadvantage of this clearer syntax is that it takes some effort to learn this new programming technique, but I think it’s worth it: this process will promote you to think about invariance when you create a class, because the style of the class is so obviously different, and unnecessary boilerplate code is deleted. However, this code style in Java also has some disadvantages (to be fair, the direct purpose of Java is never to cater to invariance):

1. As Glenn vanderburg pointed out to me, the biggest drawback is that this style violates Bertrand Meyer’s unified access principle: all services provided by the module should be used through a unified notation, no matter whether the services are realized through storage or calculation, they cannot be used Against this notation. In other words, access to a domain should not reveal whether the domain is a domain or a method that returns a value. The getstreets() method of the address class is not consistent with other domains. This problem cannot be solved in Java, but it has been solved by implementing invariance in some other JVM languages.

2. Some frameworks that rely heavily on reflection cannot work with this programming technique because they need a default constructor.

3. Because you are creating new objects instead of changing the original ones, a large number of updated systems may lead to inefficiency in garbage collection. Languages like clojure have built-in tools to make this situation more efficient by using immutable references, which is the default practice in these languages.

Immutability in groovy

You can use groovy to build a public immutable domain version of the address class, which brings a very clear implementation, as shown in Listing 6:

Listing 6. Immutable address class written in groovy


class Address {
def public final List<String> streets;
def public final city;
def public final state;
def public final zip;
def Address(streets, city, state, zip) {
this.streets = streets;
this.city = city;
this.state = state;
this.zip = zip;
}
def getStreets() {
Collections.unmodifiableList(streets);
}
}

As always, groovy requires less boilerplate code than Java, and provides some other benefits. Because groovy allows you to create properties using the familiar get / set syntax, you can create truly protected properties for object references. Consider the unit tests shown in Listing 7:

Listing 7. Unit tests show unified access in groovy


class AddressTest {
@Test (expected = ReadOnlyPropertyException.class)
void address_primitives_immutability() {
Address a = new Address(
["201 E Randolph St", "25th Floor"], "Chicago", "IL", "60601")
assertEquals "Chicago", a.city
a.city = "New York"
}
@Test (expected=UnsupportedOperationException.class)
void address_list_references() {
Address a = new Address(
["201 E Randolph St", "25th Floor"], "Chicago", "IL", "60601")
assertEquals "201 E Randolph St", a.streets[0]
assertEquals "25th Floor", a.streets[1]
a.streets[0] = "404 W Randoph St"
}
}

Note that in both cases, the test terminates when an exception is thrown because a statement violates the immutability contract. In Listing 7, though, the streets property looks like the original type, but it actually protects itself with its own getstreets () method.

Groovy’s @ immutable annotation

One of the basic tenets of this article series is that functional languages should handle more low-level details for you. A good example is the @ immutable annotation added to groovy version 1.7, which makes the encoding in Listing 6 less important. Listing 8 shows a client class that uses this annotation:

Listing 8. Immutable client class


@Immutable
class Client {
String name, city, state, zip
String[] streets
}

Because @ immutable annotation is used, this class has the following characteristics:

  • It is final.
  • Property automatically owns the private domain that synthesizes the get method.
  • Any attempt to update a property will result in a readonlypropertyexception being thrown.
  • Groovy creates both ordered and map based constructors.
  • Collection classes are encapsulated in appropriate wrappers, and arrays (and other clonable objects) are cloned.
  • Automatically generate the default equals, hashcode, and toString methods.

A comment provides so many functions! It also behaves as you would expect, as shown in listing 9:

Listing 9. The @ immutable annotation correctly handles the expected situation


@Test (expected = ReadOnlyPropertyException)
void client_object_references_protected() {
def c = new Client([streets: ["201 E Randolph St", "Ste 25"]])
c.streets = new ArrayList();
}
@Test (expected = UnsupportedOperationException)
void client_reference_contents_protected() {
def c = new Client ([streets: ["201 E Randolph St", "Ste 25"]])
c.streets[0] = "525 Broadway St"
}
@Test
void equality() {
def d = new Client(
[name: "ACME", city:"Chicago", state:"IL",
zip:"60601",
streets: ["201 E Randolph St", "Ste 25"]])
def c = new Client(
[name: "ACME", city:"Chicago", state:"IL",
zip:"60601",
streets: ["201 E Randolph St", "Ste 25"]])
assertEquals(c, d)
assertEquals(c.hashCode(), d.hashCode())
assertFalse(c.is(d))
}

An attempt to reset an object reference causes a readonlypropertyexception to be thrown. An attempt to change the content pointed to by one of the encapsulated object references will result in an unsupported operationexception being thrown. The annotation also creates the appropriate equals and hashcode methods, as shown in the last test, with the same object content, but they do not point to the same reference.

Of course, Scala and clojure both support and promote invariance, and both have clear invariance syntax. The next article will talk about their impact from time to time.

Benefits of immutability

In a list of methods that think like a functional programmer, maintaining invariance is high on the list. Although using java to build immutable objects brings more complexity in the early stage, the later simplicity brought by this abstraction can easily compensate for the previous work.

Immutable classes discard many of the typical annoying defects in Java. One of the benefits of turning to functional programming is to make people realize that tests exist to check for successful changes in code. In other words, the real purpose of testing is to validate changes, and the more changes you make, the more tests you need to make sure you’re doing it right. If you isolate changes by strictly limiting them, you create less space for errors to occur and less to test. Because changes only happen in constructors, immutable classes make writing unit tests trivial.

You don’t need to use the copy constructor, and you never need to sweat through the horrendous details of implementing the clone () method. It’s also a good choice to use immutable objects as key values in map or set; because keys in Java’s Dictionary collection can’t change values, they are very easy to use when using immutable objects as keys.

Immutable objects are also thread safe, and there is no synchronization problem. They are also unlikely to be in an unknown or unexpected state due to the occurrence of exceptions. Because all initialization occurs in the construction phase, which is an atomic process in Java, all exceptions occur before the object instance is owned. Joshua Bloch calls this the atomicity of failure: after an object has been built, this kind of success or failure based on immutability is final

The above is the whole content of this article. I hope it will help you in your study, and I hope you can support developepaer more.