修改list中对象的值_Java 中的不可变数据结构

(给ImportNew加星标,提高Java技能)

编译:ImportNew/唐尤华

最近,在我主导的几场代码面试中,经常出现不可变数据结构(Immutable Data Structure)相关内容。关于这个主题我个人并不过分教条,不变性通常体现在数据结构中,"除非必要"否则不会要求代码一定具备不变性。然而,我发现大家对不变性(Immutability)这个概念似乎有一些误解。开发者通常认为加上 `final`,或者在 Kotlin、Scala 中加上 `val` 就足以实现不可变对象。这篇文章会深入讨论不可变引用和不可变数据结构。

1. 不可变数据结构的优点

不可变数据结构有下列显著优点:

  • 没有无效状态(Invalid State)

  • 线程安全

  • 代码易于理解

  • 易于测试

  • 可用作值类型

译注:在计算机编程中包含两种类型,值类型 value type 与引用类型 reference type。值类型表示实际值,引用类型表示对其他值或对象的引用。

2. 没有无效状态

不可变对象只能通过构造函数初始化,并且通过参数限制了输入的有效性,从而确保对象不会包含无效值。例如下面这段代码示例:

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

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

3. 线程安全

由于对象值不可修改,在线程间共享时不会产生竞态条件或者数据突变问题。

4. 代码易于理解

在上面的示例代码中,使用构造函数比初始化方法更易于理解。构造函数会强制检查输入参数,而 setter 或初始化方法不会在编译时进行检查。

5. 易于测试

使用初始化方法,必须测试调用顺序对对象的影响。而使用构造函数,对象的值要么有效要么无效,无需进行排列组合测试。代码执行结果的可靠性更强,出现 `NullPointerExceptions` 的机率更小。下面是一个传递对象过程中改变了对象状态的示例:

```javapublic 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
addresses;public Person(String name, List
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
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
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,这样代码就安全了。感谢指正!

希望本文能够有助于理解 `final` 与代码不可变之间的区别,如果有任何疑问,欢迎在评论区留言。

推荐阅读

(点击标题可跳转阅读)

一步一脚印,了解多线程

Java 进程中有哪些组件会占用内存?

内联意味着简化?(1) 逃逸分析

看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

098b7abb81202e646fc6e204cd02d959.png

好文章,我在看❤️

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值