java 不可变类有什么用_Java中的不可变总结

什么是不可变的?

不变类是不能修改其实例的类。创建对象时会提供存储在不可变对象中的信息,此后,该信息将永远不变且只读。由于我们无法修改不可变的对象,因此我们需要解决此问题。例如,如果我们有一个太空飞船类,并且想要更改其位置,则必须返回一个具有修改信息的新对象。

0b72afa0d97c76db5e7feb13952393d0.png

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.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.destination = new Destination("NONE");

private ImmutableSpaceship(String name, Destination destination) {

this.destination = new Destination(destination);

return new Destination(destination);

public ImmutableSpaceship newDestination(Destination newDestination) {

return new ImmutableSpaceship(this.name, newDestination);

例子14

希望您现在对不可变具有更好的理解,并了解它们的实用性。请记住,通过最大程度地使用不可变项来保持代码的简洁明了。

最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。

0da8faf44bcf3112b1a7b253a8ba6c3c.png

c2aac9858a7962377b043aad7f06b33e.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值