在 Java 中创建不可变对象
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
不可变对象是指不会改变的对象。创建之后,就无法修改它。如果要修改不可变对象,必须先克隆它,然后在创建克隆对象时对其进行修改。
Java 不可变对象的所有字段都必须是内部private final字段,并且不能实现任何 setter 方法。它需要一个构造函数,该构造函数接受每个字段的值。
不可变对象在多线程环境和流式处理中非常有用。确保对象在处理过程中不会发生变化至关重要。由一个线程修改另一个线程的对象而导致的错误通常非常隐蔽,难以追踪。不可变对象能够从根本上杜绝这类问题。
内容
抓到你了!
列表、数组、映射、集合和其他非不可变对象可能会让人感到意外。private final没有 setter 的对象会固定为最初赋值的对象,但该对象内部的值并不是固定的(除非该对象是不可变的)。
这意味着你可能会有一个ImmutableShoppingList myShoppingList = new ImmutableShoppingList(new String[] {"apples","anchovies","pasta"})购物清单,并期望清单上总是会有“苹果”、“凤尾鱼”和“意大利面”。
有人可能会打电话myShoppingList.getList()[0] = "candy bars";把你的清单改成“糖果”、“凤尾鱼”和“意大利面”,这既不健康,显然也不是你想要的。
基本元素
好消息!基本类型是不可变的,所以你不需要做任何特殊处理。
收藏
好消息!java.util.Collections提供了许多便捷方法,可以轻松地将 Collection 转换为 UnmodifiableCollection。
查看:
Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableMap
Collections.unmodifiableNavigableMap
Collections.unmodifiableNavigableSet
Collections.unmodifiableSet
Collections.unmodifiableSortedMap
Collections.unmodifiableSortedSet
我建议你将字段存储为泛型集合(List,而不是 ArrayList),并在构造函数中将其设置为不可修改,如下所示:
public class ImmutableShoppingList {
private final List<String> list;
public ImmutableShoppingList(List<String> list){
this.list = Collections.unmodifiableList(list);
}
public List<String> getList(){
return list;
}
}
这样一来,你就可以使用 IDE 代码生成器来创建 getter,这很不错,而且它还把所有输入修饰符都放在一个地方,这也很不错。
坏消息!如果在创建集合时保留了对该集合的引用,即使内部将其存储为不可修改的集合,仍然可以对其进行修改。例如:
List<String> originalList = new ArrayList<>();
theList.add("apple");
ImmutableShoppingList blah = new ImmutableShoppingList(originalList);
originalList.add("candy bar");
这份原本应该一成不变的购物清单最初只有一个苹果,后来又被添加了一块巧克力。我们该怎么办?
克隆列表!
public class ImmutableShoppingList {
private final List<String> list;
public ImmutableShoppingList(List<String> list){
List<String> tmpListOfHolding = new ArrayList<>();
tmpListOfHolding.addAll(list);
this.list = Collections.unmodifiableList(tmpListOfHolding);
}
public String[] getList(){
return (String[]) list.toArray();
}
}
当我们创建不可变对象时,我们会对集合进行深度克隆,这会断开与原始引用的连接。现在,当我们运行“偷偷放一块糖进去”的示例时,“糖”会被添加到集合中originalList,但不会添加到原始对象中ImmutableShoppingList。
数组
坏消息!Java 没有提供任何便捷的方法来防止数组被修改。最好的办法是隐藏原始数组并始终返回一个副本,或者在底层实现中不使用数组,而是将 Collection 对象转换为数组。
我更倾向于使用集合,但如果你的对象 API 中必须使用数组,我会采取以下方法:
public class ImmutableShoppingList {
private final List<String> list;
public ImmutableShoppingList(String[] list){
this.list = Collections.unmodifiableList(Arrays.asList(list));
}
public String[] getList(){
return (String[]) list.toArray();
}
}
对象
对象字段很容易处理。如果子对象也是不可变的,那就太好了!你不需要做任何特殊操作。
如果子对象不是不可变的,它们就很像一个集合。你需要对它们进行深度克隆,否则原始引用可能会在你不知不觉中更改你原本以为是不可变的数据。
通常情况下,你会遇到代码库或库中已经存在的可变对象。在这种情况下,我喜欢创建一个继承自该可变类的不可变对象包装类。我发现静态getInstance(MutableObject obj)方法很有用,但构造函数ImmutableObject(MutableObject obj)也同样重要。
如果我想修改一个不可变对象该怎么办?
这种情况每个人都会遇到。你需要一个对象,但你并不完全了解这个对象。你无法完全接受一个不可变对象。
在这种情况下,我采用了构建器模式。
建造者模式会创建一个临时对象,该对象具有与目标对象相同的字段。它为所有字段都提供了 getter 和 setter 方法。此外,它还包含一个build()用于创建目标对象的方法。
想象一个微小的、不可改变的物体:
class ImmutableDog {
private final String name;
private final int weight
public ImmutableDog(String name, int weight){
this.name = name;
this.weight = weight;
}
public String getName(){
return this.name;
}
public int getWeight(){
return this.weight;
}
}
以下是建造者的样子:
class ImmutableDogBuilder {
private String name;
private int weight;
public ImmutableDogBuilder(){}
public ImmutableDog build(){
return new ImmutableDog(name, weight);
}
public ImmutableDogBuilder setName(String name){
this.name = name;
return this;
}
public ImmutableDogBuilder setWeight(int weight){
this.weight = weight;
return this;
}
public String getName(){
return this.name;
}
public int getWeight(){
return this.weight;
}
}
注意一下 setter 方法。我非常喜欢这种在构建器类中为每个 setter 方法返回值的模式,因为它创建了一个非常流畅的 API。你可以像这样this使用它:ImmutableDogBuilder
ImmutableDogBuilder dogBuilder = new ImmutableDogBuilder().setName("Rover").setWeight(25);
您可以想象,对于字段较多的类来说,这将大大简化您的代码。
建造者应该放在哪里?
这里有两种不同的观点。
一方面,你可以为构建器创建一个单独的类。这很简单,也很符合惯例,而且你的 IDE 很可能会将这些类分组在一起,因为它们的名称可能很相似。
另一方面,您可以将构建器类作为公共静态内部类嵌入到不可变对象类中。
我更倾向于将构建器类嵌入到不可变对象中,因为我将构建器视为不可变对象的辅助函数,而不是独立存在的对象。这样可以保持它们之间的紧密耦合。
如果存在包含错误数据的不可变对象该怎么办?
这种情况每个人都会遇到,尤其是在接受输入数据的时候。错误数据!
糟糕的数据不好,但不可更改的糟糕数据似乎尤其糟糕——你甚至无法修复它!
我采用两种方法来防止错误数据被转换为不可变对象。
我的主要方法是使用一套业务规则来测试数据的合理性和合法性。这些业务规则会检查构建器,如果构建器通过测试,我就认为可以创建不可变对象。
我的第二种方法是将少量业务逻辑嵌入到不可变对象中。我不允许必填字段为空,任何可为空的字段都是一个Optional.
例如:
class ImmutableDog {
private final String name;
private final Optional<int> weight
public ImmutableDog(String name, Optional<int> weight){
Objects.requireNonNull(name);
this.name = name;
this.weight = weight;
}
public String getName(){
return this.name;
}
public Optional<int> getWeight(){
return this.weight;
}
}
这是一份ImmutableDog需要填写名字但不需要填写体重的问卷。知道狗狗的名字很重要,但并不一定需要知道“毛毛”重15磅。
Objects.requireNonNull如果未提供名称,则会立即抛出异常NullPointerException。这可以防止创建无意义的不可变对象。此外,它还允许不可变对象的用户(例如流或函数)跳过对空值的处理。这里不存在空值。
使用该方法Optional<int>可以让用户ImmutableDog立即意识到他们可能需要处理空值。提供OptionalAPI 为下游用户提供了一种简单、实用的方式来处理空值。
包起来
不可变对象需要一些特殊的护理和处理,但它们的实用性是值得的。
需要牢记的主要原则是:
- 克隆不可变对象内部的数组、集合和对象
- 当你需要可变对象时,请使用构建器。
- 在对象的 API 中使用 Optional 来指示字段是否可为空。
- 快速处理错误数据——Objects.requireNonNull 可以提供帮助
继续前进,停止变异
既然你已经读到这里,那就订阅我的邮件列表,获取更多类似内容吧。邮件每月发送一次,你可以随时取消订阅,我保证不会发送垃圾邮件。
文章来源:https://dev.to/monknomo/make-an-immutable-object---in-java-480n



