请看我不是在问什么是不变性,我理解不变性但问题是如何在引用可变对象时创建不可变类。此外,我的班级未通过可变性探测器项目检查,因此请求您的观点。
我创建了一个Immutable类EmpAndAddress.java,它引用了一个可复制的可变类EmpAddress.java。 我遵循了java规则并尝试使用可变性检测器测试我的类,但是我的类失败了不可变测试。只是想检查一下我是否遗漏了什么。在我的Immutable中,我总是创建类型为EmpAddress的新对象,可以遵循规则。
http://mutabilitydetector.github.io/MutabilityDetector/ 1. Mutable EmpAddress.java
public class EmpAddress implements Cloneable{
public String empCity;
public int zipCode;
public EmpAddress(String empCity, int zipCode) {
super();
this.empCity = empCity;
this.zipCode = zipCode;
}
public String getEmpCity() {
return empCity;
}
public void setEmpCity(String empCity) {
this.empCity = empCity;
}
public int getZipCode() {
return zipCode;
}
public void setZipCode(int zipCode) {
this.zipCode = zipCode;
}
protected Object clone() throws CloneNotSupportedException {
EmpAddress clone=(EmpAddress)super.clone();
return clone;
}
}
public final class EmpAndAddress implements Cloneable {
private final int empId;
private final String empName;
private final EmpAddress eAddr;
public EmpAndAddress(int empId,String empName,EmpAddress eAddr){
super();
this.empId = empId;
this.empName = empName;
this.eAddr = new EmpAddress(" ", -1);
}
public int getEmpId() {
return empId;
}
public String getEmpName() {
return empName;
}
public EmpAddress geteAddr() throws CloneNotSupportedException {
return (EmpAddress) eAddr.clone();
}
}
答案 0 :(得分:1)
我看到的唯一问题是您实际上没有使用传递给EmpAddress
构造函数的EmpAndAddress
实例。我怀疑这是你的故意。
在任何情况下,尽管引用了一个可变对象,但要确保你的类是不可变的,那就是在构造函数中接收EmpAddress
实例时以及从{{1}返回实例时执行克隆方法。
您已经在geteAddr()
方法中执行此操作,因此您可以在这方面做得很好。
你所缺少的就是修复你的构造函数,如下所示:
geteAddr()
答案 1 :(得分:1)
MutabilityDetector代码正在检查该类是否是可传递的。这个类本身不可变是不够的。所有类字段的类型也必须是不可变的。假定字段引用的子对象是父对象状态的一部分,因此更改子对象会更改父对象。
在您的情况下,(假设)不可变类EmpAndAddress
有一个类型可变的字段。此外,EmpAndAddress
对象中的字段使用作为构造函数参数传递的值进行初始化。如果构造函数的调用者保留EmpAddress
引用,则它可以更改EmpAndAddress
对象的状态。
答案 2 :(得分:0)
完全披露:Mutability Detector的作者......这里有一个很好的答案。
如果我们从您在问题中定义的类开始,并断言它是不可变的,就像这样;
@Test
public void isImmutable() {
assertImmutable(EmpAndAddress.class);
}
您收到测试失败,并显示以下消息:
org.mutabilitydetector.unittesting.MutabilityAssertionError:
Expected: org.mutabilitydetector.stackoverflow.Question_32020847$EmpAndAddress to be IMMUTABLE
but: org.mutabilitydetector.stackoverflow.Question_32020847$EmpAndAddress is actually NOT_IMMUTABLE
Reasons:
Field can have a mutable type (org.mutabilitydetector.stackoverflow.Question_32020847$EmpAddress) assigned to it. [Field: eAddr, Class: org.mutabilitydetector.stackoverflow.Question_32020847$EmpAndAddress]
Allowed reasons:
None.
(我已将这两个类定义为静态内部类,因此在消息中显示为$ EmpAndAddress,但暂时忽略它。)
Another answer这个问题是完全正确的。 EmpAndAddress
被认为是可变的,因为EmpAddress
被认为是可变的,并且不可变对象的每个字段也应该是不可变的。由于以下几个原因,EmpAddress
是可变的:可以是子类;有公共非最终领域;二传手术方法。第一个问题是,为什么克隆getter中的EmpAddress
字段无助于使其不可变?嗯,在这种情况下,它确实使它变得不可变,但是Mutability Detector没有执行那种对它充满信心所需的分析。 Mutability Detector没有任何特殊的分析可以安全地克隆可变对象,让它们逃脱"给来电者。这是因为它很容易误用.clone
并引入可变性。想象一下,如果EmpAddress
有一个可变字段,如List
,你可以观察到如此突变:
@Test
public void go_on_try_to_mutate_me_now_i_have_a_list_field() throws Exception {
EmpAndAddress e = new EmpAndAddress(1234, "John Doe");
assertThat(e.geteAddr().getMyList(), is(Collections.<String>emptyList()));
e.geteAddr().getMyList().add("Haha, I'm mutating you");
assertThat(e.geteAddr().getMyList(), is(Collections.<String>emptyList())); // Fails because list now has one element in it
}
这是因为Object.clone
不执行深层复制。在这种情况下,它是唯一安全的,因为EmpAddress
中克隆的字段是不可变的(String
和原始int
)。 Mutability Detector可以尝试识别.clone
的安全使用,但它可能非常脆弱。因为它不能确信你的类是不可变的,所以Mutability Detector决定它是可变的。
你是正确的,创建一个类型为EmpAddress的新对象有助于使类不可变,因为它是&#34;保护&#34;实例,保持私有,没有其他代码可以访问它。如果构造函数接受了一个实例并将其分配给一个字段,那么将该参数传递给构造函数的人都可以对其进行修改,从而改变将其用于字段的任何实例。就像在这个例子中一样:
@Test
public void mutate_object_by_giving_it_a_parameter_then_modifying_it() throws Exception {
EmpAddress empAddress = new EmpAddress("New York", 1234);
EmpAndAddress e = new EmpAndAddress(1234, "John Doe", empAddress);
assertThat(e.geteAddr().getCity, is("New York"));
empAddress.setCity("Haha, I'm mutating you");
assertThat(e.geteAddr().getCity(), is("New York")); // fails because city has been changed
}
那么,该怎么办呢?有几种选择。
方法1:覆盖Mutability Detector,因为你知道更好
更改您的测试以添加&#34;允许的原因&#34;是可变的。也就是说,您对失败是假阳性感到满意,您希望捕获引入可变性的其他潜在错误,但忽略这种情况。为此,请添加如下代码:
@Test
public void isImmutable_withAllowedReason() {
assertInstancesOf(EmpAndAddress.class, areImmutable(),
AllowedReason.assumingFields("eAddr").areNotModifiedAndDoNotEscape());
}
在使用此选项之前,您应该非常确定这一点,如果您不熟悉不可变对象,我建议您不要这样做,这样您就可以学会更安全地创建不可变对象。
方法2:使EmpAddress
也是不可变的,如下所示:
@Immutable
public static final class ImmutableEmpAddress {
private final String empCity;
private final int zipCode;
public ImmutableEmpAddress(String empCity, int zipCode) {
this.empCity = empCity;
this.zipCode = zipCode;
}
public String getEmpCity() { return empCity; }
public int getZipCode() { return zipCode; }
}
然后当您将其作为EmpAndAddress
字段返回时,您不需要克隆它。这将是一个理想的解决方案。
方法3:创建不可变适配器
但是,在某些情况下,您无法使EmpAddress
不可变。也许代码在不同的库中,或者需要通过反射(例如Hibernate或其他JavaBean库)在其上设置字段的框架使用它。在这种情况下,您可以创建一个不可变的适配器,如下所示:
@Immutable
public static final class ImmutableEmpAddressAdapter {
public final String empCity;
public final int zipCode;
public ImmutableEmpAddressAdapter(EmpAddress mutableAddress) {
// perform a deep copy of every field on EmpAddress that's required to re-construct another instance
this.empCity = mutableAddress.getEmpCity();
this.zipCode = mutableAddress.getZipCode();
}
public EmpAddress getEmpAddress() {
return new EmpAddress(this.empCity, this.zipCode);
}
}
然后在EmpAndAddress
看起来像这样:
public static final class EmpAndAddress {
// some code ommitted for brevity
private final ImmutableEmpAddressAdapter immutableEmpAddressAdapter;
public EmpAndAddress(int empId, String empName){
this.immutableEmpAddressAdapter = new ImmutableEmpAddressAdapter(new EmpAddress(" ", -1));
}
public EmpAddress geteAddr() {
return immutableEmpAddressAdapter.getEmpAddress();
}
}
虽然这种技术需要更多的代码,但是对于这个类的其他读者来说,EmpAddress
具有不可变的字段并且不依赖于Object.clone
的脆弱行为< / p>
如果您想使类不可变,这也是一种很好的技术,但您必须进行许多代码更改才能执行此操作。这允许您在代码库中的越来越多的地方逐渐引入不可变版本,直到最终原始的可变类仅用于系统的边缘,甚至完全消失。
我希望这个anwser和Mutability Detector在学习如何创建不可变对象时非常有用。