什么是不可变的?
不变类是不能修改其实例的类。创建对象时会提供存储在不可变对象中的信息,此后,该信息将永远不变且只读。由于我们无法修改不可变的对象,因此我们需要解决此问题。例如,如果我们有一个太空飞船类,并且想要更改其位置,则必须返回一个具有修改信息的新对象。
public Spaceship exploreGalaxy() {
return new Spaceship(name, Destination.OUTER_SPACE);
}
例子1
不可变的优点
乍一看,您会认为不可变是没有用的,但是,它们提供了许多优点。
首先,不可变的类大大减少了实现稳定且容错的系统所需的精力。创建此类系统时,阻止不更改的不可变属性非常有用。
想象一下,我们正在为大型银行开设Bank类。金融危机过后,银行害怕让其用户产生负余额。因此,他们制定了一条新规则并添加了一个验证方法,以在函数调用导致负余额时抛出IllegalArgumentException。这种类型的规则称为不变式。
public class BankAccount{
[...]
private void validate(long balance) {
if (balance < 0) {
throw new IllegalArgumentException("balance must not be negative:"+ balance);
}
}
}
例子2
在典型的类中,每次更改用户余额时都会调用validate()方法。如果用户提款,偿还债务或从其帐户转帐,我们将必须调用validate方法。但是,对于不可变的类,我们只需要在类构造函数中调用一次validate方法。
public BankAccount(long balance) {
validate(balance);
this.balance = balance;
}
例子3
由于不可变的对象永远不会改变,因此此条件在对象的整个生命周期内都适用。不需要进一步的验证。每当调用修改余额的方法时,都会返回一个新对象,再次调用构造函数并重新验证该对象。这非常有用,因为它允许我们集中所有不变量,并确保对象在整个生命周期中都是一致的。
同样,不可变变量可用于支持容错系统。假设您尝试从银行提款,但是在从帐户中提款到从自动柜员机中提款之间存在错误。在普通班级,您的钱将永远消失。帐户对象已更改,为时已晚。但是在一个不变的类中,您可能会引发错误,从而防止您的帐户在实际收到之前就亏钱了。
public ImmutableAccount withdraw(long amount) {
long newBalance = newBalance(amount);
return new ImmutableAccount(newBalance);
}
private long newBalance(long amount) {
// exception during balance calculation
}
例子4
不可变的对象永远不会进入不一致的状态,即使在发生异常的情况下也是如此。这样可以稳定我们的系统,并消除了无法预料的错误使整个系统不稳定的威胁。除了初始验证的成本外,这种稳定性是免费的。
不可变的第二个优点是它们可以在对象之间自由共享。假设我们创建了一个帐户对象的副本。复制对象时,我们使两个对象共享同一余额对象。
例子5
但是,由于两个对象都是不可变的,因此如果我们更改其中一个对象的平衡,则不会影响另一个对象。另一个对象创建该类的新的不可变实例,并且这两个对象没有关联。
出于同样的原因,不可变对象在复制对象时不需要复制构造函数。在多线程环境中使用无锁算法时,甚至可以自由共享不可变对象,在多线程环境中,多个动作并行发生。
最后,不可变对象也是用作Map键和Set元素的理想选择,因为Map键和Set元素绝不能更改。
不可变的缺点
正如您可能已经意识到的那样,不可变的刚性可能是一项巨大的资产,但也可能是不利的。不可变项的最大弱点是它们可能导致性能问题。每次您有新的类状态时,都需要创建一个新对象。因此,与创建可变对象相比,您通常需要创建更多不变的对象。从逻辑上讲,创建的对象越多,使用的系统资源就越多。
这可能是一个问题,也可能不是问题,这取决于多种因素。你想做什么?您的程序运行哪种硬件?您要构建台式机还是Web应用程序?您的程序有多大?这些因素的组合决定了使类不可变是否会导致性能问题。通常,您应该尝试尽可能多地利用不可变对象。首先,使每个类不可变,并通过使用简单方法创建小的类来促进其不可变。简洁和干净的代码是关键。如果您有干净的代码,则可以促进不变性。如果您有不可变项,那么您的代码会更干净。一旦有了程序,就对其进行测试。查看性能如何,如果性能不令人满意,请逐渐放松不变性规则。
如何使一成不变
既然我已经向您展示了为什么不可变的价值何在,以及何时使用它们,我将向您展示如何制作不可变的类。让我们经历一下将可变类太空船变成不可变类的过程:
public class Spaceship {
public String name;
public Destination destination;
public Spaceship(String name) {
this.name = name;
this.destination = Destination.NONE;
}
public Spaceship(String name, Destination destination) {
this.name = name;
this.destination = destination;
}
public Destination currentDestination() {
return destination;
}
public Spaceship exploreGalaxy() {
destination = Destination.OUTER_SPACE;
}
[…]
}
例子7
要将可变类变成不可变类,应遵循以下四个步骤:
1.
将所有字段设为私有和最终
2.
3.
不要提供任何修改对象状态的方法
4.
5.
确保该类不能扩展
6.
7.
确保对任何可变字段的独占访问
8.
将所有字段设为私有和最终
使可变类不变的第一步是将其所有字段更改为私有和最终字段。我们将变量设为私有,这样就不能从类外部访问它们。如果可以从班级外部访问它们,则可以更改它们。我们还将字段定为最终字段,以明确表达我们永远不希望在班级中重新分配它们。如果有人尝试重新分配参考变量,则会发生编译器错误。
private final String name;
private final Destination destination;
例子八
不要提供任何修改对象状态的方法
下一步是投影对象的状态,以防被修改。如前所述,根据定义,不可变对象不能修改其对象的状态。只要您拥有修改对象状态的方法,就必须返回一个新对象。
public ImmutableSpaceship exploreGalaxy() {
return new ImmutableSpaceship(name, Destination.OUTER_SPACE);
}
例子9
我们不需要更改的任何字段(例如名称)都可以直接从当前对象中复制。如前所述,这是因为不可变的对象可以自由共享字段。我们确实更改的字段需要初始化为新对象。
确保该类不能扩展
为了防止我们的班级被改变,我们还需要保护我们的班级不被扩展。如果可以扩展一个类,则可以覆盖该类中的方法。重写的方法可能会修改我们的对象,这违反了不变性规则。让我们来看一个代码示例:
public class EvilSpaceship extends Spaceship {
[...]
@Override
public EvilSpaceship exploreGalaxy() {
this.destination = Destination.OUTER_SPACE;
return this;
}
}
例子十
为了阻止这艘EvilSpaceship破坏我们神圣的一成不变,请将课程定为最终课程。
public final class Spaceship
例子11
确保对可变字段的独占访问
确保不变性的最后一步是保护我们对可变字段的访问。请记住,不可变的字段可以自由共享,因此,如果我们具有独占访问权限就没有关系。拥有对可变字段的访问权限的任何人都可以更改它,从而修改我们的不可变对象。为了防止任何人直接访问可变字段,我们绝不应该获取或返回对Destination对象的直接引用。相反,我们必须创建可变对象的深层副本,然后使用它。只要不直接共享可变对象,外部对象内部的更改就不会对我们的不可变对象产生任何影响。为了实现独占访问,我们必须检查所有公共方法和构造函数是否有任何传入或传出的Destination引用。
公共构造函数未收到任何目标引用。它创建的Destination对象是安全的,因为无法从外部访问它。因此,公共构造函数实际上是好的。
但是,在currentDestination()方法中,我们返回了Destination对象,这是一个问题。创建实际目标的深层副本,然后返回对该副本的引用,而不是返回真实引用。
public Destination currentDestination() {
return new Destination(destination);
}
例子12
我们仍然拥有的最后一个公共方法是newDestination()方法。它接收一个Destination引用,并将其直接转发给我们的构造函数。这意味着它所引用的对象与调用此方法的对象相同。为了防止这种情况,我们可以在此方法中进行深拷贝,也可以在构造函数中进行深拷贝。我将在构造函数中实现此更改:
private ImmutableSpaceship(String name, Destination destination) {
this.name = name;
this.destination = new Destination(destination);
}
例子13
最好在私有构造函数中进行此更改,因为现在,如果我们创建其他修改目标的方法,它们还将自动创建此可变字段的深层副本。
现在,我们已经完全确保了班级的不变性:
public final class ImmutableSpaceship {
private final String name;
private final Destination destination;
public ImmutableSpaceship(String name) {
this.name = name;
this.destination = new Destination("NONE");
}
private ImmutableSpaceship(String name, Destination destination) {
this.name = name;
this.destination = new Destination(destination);
}
public Destination currentDestination() {
return new Destination(destination);
}
public ImmutableSpaceship newDestination(Destination newDestination) {
return new ImmutableSpaceship(this.name, newDestination);
}
[…]
}
例子14
希望您现在对不可变具有更好的理解,并了解它们的实用性。请记住,通过最大程度地使用不可变项来保持代码的简洁明了。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。