创建不可变的Java对象

71

我的目标是使一个Java对象不可变。我有一个叫做Student的类。我按照以下方式编写它以实现不可变性:

public final class Student {

private String name;
private String age;

public Student(String name, String age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}

public String getAge() {
    return age;
}

}

我的问题是,如何最好地实现 Student 类的不可变性?


3
来自Java教程:定义不可变对象的策略 - Ted Hopp
可能是如何在Java中创建不可变对象?的重复问题。 - roottraveller
Java SE 16使创建不可变类变得非常容易。https://stackoverflow.com/a/65976915/10819573 - Arvind Kumar Avinash
16个回答

71

严格来说,您的类并不是不可变的,它只是有效地不可变的。要使其不可变,您需要使用 final

private final String name;
private final String age;

虽然这个差异似乎微不足道,但在多线程上下文中会产生重大影响。一个不可变的类本质上是线程安全的,而一个有效不可变的类只有在被安全发布时才是线程安全的。

一个不可变类的正确模式是什么,它封装了一个可变类的实例,但永远不会允许该实例暴露给任何可能在构造函数返回后对其进行更改的代码?例如,foofinal 字段持有一个数组 arr,构造函数最后一件事是将 5 存储到第 3 个元素中。如果一个线程执行 blah=new foo(),另一个线程访问 blah.arr[5],我理解将数组引用存储到 arr 保证在第二个线程看到对 blah 的写入之前发生,但是对 arr[5] 的写入呢? - supercat
@supercat 不是100%确定,说实话。你应该问这个问题。我认为一个安全的模式是先填充一个临时数组,然后将其分配给最终变量。但如果直接填充最终变量,我不确定。 - assylias
@supercat 将可变类的实例设置为私有和最终,同时使返回该实例的任何方法返回对象的新副本。 - Melad Basilius
@MeladEzzat:自我撰写以上内容以来,我已经更加仔细地阅读了规范,并且在字段可以合理地为“final”的情况下,Java提供了必要的语义。然而,在Java 7中,我找不到任何处理惰性生成的不可变数据的方法,而不会对消费者施加额外的间接层或锁定。从机器的角度来看,应该可以将所有负担都放在惰性生成数据的类上,因为在创建数组之前,没有消费者线程可能将数组缓存在高速缓存中,因此如果消费线程将数组强制推出到主内存... - supercat
在任何消费者线程可能看到它之前,不应该有任何方式让消费者看到陈旧的数据。不幸的是,我不知道如何在懒惰创建线程上建立适当的内存屏障;在写入线程上放置锁定“几乎”肯定会起作用,但JIT将被允许打破它。 - supercat

60

制作不可变类需要考虑以下几点:

  • 将你的类设为final - 你已经完成了这一步
  • 将所有的属性设置为privatefinal - 在你的代码中进行适当的更改
  • 不要提供任何可以改变实例状态的方法
  • 如果你的类中有可变的属性,例如List或者Date,仅仅将它们设置为final是不够的。你应该在它们的getters中返回一个防御性拷贝,以便通过调用方法来修改它们的状态。

对于第四点,假设你的类中有一个Date属性,那么它的getter应该像这样:

public Date getDate() {
    return new Date(this.date.getTime());
}

如果您的可变字段本身包含某些可变字段,而那个字段又可能包含其他可变字段,那么创建一个防御性副本将会变得很棘手。在这种情况下,您需要迭代地复制每个可变字段。我们将这些可变字段的迭代复制称为深层复制

自己实现深层复制可能很麻烦。但是,抛开这个问题,当您发现自己陷入需要进行深度防御性复制的要求时,应重新考虑类设计。


1
“不要提供任何改变实例状态的方法”我认为这并不是必须的。只要状态更改对外部世界不可见,那就没问题。 - arshajii
1
如果您有一个私有字段,其他人无法看到。 - arshajii
1
我的意思是,如果没有getter/setter,只有一个字段供对象本身使用。也许我们对“状态改变”的定义不同;你会认为将这样的字段更改为状态改变吗? - arshajii
3
如果一个对象的内部状态可以改变,那么这个对象就不是不可变的。如果内部状态进入到 equalshashCode 的逻辑中,则说明内部状态(间接地)对外部可见。 - Ted Hopp
1
@arshajii。刚刚查看了源代码。看起来你是对的。现在我有点困惑了。 - Rohit Jain
显示剩余21条评论

7
如何将可变对象变为不可变对象?
  1. 将类声明为 final,以防止其被继承。
  2. 将所有字段设为 private,以防止直接访问。
  3. 不提供变量的 setter 方法。
  4. 将所有可变字段设为 final,这样它的值只能被赋值一次。
  5. 通过执行深拷贝的构造函数来初始化所有字段。
  6. 在 getter 方法中执行对象克隆,以返回副本而不是实际对象引用。
为什么要创建不可变对象?不可变对象是指其状态(对象数据)在构造后无法更改的对象。 source
  • 安全性:存储敏感信息,如用户名、密码、连接URL、网络连接等。
  • 易于构建、测试和使用。
  • 自动线程安全,没有同步问题
  • 不需要复制构造函数。
  • 不需要实现克隆。
  • 允许hashCode使用惰性初始化,并缓存其返回值。
  • 在作为字段使用时不需要进行防御性复制。
  • 是良好的Map键和Set元素(这些对象在集合中不能改变状态)。
  • 它们的类不变式在构造时建立,永远不需要再次检查。
  • 总是具有“失败原子性”(Joshua Bloch使用的术语):如果不可变对象抛出异常,则永远不会处于不良或不确定状态。

来源

在Java中,字符串是不可变的,这提供了诸如缓存、安全性、易于重用而无需复制等功能。 来源


3

让变量私有化且不提供设置方法对于原始数据类型是有效的。如果我的类有任何对象集合呢?

如何使任何类都具备不可变性和对象集合?

编写您自己的集合对象并扩展集合类,遵循私有变量和没有设置方法的规则。或者返回集合对象的克隆对象。

public final class Student {

private StudentList names;//Which is extended from arraylist

public Student() {
names = DAO.getNamesList()//Which will return All Student names from Database  its upto you how you want to implement.
}

public StudentList getStudentList(){
return names;//you need to implement your own methods in StudentList class to iterate your arraylist; or you can return Enumeration object.
}

public Enumeration getStudentNamesIterator(
Enumeration e = Collections.enumeration(names);
return e;
}

public class StudentList extends ArrayList {

}

3

使用 final 关键字:

private final String name;
private final String age;

它不提供不可变性。 - Tugrul Bayrak

2

这样可以,但我建议将字段也定义为 final

另外,我会将年龄定义为 intdouble 而不是字符串。


2

稍微扩展一下答案。

final并不等同于Immutable,但可以通过特定方式使用final使某些内容变得不可变。

某些类型是不可变的,它们代表不变的值而不是可变状态的对象。例如字符串、数字等都是不可变的。通常情况下,我们的对象最终会转化为数据结构,引用不可变值,但我们通过将新值分配给相同的字段名来更改数据结构。

因此,要使某个东西真正不可变,必须确保从上至下都使用final,直到达到组合树底部的每个字段都达到基本值。否则,在对象下面的某个位置进行更改,该对象就不是完全不可变的。


1

虽然回答有些晚,但或许可以帮助其他有这个问题的人。

  1. 不可变对象的状态在构造后不能修改,任何修改都应导致新的不可变对象。
  2. 所有不可变类的字段都应该是final的。
  3. 对象必须正确构造,即在构造过程中对象引用不能泄漏。
  4. 为了限制子类改变父类的不可变性,对象应该是final的。

我认为这个链接可以更好地帮助你:阅读更多:http://javarevisited.blogspot.com/2013/03/how-to-create-immutable-class-object-java-example-tutorial.html#ixzz40VDQDDL1


1
你的示例已经是不可变对象,因为Student类中的字段只能在实例初始化时设置。
要使对象不可变,您必须执行以下步骤:
  1. 不要使用任何可以更改类字段的方法。例如,不要使用Setters。
  2. 避免使用公共的非final字段。如果您的字段是公共的,则必须将它们声明为final并在构造函数中或直接在声明行中初始化它们。

0
以下是幾條規則,有助於使 Java 中的類不可變: 1. 不可更改不可變物件的狀態,任何修改都應導致新的不可變物件。 2. 不可變類的所有字段都應該是 final 的。 3. 物件必須正確地構建,即在構建過程中不得泄漏物件引用。 4. 父類的不可變性應該是最終的,以限制子類的更改。
示例:
public final class Contacts {

private final String name;
private final String mobile;

public Contacts(String name, String mobile) {
    this.name = name;
    this.mobile = mobile;
}

public String getName(){
    return name;
}

public String getMobile(){
    return mobile;
}

}

请参考此链接:http://javarevisited.blogspot.in/2013/03/how-to-create-immutable-class-object-java-example-tutorial.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接