Java中的不可变数据结构 - Jworks.io

19-03-27 banq
              

开发人员通常认为拥有final引用,或者val在Kotlin或Scala中,足以使对象不可变。这篇博客文章深入研究了不可变引用和不可变数据结构。

不可变数据结构的好处

不可变数据结构具有一些显着的好处,例如:

  • 没有无效的状态
  • 线程安全
  • 更容易理解代码
  • 更容易测试
  • 可用于值类型

1. 没有无效的状态

当一个对象是不可变的时,很难让对象处于无效状态。该对象只能通过其构造函数实例化,这将强制对象的有效性。这样,可以强制执行有效状态所需的参数。一个例子:

Address address = new Address();
address.setCity("Sydney");
// address is in invalid 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.

2.线程安全

由于无法更改对象,因此可以在线程之间共享它,而不会出现竞争条件或数据突变问题。

3.更容易理解代码

与无效状态中的代码示例类似,使用构造函数通常比使用初始化方法更容易。这是因为构造函数强制执行必需的参数,而在编译时不强制执行setter或initialiser方法。

4.更容易测试

由于对象更容易预测,因此没有必要测试初始化​​方法的所有排列; 即,在调用类的构造函数时,该对象有效或无效。使用这些类的代码的其他部分变得更加可预测,NullPointerExceptions的机会更少。有时,当传递对象时,有一些方法可能会改变对象的状态。例如:

public boolean isOverseas(Address address) {
    if(address.getCountry().equals("Australia") == false) {
        address.setOverseas(true); // address has now been mutated!
        return true;
    } else {
        return false;
    }
}

一般来说,上面的代码是不好的做法。它返回一个布尔值,并可能改变对象的状态。这使得代码更难理解和测试。更好的解决方案是从Address类中删除setter,并通过测试国家名称返回一个布尔值。更好的方法是将此逻辑移动到Address类本身(address.isOverseas())。当确实需要设置状态时,在不改变输入的情况下制作原始对象的副本。

5. 可用于值类型

想象一下金额,比如10美元。10美元总是10美元。在代码中,这可能看起来像公共货币(最终BigInteger金额,最终货币货币)。正如您在此代码中所看到的,不可能将10美元的值更改为除此之外的任何值,因此上述内容可以安全地用于值类型。

6. final引用不会使对象不可变

如前所述,我经常遇到的问题之一是这些开发人员中的很大一部分并不完全理解final引用和不可变对象之间的区别。似乎这些开发人员的共同理解是,变量成为final的那一刻,数据结构变得不可变。不幸的是,这并不是那么简单,我想一劳永逸地解决这种误解:

final引用不会使您的对象不可变!

换句话说,下面的代码并没有使对象不变:

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

为什么不?好吧,虽然`person`是一个final字段,并且无法重新分配,但Person类可能有一个setter方法或其他可变mutator方法,可以执行如下操作:

person.setName("Cindy");

无论最终修饰符如何,都可以轻松完成。或者,Person类可能会公开这样的地址列表。访问此列表允许您向其添加地址,因此会改变person对象,如下所示:

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

我们的final引用没有帮助我们阻止我们改变人物对象。

好的,现在我们已经解决了这个问题,让我们深入了解一下我们如何使一个类不可变。在设计我们的类时,我们需要记住几件事:

  • 不要以可变的方式暴露内部状态
  • 不要在内部改变状态
  • 确保子类不会覆盖上述行为

根据以下准则,让我们设计一个更好的Person类版本。 

public final class Person {    // final class, can’t be overridden by subclasses
    private final String name;     // final for 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() {
        return country;
    }
}

现在,可以使用以下代码:

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

现在,由于Person和Address类的设计,上面的代码是不可变的,同时还有final的引用,因此无法将person变量重新分配给其他任何东西。

更新:正如有些人提到的,上面的代码仍然是可变的,因为我没有在构造函数中复制地址列表。因此,如果不在构造函数中调用新的ArrayList(),仍然可以执行以下操作:

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

但是,由于现在在构造函数中创建了一个副本,上面的代码将不再影响Person类中复制的地址列表引用,从而使代码安全。谢谢大家的好评!

​​​​​​​

              

1