88

我读到要使 Java 中的类不可变,我们应该执行以下操作,

  1. 不提供任何设置器
  2. 将所有字段标记为私有
  3. 使课程最终

为什么需要第 3 步?我为什么要给班级打分final

4

11 回答 11

147

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

public class Immutable {
     private final int value;

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

     public int getValue() {
         return value;
     }
}

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

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());
    }
}

请注意,在我的Mutable子类中,我重写了getValue读取子类中声明的新的可变字段的行为。结果,您的类最初看起来是不可变的,但实际上并不是不可变的。我可以将这个Mutable对象传递到任何需要对象的地方Immutable,假设该对象是真正不可变的,这可能会对代码造成非常糟糕的事情。标记基类final可以防止这种情况发生。

希望这可以帮助!

于 2012-09-06T19:02:56.587 回答
55

与许多人认为的相反,不需要创建一个不可变final类。

制作不可变类的标准论点final是,如果你不这样做,那么子类可以增加可变性,从而违反了超类的契约。该类的客户将假定不变性,但当某些东西从他们下面发生变异时会感到惊讶。

如果你把这个论点带到它的逻辑极端,那么所有的方法都应该被创建final,否则一个子类可能会以一种不符合其超类契约的方式覆盖一个方法。有趣的是,大多数 Java 程序员认为这很荒谬,但对不可变类应该是final. final我怀疑这与 Java 程序员一般不完全接受不变性的概念有关,也许与Java 中关键字的多重含义有关的某种模糊思维有关。

遵守超类的合同并不是编译器可以或应该始终强制执行的事情。编译器可以强制执行合同的某些方面(例如:最小的方法集及其类型签名),但是编译器无法强制执行典型合同的许多部分。

不变性是类契约的一部分。它与人们更习惯的一些事情有点不同,因为它说明了类(和所有子类)不能做什么,而我认为大多数 Java(通常是 OOP)程序员倾向于将合同视为与一个类可以做什么,而不是它不能做什么。

不变性不仅影响单个方法——它影响整个实例——但这与Java 的工作方式equals和工作方式并没有太大的不同。hashCode这两种方法在Object. 这份合同非常仔细地列出了这些方法不能做的事情。该合同在子类中更加具体。它很容易被覆盖equalshashCode以违反合同的方式。事实上,如果您只覆盖这两种方法中的一种而没有另一种,那么您很可能违反了合同。那么应该equals并且hashCode已经被宣布finalObject避免这种情况?我想大多数人会争辩说他们不应该。同样,没有必要创建不可变类final.

也就是说,您的大多数类,无论是否不可变,都可能应该final. 请参阅Effective Java Second Edition第 17 条:“设计和记录继承或禁止它”。

因此,第 3 步的正确版本应该是:“使类成为最终类,或者在设计子类时,清楚地记录所有子类必须继续是不可变的。”

于 2012-09-08T02:30:10.640 回答
23

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

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

最好将您的属性标记为私有和最终的,如果您想保护“合同”,请将您的吸气剂标记为最终的。

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

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

于 2013-03-07T21:21:25.687 回答
5

如果它不是最终的,那么任何人都可以扩展该类并做任何他们喜欢的事情,例如提供设置器、隐藏您的私有变量以及基本上使其可变。

于 2012-09-06T19:01:17.890 回答
4

这限制了其他类扩展你的类。

final 类不能被其他类扩展。

如果一个类扩展了您想要使其不可变的类,则由于继承原则,它可能会改变类的状态。

只需澄清“它可能会改变”。子类可以覆盖超类行为,例如使用方法覆盖(如 templatetypedef/Ted Hop 答案)

于 2012-09-06T18:59:55.703 回答
4

如果你不让它最终我可以扩展它并使其不可变。

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;
  }
}

现在,我可以将 FakeImmutable 传递给任何期望 Immutable 的类,并且它不会像预期的合约那样表现。

于 2012-09-06T19:05:38.000 回答
4

对于创建不可变类,不必将类标记为final.

让我从标准库本身举一个这样的例子: BigInteger是不可变的,但它不是final

实际上,不变性是一个概念,根据该概念,对象一旦创建就无法修改。

让我们从 JVM 的角度来思考。从 JVM 的角度来看,这个类的对象应该在任何线程可以访问它之前完全构造,并且对象的状态在构造之后不应该改变。

不变性意味着一旦创建对象就无法更改其状态,这是通过三个拇指规则实现的,这些规则使编译器认识到类是不可变的,它们如下:

  • 所有非private字段都应该是final
  • 确保类中没有可以直接或间接更改对象字段的方法
  • 类中定义的任何对象引用都不能从类外部修改

有关详细信息,请参阅此 URL

于 2016-07-22T07:23:05.837 回答
1

假设您有以下课程:

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class PaymentImmutable {
    private final Long id;
    private final List<String> details;
    private final Date paymentDate;
    private final String notes;

    public PaymentImmutable (Long id, List<String> details, Date paymentDate, String notes) {
        this.id = id;
        this.notes = notes;
        this.paymentDate = paymentDate == null ? null : new Date(paymentDate.getTime());
        if (details != null) {
            this.details = new ArrayList<String>();

            for(String d : details) {
                this.details.add(d);
            }
        } else {
            this.details = null;
        }
    }

    public Long getId() {
        return this.id;
    }

    public List<String> getDetails() {
        if(this.details != null) {
            List<String> detailsForOutside = new ArrayList<String>();
            for(String d: this.details) {
                detailsForOutside.add(d);
            }
            return detailsForOutside;
        } else {
            return null;
        }
    }

}

然后你扩展它并打破它的不变性。

public class PaymentChild extends PaymentImmutable {
    private List<String> temp;
    public PaymentChild(Long id, List<String> details, Date paymentDate, String notes) {
        super(id, details, paymentDate, notes);
        this.temp = details;
    }

    @Override
    public List<String> getDetails() {
        return temp;
    }
}

在这里我们对其进行测试:

public class Demo {

    public static void main(String[] args) {
        List<String> details = new ArrayList<>();
        details.add("a");
        details.add("b");
        PaymentImmutable immutableParent = new PaymentImmutable(1L, details, new Date(), "notes");
        PaymentImmutable notImmutableChild = new PaymentChild(1L, details, new Date(), "notes");

        details.add("some value");
        System.out.println(immutableParent.getDetails());
        System.out.println(notImmutableChild.getDetails());
    }
}

输出结果将是:

[a, b]
[a, b, some value]

正如您所看到的,虽然原始类保持其不变性,但子类可以是可变的。因此,在您的设计中,您无法确定您正在使用的对象是不可变的,除非您将您的类设为 final。

于 2019-09-13T12:51:25.830 回答
0

假设以下类不是final

public class Foo {
    private int mThing;
    public Foo(int thing) {
        mThing = thing;
    }
    public int doSomething() { /* doesn't change mThing */ }
}

它显然是不可变的,因为即使子类也不能修改mThing. 但是,子类可以是可变的:

public class Bar extends Foo {
    private int mValue;
    public Bar(int thing, int value) {
        super(thing);
        mValue = value;
    }
    public int getValue() { return mValue; }
    public void setValue(int value) { mValue = value; }
}

现在,可分配给类型变量的对象Foo不再保证是可变的。这可能会导致诸如散列、相等、并发等问题。

于 2012-09-06T19:06:38.170 回答
0

设计本身没有价值。设计总是用来实现一个目标。这里的目标是什么?我们想减少代码中的意外数量吗?我们想防止错误吗?我们是否盲目遵守规则?

此外,设计总是要付出代价的。每一个名副其实的设计都意味着你有一个目标冲突

考虑到这一点,您需要找到以下问题的答案:

  1. 这将防止多少明显的错误?
  2. 这将防止多少微妙的错误?
  3. 这多久会使其他代码更复杂(= 更容易出错)?
  4. 这会使测试更容易还是更难?
  5. 您项目中的开发人员有多优秀?他们需要多少大锤指导?

假设您的团队中有许多初级开发人员。他们会拼命尝试任何愚蠢的事情,只是因为他们还不知道解决问题的好方法。使类最终可以防止错误(好),但也可以使他们想出“聪明”的解决方案,例如将所有这些类复制到代码中各处的可变类中。

另一方面,在一个类被广泛使用之后创建一个类将是非常困难的,但是如果你发现你需要扩展它final就很容易创建一个final类。final

如果您正确使用接口,则可以通过始终使用接口,然后在需要时添加可变实现来避免“我需要使这个可变”问题。

结论:这个答案没有“最佳”解决方案。这取决于你愿意付出什么样的代价以及你必须付出什么样的代价。

于 2017-01-26T10:09:43.923 回答
0

equals() 的默认含义与引用相等相同。对于不可变数据类型,这几乎总是错误的。所以你必须重写 equals() 方法,用你自己的实现替换它。关联

于 2018-05-09T21:37:58.897 回答