Immutable data structures in Java

Recently, in several code interviews led by me, Immutable Data Structure (Immutable Data Structure) related content often appeared. Personally, I am not too dogmatic on this topic. Immutability is usually reflected in the data structure, and the code is not required to have immutability " unless necessary ". However, I found that everyone seems to have some misunderstandings about the concept of immutability. Developers usually think that adding `final`, or adding `val` in Kotlin or Scala is sufficient to achieve immutable objects. This article will discuss immutable references and immutable data structures in depth.


1. The advantages of immutable data structures


Immutable data structures have the following significant advantages:


  • No invalid state (Invalid State)

  • Thread safe

  • The code is easy to understand

  • Easy to test

  • Can be used as a value type


Annotation: There are two types in computer programming, value type and reference type. Value types represent actual values, and reference types represent references to other values ​​or objects.


2. No invalid state


Immutable objects can only be initialized through the constructor, and the validity of the input is limited by parameters, thus ensuring that the object does not contain invalid values. For example, the following code example:


```java
Address address = new Address();
address.setCity("Sydney");
// 由于没有设置 country,address 现在处于无效状态.

Address address = new Address("Sydney", "Australia");
// Address 对象有效并且不提供 setter 方法,因此 address 对象会一直保持有效.
```


3. Thread safety


Since the object value cannot be modified, there will be no race conditions or data mutation problems when sharing between threads.


4. The code is easy to understand


In the example code above, using the constructor is easier to understand than the initialization method. The constructor will force the check of input parameters, while the setter or initialization method will not check at compile time.


5. Easy to test


Using the initialization method, you must test the effect of the calling sequence on the object. Using the constructor, the value of the object is either valid or invalid, without the need for permutation and combination tests. The reliability of code execution results is stronger, and the probability of `NullPointerExceptions` is smaller. The following is an example of changing the state of the object during the transfer:


```java
public boolean isOverseas(Address address)
{
 if(address.getCountry().equals("Australia") == false) {
   address.setOverseas(true); // address 的值发生了改变!
   return true;
 } else {
   return false;
 }
}
```


上面的代码是一种错误示范,在返回 `boolean` 结果的同时改变了对象状态。这样的代码可读性和可测性都很差。一种更好的方法是从 `Address` 类中移除 setter 方法,为 `country` 属性提供 `boolean` 类型的测试方法;更进一步,可以把 `address.isOverseas()` 的逻辑移到 `Address` 类中。需要设置状态时,拷贝原来的对象而非修改输入对象的值。


6. 可作为值类型使用


如何做到使用 `Money` 对象表示10美金,使用的时候一直是10美金?比如这段代码,`public Money(final BigInteger amount, final Currency currency)` 确保了一旦声明10美金后接下来不会改变。这样对象的值可以安全地作为值类型使用。


7. final 并不能让对象变成不可变对象


文章开头提到过,我经常遇到开发者不能完全理解 `final` 引用和不可变对象的区别。最常见误区是,只要在变量前加上 `final` 就会成为不可变数据结构。不幸的是,实际并没有这么简单。接下来会为大家消除这个误解:


在变量前加 `final` 不会产生不可变对象。


换句话说,下面这段代码生成的对象是可变对象:


```java
final Person person = new Person("John");
```


尽管 `person` 是一个 final 字段不能重新赋值,但 `Person` 类可能提供了 setter 方法或者其他修改方法,比如像下面这个方法:


```java
person.setName("Cindy");
```


无论是否加 `final` 修饰符,轻易就可以修改对象。不仅如此,`Person` 类可能还提供了许多修改 address 属性的类似方法,调用它们可以向对象添加地址,同样会修改 `person` 对象。


```java
person.getAddresses().add(new Address("Sydney"));
```


`final` 引用并没能阻止修改对象。


现在我们已经澄清了这个误解,接下来讨论如何让类具有不可变的特性。在设计时需要考虑以下事项:


  • 不要把内部状态暴露出来

  • 不要在内部修改状态

  • 确保子类不会破坏上面的行为


按照上面这些建议,让我们重新设计 `Person` 类:


```java
public final class Person {  // final 类, 不支持重载
 private final String name;     // 加 final 修饰, 支持多线程
 private final List<Address> addresses;

 public Person(String name, List<Address> addresses) {
   this.name = name;
   this.addresses = List.copyOf(addresses);   // 拷贝列表, 避免从外面修改对象 (Java 10+).
               // 也可以使用 Collections.unmodifiableList(new ArrayList<>(addresses));

 }

 public String getName() {
   return this.name;   // String 是不可变对象, 可以暴露
 }

 public List<Address> getAddresses() {
   return addresses; // Address list 可以修改
 }
}

public final class Address {    // final 类, 不支持重载
   private final String city;       // 只使用不可变类
   private final String country;

   public Address(String city, String country) {
       this.city = city;
       this.country = country;
   }

   public String getCity() {
       return city;
   }

   public String getCountry() {
       return country;
   }
}
```


现在,代码变成下面这样:


```java
import java.util.List;
final Person person = new Person("John", List.of(new Address(“Sydney”, "Australia"));
```


更新后的 `Person` 和 `Address` 让上面的代码成为不可变代码。不仅如此,`final` 引用让 `person` 变量无法再次赋值。


更新:正如评论中[指出的][1],上面的代码还是可以修改的,因为并没有在构造函数中执行列表拷贝。如果不在构造函数中调用 `new ArrayList()` 还可以像下面这样做:


```java
final List<Address> addresses = new ArrayList<>();
addresses.add(new Address("Sydney", "Australia"));
final Person person = new Person("John", addressList);
addresses.clear();
```


[1]:https://www.reddit.com/r/java/comments/azryu6/final_vs_immutable_data_structures_in_java/?st=jt74o32w&sh=40d418d3


由于不在构造函数中执行 `copy`,上面的代码无法修改 `Person` 类中拷贝后的 address list,这样代码就安全了。感谢指正!


Guess you like

Origin blog.51cto.com/15082395/2590359