为什么在Java中声明一个不可变的类final?

Ana*_*and 76 java final immutability

我读到要在Java中创建一个不可变,我们应该执行以下操作,

  1. 不要提供任何安装者
  2. 将所有字段标记为私有
  3. 让课程最终

为什么需要第3步?我为什么要上课final

tem*_*def 125

如果你没有标记该类final,我可能会突然让你看似不可变的类实际上是可变的.例如,考虑以下代码:

public class Immutable {
     private final int value;

     public Immutable(int value) {
         this.value = value;
     }

     public int getValue() {
         return value;
     }
}
Run Code Online (Sandbox Code Playgroud)

现在,假设我执行以下操作:

public class Mutable extends Immutable {
     private int realValue;

     public Mutable(int value) {
         super(value);

         realValue = value;
     }

     public int getValue() {
         return realValue;
     }
     public void setValue(int newValue) {
         realValue = newValue;
     }

    public static void main(String[] arg){
        Mutable obj = new Mutable(4);
        Immutable immObj = (Immutable)obj;              
        System.out.println(immObj.getValue());
        obj.setValue(8);
        System.out.println(immObj.getValue());
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,在我的Mutable子类中,我已经重写了getValue读取在我的子类中声明的新的可变字段的行为.因此,您的类最初看起来是不可变的,实际上并不是一成不变的.我可以MutableImmutable期望对象的任何地方传递此对象,这可能会做非常糟糕的事情来代码,假设对象是真正不可变的.标记基类final可防止这种情况发生.

希望这可以帮助!

  • @ anand-想象一下,你有一个带有"不可变"参数的函数.我可以将一个`Mutable`对象传递给该函数,因为`Mutable扩展了Immutable`.在该函数内部,当您认为您的对象是不可变的时,我可以有一个辅助线程,并在函数运行时更改值.我还可以给你一个函数存储的`Mutable`对象,然后在外部改变它的值.换句话说,如果你的函数假设值是不可变的,它可能很容易破坏,因为我可以给你一个可变对象并在以后更改它.那有意义吗? (18认同)
  • 如果我做Mutable m = new Mutable(4); m.setValue(5); 在这里,我正在玩Mutable类对象,而不是Immutable类对象.所以,我仍然困惑为什么Immutable类不是不可变的 (12认同)
  • @ anand-传递`Mutable`对象的方法不会改变对象.值得关注的是,该方法可能会假定对象在不存在时是不可变的.例如,该方法可能会假设,因为它认为对象是不可变的,所以它可以用作`HashMap`中的键.然后,我可以通过传入一个`Mutable`,等待它将对象存储为键,然后更改`Mutable`对象来打破该函数.现在,查找"HashMap"将失败,因为我更改了与对象关联的键.那有意义吗? (5认同)
  • @ templatetypedef-是的,我现在得到了..必须说这是一个很棒的解释..虽然抓了一点时间来抓住它 (3认同)

Lau*_*ves 39

相反的是,许多人认为,做一个不变类final必需的.

创建不可变类的标准参数final是,如果不这样做,则子类可能会增加可变性,从而违反了超类的约定.该类的客户将承担不变性,但当从它们下面突变出来时会感到惊讶.

如果将此参数置于其逻辑极值,则应该生成所有方法final,否则子类可以以不符合其超类合同的方式覆盖方法.有趣的是,大多数Java程序员认为这是荒谬的,但对于不可变类应该是这样的想法是不行的final.我怀疑它与Java程序员有关,一般而言,对于不可变性的概念并不完全感到满意,也许是某种与finalJava中关键字的多重含义有关的模糊思考.

符合您的超类合同不是可以或应该始终由编译器强制执行的.编译器可以强制执行合同的某些方面(例如:最小的方法集和类型签名),但是编译器无法执行典型合同的许多部分.

不变性是一个阶级合同的一部分.这是从一些东西的人更习惯有点不同,因为它说什么类(以及所有子类),不能做什么,而我认为大多数Java(一般OOP)的程序员往往会去想合同的有关一个班级可以做什么,而不是它不能做什么.

不变性也不仅仅影响一个方法 - 它影响整个实例 - 但这equalshashCodeJava工作方式和方法并没有太大的不同.这两种方法都有一份具体的合同Object.这份合同非常仔细地列出了这些方法无法做到的事情.该子合约在子类中更具体.覆盖equalshashCode以违反合同的方式很容易.事实上,如果你只重写这两种方法中的一种而没有另一种方法,那么很可能你违反了合同.所以应该equalshashCode已经宣布finalObject避免这种情况?我想大多数人都认为他们不应该这样做.同样,没有必要创建不可变类final.

也就是说,你的大多数课程,不论是否一成不变,都应该final.请参阅Effective Java Second Edition第17项:"继承的设计和文档,否则禁止它".

因此,步骤3的正确版本将是:"使类最终,或者在设计子类时,清楚地记录所有子类必须继续是不可变的."

  • 值得注意的是,Java“期望”但并不强制执行,给定两个对象“ X”和“ Y”,“ X.equals(Y)”的值将是不可变的(只要“ X”和“ Y”继续引用相同的对象)。对于哈希码,它也有类似的期望。显然,没有人应该期望编译器强制实现等价关系和哈希码的不变性(因为它不能做到)。我认为人们没有理由期望该类型的其他方面会强制实施它。 (3认同)
  • +1 - 我一直和你在一起直到布洛赫报价.我们不必如此偏执.99%的编码是合作的.如果有人真的需要覆盖某些东西,那就没什么大不了的.我们99%的人都没有编写像String或Collection这样的核心API.不幸的是,很多Bloch的建议都基于这种用例.它们对大多数程序员来说并不是很有帮助,尤其是新程序员. (3认同)
  • 我最喜欢这个解释。总是创建一个不可变的类“final”会限制它的用处,尤其是当你设计一个要扩展的 API 时。为了线程安全,让你的类尽可能不可变是有意义的,但要保持它的可扩展性。您可以将字段设为“protected final”而不是“private final”。然后,明确地建立一个关于遵守不变性和线程安全保证的子类的契约(在文档中)。 (2认同)

Jus*_*hms 20

不要将整个班级标记为最终.

如某些其他答案所述,允许扩展不可变类是有正当理由的,因此将该类标记为final并不总是一个好主意.

最好将您的属性标记为私有和最终,如果您想保护"合同",请将您的获取者标记为最终.

通过这种方式,您可以允许扩展类(甚至可能是可变类),但是类的不可变方面是受保护的.属性是私有的,无法访问,这些属性的getter是final,无法覆盖.

使用不可变类的实例的任何其他代码将能够依赖于类的不可变方面,即使它传递的子类在其他方面是可变的.当然,因为它需要你的课程的一个实例,它甚至不知道这些其他方面.

  • 我宁愿将所有不可变的方面封装在一个单独的类中,并通过组合的方式使用它。这比在单个代码单元中混合可变和不可变状态更容易推理。 (2认同)
  • 完全同意。如果您的可变类包含不可变类,那么组合将是实现此目的的一种非常好的方法。如果您的结构是相反的,那么这将是一个很好的部分解决方案。 (2认同)

Rog*_*sjö 6

如果您没有将其定为最终版本,我可以扩展它并使其不可变。

public class Immutable {
  privat final int val;
  public Immutable(int val) {
    this.val = val;
  }

  public int getVal() {
    return val;
  }
}

public class FakeImmutable extends Immutable {
  privat int val2;
  public FakeImmutable(int val) {
    super(val);
  }

  public int getVal() {
    return val2;
  }

  public void setVal(int val2) {
    this.val2 = val2;
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,我可以将 FakeImmutable 传递给任何期望 Immutable 的类,并且它不会按照预期的契约运行。

  • 我认为这与我的答案几乎相同。 (2认同)

dig*_*oel 5

如果它不是最终的那么任何人都可以扩展课程并做任何他们喜欢的事情,比如提供setter,遮蔽你的私有变量,并且基本上使它变得可变.