不可变的类和子类

我正在尝试学习可变/不可变类,我遇到了这篇文章

提供的部分答案是:

如果要强制实现不变性,则不能拥有子类。 请参阅例如java.lang.String,这是一个最终类,原因如下:为了防止人们对String进行子类化以使其可变。

好吧,我理解, 但是 ,你将如何处理这个问题。 假设您被赋予了创建3个Employee类,Accountant,ITDepartment和QualityAssurance的任务。 现在,您可以创建一个名为Employee的抽象类,其中包含所有人共享的常用方法(员工ID,姓名,工资等),但是,您的类不再是不可变的。

使用Java,那你怎么解决这个问题呢? 你会创建3个类,使它们成为最终类,并且不实现抽象方法吗? (所以,没有任何子类化)或者你会使用接口,并只提供getter?

如果要强制实现不变性,则不能拥有子类。

这几乎是正确的,但并非完全如此。 重申一下:

如果要强制实现不变性,则必须确保所有子类都是不可变的。

允许子类化的问题在于,通常任何可以创建类的人都可以子类化任何公共非final类。

但是所有子类都必须调用其超类的构造函数之一。 包私有构造函数只能由同一包中的子类调用。

如果您密封包以便控制包中的哪些类,则可以限制子类化。 首先定义一个你想要子类的类:

public abstract class ImmutableBaseClass { ImmutableBaseClass(...) { ... } } 

由于所有子类都必须能够访问超级构造函数,因此可以确保您定义的包中的所有子类遵循不可变的规则。

 public final class ImmutableConcreteClass extends ImmutableBaseClass { public ImmutableConcreteClass(...) { super(...); } } 

要将此应用于您的示例,

 public abstract class Employee { private final Id id; private final Name name; // Package private constructor in sub-classable class. Employee(Id id, Name name, ...) { // Defensively copy as necessary. } } public final class Accountant extends Employee { // Public constructos allowed in final sub-classes. public Accountant(Id id, Name name, ...) { super(id, name, ...); // Call to super works from same package. } } public final class ITWorker extends Employee { // Ditto. public ITWorker(Id id, Name name, ...) { super(id, name, ...); } } 

值得思考为什么不可变性 – 通常是因为你需要假设的数据不会改变。 执行此操作的一种方法是使用包含特定于子类的详细信息的非最终成员创建最终的Employee类:

 public final class Employee { private final long employeeId; private final String firstName; private final String lastName; private final DepartmentalDetails details; public Employee(long employeeId, String firstName, String lastName, DepartmentalDetails details) { super(); this.employeeId = employeeId; this.firstName = firstName; this.lastName = lastName; this.details = details; } } abstract class DepartmentalDetails { } final class AccountantDetails extends DepartmentalDetails { // Things specific to accountants } final class ITDetails extends DepartmentalDetails{ // Things specific to IT } final class QualityAssuranceDetails extends DepartmentalDetails{ // Things specific to QA } 

这在技术上是不可变的(因为实现者可以编写DepartmentalDetails的可变实现),但它确实封装了可变性,因此它提供了相同的好处,同时允许一些可扩展性。 (这与组合与inheritance的概念有关,虽然我不相信这种模式通常是如何使用的。)

我认为值得考虑的另一种可能性 – 您可以按照您的建议制作三个子类,使它们都是最终的,并在抽象类上贴上一个大评论,说所有实现都应该是不可变的。 它不是防弹的,但在一个小型开发项目中,复杂性节省可能值得冒小风险。

java.lang.String很特别,非常特别 – 它是一种在任何地方使用的基本类型。 特别是,java安全框架在很大程度上依赖于字符串,因此每个人都看到String的相同内容是至关重要的,即String必须是不可变的(即使在不安全的发布下!)(不幸的是很多人盲目地应用那些严格的要求字符串到他们自己的类,通常是不合理的)

即便如此,如果String可以被子类化,这并不是什么大问题,只要String中的所有方法都是最终的,因此子类不会弄乱String应该是什么样的。 如果我接受一个String,你给我一个String的子类,我不在乎你在子类中做了什么; 只要String超类的内容和行为没有调整。

当然,作为这样一种基本类型,将String标记为final是明智的,以避免所有混淆。


在您的用例中,您可以拥有一个抽象的Employee,并确保所有子类都实现为不可变的。 在运行时,任何Employee对象都必须属于具有不可变性的具体子类。

如果,你无法控制谁是Employee的子类,并且你怀疑他们要么是不知道他们在做什么的蠢货,要么是想要造成麻烦的坏人,你至少可以保护Employee中的部分,这样子类就不会弄乱它起来。 例如,无论子类做什么,Employee的名称都是不可变的 –

 final String name; protected Employee(String name) { this.name = name; } public final String getName(){ return name; } // final method! 

然而,大多数程序员在大多数应用程序中这种防御性设计通常是不合理的。 我们不必如此偏执。 99%的编码是合作的。 如果有人真的需要覆盖某些东西,那就没什么大不了的。 我们99%的人都没有编写像String或Collection这样的核心API。 不幸的是,很多Bloch的建议都基于这种用例。 这些建议对大多数程序员来说并没有多大帮助,尤其是新程序员,他们可以使用内部应用程序。