Java中的不可变数据结构

做为我最近一直在进行的一些编码访谈的一部分,有时会出现不变性问题。我本身并不过度教条,但每当不须要可变状态时,我会试图摆脱致使可变性的代码,这在数据结构中一般是最明显的。然而,彷佛对不可变性的概念存在一些误解,开发人员一般认为拥有final引用,或者val在Kotlin或Scala中,足以使对象不可变。这篇博客文章深刻研究了不可变引用和不可变数据结构java

不可变数据结构的好处

不可变数据结构具备显着优点,例如:安全

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

没有无效的状态

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

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.
复制代码

线程安全

因为没法更改对象,所以能够在线程之间共享它,而不会出现竞争条件或数据突变问题。数据结构

易于理解的代码

与无效状态的代码示例相似,使用构造函数一般比初始化方法更容易。这是由于构造函数强制执行必需的参数,而setter或initializer方法在编译时不会强制执行。app

更易于测试的代码

因为对象更具可预测性,所以没必要测试初始化​​方法的全部排列,即在调用类的构造函数时,该对象有效或无效。使用这些类的代码的其余部分变得更可预测,具备更少的NullPointerException 机会。有时,当传递对象时,有些方法可能会改变对象的状态。例如:ide

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())。当确实须要设置状态时,在不改变输入的状况下制做原始对象的副本。函数

可用于值类型

想象一下金额,好比10美圆。10美圆将永远是10美圆。在代码中,这可能看起来像 public Money(final BigInteger amount, final Currency currency)。正如您在此代码中看到的那样,不可能将10美圆的值更改成除此以外的任何值,所以,上述内容能够安全地用于值类型。测试

最终引用不要使对象不可变

如前所述,我常常遇到的问题之一是这些开发人员中的很大一部分并不彻底理解最终引用和不可变对象之间的区别。彷佛这些开发人员的共同理解是,变量成为最终的那一刻,数据结构变得不可变。不幸的是,这并非那么简单,我想一劳永逸地把这种误解带出世界:ui

A final reference does not make your objects immutable!this

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

final Person person = new Person("John");
复制代码

为何不?好吧,虽然person是最后一个字段并且没法从新分配,可是 Person类可能有一个setter方法或其余mutator方法,能够执行以下操做:

person.setName("Cindy");
复制代码

不管最终修饰符如何,这都是一件很是容易的事情。或者, Person类可能会公开这样的地址列表。访问此列表容许您向其添加地址,所以,以下所示改变 person对象:

person.getAddresses().add(new Address("Sydney"));
复制代码

好了,既然咱们已经解决了这个问题,那么让咱们深刻了解一下咱们如何使类不可变。在设计咱们的类时,咱们须要记住几件事:

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

根据如下准则,让咱们设计一个更好的Person class 版本 。

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"));
复制代码

如今,上面的代码是不可变的,可是因为PersonAddress 类的设计 ,同时还有最终引用,所以没法将person变量从新分配给其余任何东西。

更新:正如有些人提到的,上面的代码仍然是可变的,由于我没有在构造函数中复制地址列表。所以,若是不在ArrayList() 构造函数中调用new ,仍然能够执行如下操做:

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

可是,因为在构造函数中建立了一个新副本,上面的代码将再也不影响类中复制的地址列表引用 Person ,从而使代码安全。

我但愿上述内容有助于理解最终和不变性之间的差别。若是您有任何意见或反馈,请在下面的评论中告诉我。

再次,很是感谢个人同事Winston花时间校对和审阅这篇博文!

英文原文:dzone.com/articles/im…

查看更多文章

公众号:银河系1号

联系邮箱:public@space-explore.com

(未经赞成,请勿转载)

相关文章
相关标签/搜索