在面向对象的编程中,如果对象的状态在创建后无法修改,则该对象是不可变的 。
在Java中,不可变对象的一个很好的例子是String
。 创建完成后,我们将无法修改其状态。 我们可以要求它创建新的字符串,但是它自己的状态永远不会改变。
但是,JDK中没有那么多不可变的类。 以类Date
为例。 可以使用setTime()
修改其状态。
我不知道为什么JDK设计师决定以不同的方式来制作这两个非常相似的类。 但是,我认为可变Date
的设计有许多缺陷,而不变的String
则更多地体现了面向对象范例的精神。
而且,我认为在一个完美的面向对象的世界中所有类都应该是不变的 。 不幸的是,有时由于JVM的限制在技术上是不可能的。 尽管如此,我们应该始终追求最佳。
这是支持不变性的参数的不完整列表:
- 不变的对象更易于构造,测试和使用
- 真正不可变的对象始终是线程安全的
- 它们有助于避免时间耦合
- 它们的使用无副作用(无防御性副本)
- 避免身份变异性问题
- 他们总是有失败原子性
- 它们更容易缓存
- 他们防止NULL引用, 这是不好的
让我们一一讨论最重要的论点。
线程安全
第一个也是最明显的论点是,不可变对象是线程安全的。 这意味着多个线程可以同时访问同一对象,而不会与另一个线程发生冲突。
如果没有对象方法可以修改其状态,那么无论它们有多少个以及被调用的频率是多少,它们都将在自己的堆栈内存空间中工作。
Goetz等。 在非常著名的《 Java Concurrency in Practice》 (强烈推荐)一书中,更详细地介绍了不可变对象的优点。
避免时间耦合
这是时间耦合的示例(代码发出两个连续的HTTP POST请求,其中第二个包含HTTP正文):
Request request = new Request("http://example.com");
request.method("POST");
String first = request.fetch();
request.body("text=hello");
String second = request.fetch();
此代码有效。 但是,您必须记住,应该在配置第二个请求之前配置第一个请求。 如果我们决定从脚本中删除第一个请求,则将删除第二行和第三行,并且不会从编译器中得到任何错误:
Request request = new Request("http://example.com");
// request.method("POST");
// String first = request.fetch();
request.body("text=hello");
String second = request.fetch();
现在,该脚本已损坏,尽管它编译时没有错误。 这就是时间耦合的意义所在—代码中总是有一些程序员必须记住的隐藏信息。 在此示例中,我们必须记住,第一个请求的配置也用于第二个请求。
我们必须记住,第二个请求应始终保持在一起,并在第一个请求之后执行。
如果Request
类是不可变的,则第一个代码段将一开始就无法工作,并且将被重写为:
final Request request = new Request("");
String first = request.method("POST").fetch();
String second = request.method("POST").body("text=hello").fetch();
现在,这两个请求没有耦合。 我们可以安全地删除第一个,第二个仍然可以正常工作。 您可能会指出存在代码重复。 是的,我们应该摆脱它,然后重新编写代码:
final Request request = new Request("");
final Request post = request.method("POST");
String first = post.fetch();
String second = post.body("text=hello").fetch();
瞧,重构并没有破坏任何东西,我们仍然没有时间耦合。 可以安全地从代码中删除第一个请求,而不会影响第二个请求。
我希望这个例子能证明操作不可变对象的代码更具可读性和可维护性,因为它没有时间上的耦合。
避免副作用
让我们尝试在新方法中使用我们的Request
类(现在它是可变的):
public String post(Request request) {request.method("POST");return request.fetch();
}
让我们尝试发出两个请求-第一个请求使用GET方法,第二个请求使用POST:
Request request = new Request("http://example.com");
request.method("GET");
String first = this.post(request);
String second = request.fetch();
方法post()
具有“副作用”,它对可变对象request
进行了更改。 在这种情况下,这些更改并不是真正预期的。 我们希望它发出POST请求并返回其主体。 我们不想阅读其文档只是为了发现它还在后台修改了我们作为参数传递给它的请求。
不用说,这种副作用会导致错误和可维护性问题。 使用不可变的Request
会更好:
public String post(Request request) {return request.method("POST").fetch();
}
在这种情况下,我们可能没有任何副作用。 任何人都不能修改我们的request
对象,无论它在何处使用以及方法调用传递给调用堆栈的深度如何:
Request request = new Request("http://example.com").method("GET");
String first = this.post(request);
String second = request.fetch();
此代码是绝对安全且无副作用的。
避免身份变异
通常,如果对象的内部状态相同,我们希望它们相同。 Date
类是一个很好的例子:
Date first = new Date(1L);
Date second = new Date(1L);
assert first.equals(second); // true
有两个不同的对象。 但是,它们彼此相等,因为它们的封装状态相同。 通过自定义的equals()
和hashCode()
方法的重载实现,可以实现这一点。
这种方便的方法与可变对象一起使用的结果是,每次我们修改对象的状态时,它都会更改其身份:
Date first = new Date(1L);
Date second = new Date(1L);
first.setTime(2L);
assert first.equals(second); // false
在您开始将可变对象用作地图中的键之前,这看起来很自然:
Map<Date, String> map = new HashMap<>();
Date date = new Date();
map.put(date, "hello, world!");
date.setTime(12345L);
assert map.containsKey(date); // false
修改date
状态对象时,我们不希望它更改其身份。 我们不希望仅因为其键的状态已更改而在映射中丢失条目。 但是,这正是上面示例中发生的情况。
当我们向地图添加对象时,其hashCode()
返回一个值。 HashMap
使用此值将条目放置到内部哈希表中。 当我们调用containsKey()
时,对象的哈希码是不同的(因为它基于其内部状态),并且HashMap
在内部哈希表中找不到它。
调试可变对象的副作用非常烦人且困难。 不可变的对象完全避免了它。
失效原子性
这是一个简单的示例:
public class Stack {private int size;private String[] items;public void push(String item) {size++;if (size > items.length) {throw new RuntimeException("stack overflow");}items[size] = item;}
}
很明显,如果Stack
类在溢出时引发运行时异常,则该对象将处于断开状态。 它的size
属性将增加,而items
将不会获得新元素。
不变性可以防止此问题。 对象永远不会处于损坏状态,因为仅在其构造函数中修改了对象的状态。 构造函数将失败,拒绝对象实例化,或者成功,则将生成有效的固态对象,该对象永远不会更改其封装状态。
有关此主题的更多信息,请阅读Joshua Bloch撰写的有效Java,第二版 。
反对不变性的争论
有许多反对不变性的论点。
- “不可迁移性不适用于企业系统”。 我经常听到人们说不变性是一种奇特的功能,而在实际企业系统中绝对不可行。 作为反驳,我只能显示一些仅包含不可变Java对象的实际应用程序示例: jcabi-http , jcabi-xml , jcabi-github , jcabi-s3 , jcabi-dynamo , jcabi-simpledb所有仅与不可变类/对象一起使用的Java库。 netbout.com和stateful.co是仅与不可变对象一起使用的Web应用程序。
- “更新现有对象比创建新对象便宜”。 Oracle 认为 :“对象创建的影响通常被高估了,并且可以被与不可变对象相关联的某些效率所抵消。 其中包括由于垃圾收集而减少的开销,以及消除了保护可变对象免受损坏所需的代码。” 我同意。
如果您还有其他论点,请在下面发表,我将尝试发表评论。
翻译自: https://www.javacodegeeks.com/2014/09/objects-should-be-immutable.html