泛谈Java中的不可变数据结构

 更新时间:2019年05月23日 15:45:07   作者:银河1号  
开发人员通常认为拥有final引用,或者val在Kotlin或Scala中,足以使对象不可变。这篇博客文章深入研究了不可变引用和不可变数据结构,下面小编来和大家一起学习它

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

不可变数据结构的好处

不可变数据结构具有显着优势,例如:

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

没有无效的状态

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

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方法在编译时不会强制执行。

更易于测试的代码

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

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美元的值更改为除此之外的任何值,因此,上述内容可以安全地用于值类型。

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

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

A final reference does not make your objects immutable!

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

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"));

现在,上面的代码是不可变的,但是由于Person 和 Address 类的设计 ,同时还有最终引用,因此无法将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 ,从而使代码安全。

我希望上述内容有助于理解最终和不变性之间的差异。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Java数据库操作库DButils类的使用方法与实例详解

    Java数据库操作库DButils类的使用方法与实例详解

    这篇文章主要介绍了JDBC数据库操作库DButils类的使用方法详解,需要的朋友可以参考下
    2020-02-02
  • Java中ArrayList类用法详解

    Java中ArrayList类用法详解

    这篇文章主要给大家介绍了关于Java中ArrayList类用法的相关资料,ArrayList是Java中的一种常见的数据结构,它实现了List接口,是线程不安全的动态数组,需要的朋友可以参考下
    2023-09-09
  • java浏览器文件打包下载过程解析

    java浏览器文件打包下载过程解析

    这篇文章主要介绍了java浏览器文件打包下载过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • Java流操作之数据流实例代码

    Java流操作之数据流实例代码

    这篇文章主要介绍了Java流操作之数据流实例代码,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Java下载文件时文件名乱码问题解决办法

    Java下载文件时文件名乱码问题解决办法

    我最近在开发时遇到了文件另存为时文件名出现乱码,在火狐上正常的文件名,在IE中又出现乱码问题,然后好不容易在IE下调试好了文件名乱码问题,在火狐下又出现乱码,最后终于感觉这样是能解决了。具体如下:
    2013-04-04
  • Java枚举与注解的创建步骤

    Java枚举与注解的创建步骤

    这篇文章通过抽象的概念和具体实现步骤,充分说明了java枚举与注解的概念和使用方法,通过该篇文章你可以学会如何自定义枚举类和了解部分Java内置注解,希望对你有所帮助
    2021-06-06
  • java彩色瓷砖编程题分析

    java彩色瓷砖编程题分析

    这篇文章主要介绍了java彩色瓷砖编程题的详细解题思路以及解决方法分享,对此有兴趣的参考下。
    2018-02-02
  • OpenTelemetry Java SDK 高级用法解析

    OpenTelemetry Java SDK 高级用法解析

    这篇文章主要介绍了OpenTelemetry Java SDK 的高级用法示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • java读取磁盘并遍历磁盘文件过程解析

    java读取磁盘并遍历磁盘文件过程解析

    这篇文章主要介绍了java读取磁盘并遍历磁盘文件过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • Java连接数据库JDBC技术之prepareStatement的详细介绍

    Java连接数据库JDBC技术之prepareStatement的详细介绍

    这篇文章主要介绍了Java连接数据库JDBC技术之prepareStatement的详细介绍,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07

最新评论