不可变类和子类

9

我正在学习可变/不可变类,然后我发现了这篇文章

其中提供的答案部分是:

如果你想强制不可变性,则不能拥有子类。例如java.lang.String,它是一个final class,原因在于:为了防止人们子类化String使其变得可变。

好的,我明白了但是,如果你需要创建三个员工类(Accountant, ITDepartment和QualityAssurance),该怎么处理这个问题?你可以创建一个名为Employee的抽象类,其中包含所有公共方法(员工ID、名称、薪水等),但是你的类不再是不可变的。

在Java中,你会如何解决这个问题?你会创建这3个类,使它们变成final,并且不实现抽象方法吗?(因此,根本不允许子类化)还是使用接口,只提供getters?


@Cruncher - 我的猜测是创建这3个类,但作为最终方案可能不是最佳选择。 - user5078574
2
使用接口没有任何问题。是的,接口不能保证所有实现都是不可变的,但您可以保证您提供的所有实现都是不可变的。其他实现不是您的问题。 - biziclop
1
@Cruncher 你可以让你的代码无懈可击。但是你无法提前让其他代码无懈可击。没有办法强制所有接口实现者都不可变,也没有真正需要这样做。 - biziclop
3
两个问题浮现在脑海中:(1)将代表员工的类设置为不可变的是否合理?人们会认为,员工的状态在其就业期间会发生变化。 (2)为什么会计师/IT部门/质量保证应该继承自员工而不是成为其属性("职位")? - RealSkeptic
显示剩余18条评论
4个回答

7
如果你想实现不可变性,就不能有子类。
这几乎是正确的,但并非完全如此。重新陈述一下:
如果你想实现不可变性,你必须确保所有子类都是不可变的。
允许子类化的问题在于通常任何可以创建类的人都可以对任何公共的非 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, ...);
  }
}

感谢您提供的代码,为了更好地理解,您能否使用员工/质量保证示例进行演示?其次,在某些情况下,例如我在帖子中提到的示例,您可以创建可变类并将其保留吗?只要您添加了线程安全的代码即可。 - user5078574
我正在阅读《Effective Java》,它说要优先使用接口而不是抽象类。现在,如果我要使用接口,并且在接口中只提供getter和某些方法,那么这样做可以吗?前提是我的类中的变量是私有的final。这样,创建后就无法更改这些类。 - user5078574
@Nexusfactor,不行。除了限制整个接口的可见性以外,没有办法限制实现接口的类,这会使您的客户端无法提及基础类型。 - Mike Samuel
为了我的理解,你能否向我展示如何在这种情况下进行防御性拷贝?我能不能只将getter设置为final? - user5078574
在防御性复制方面,如果您的输入是可变的,那么您不能依赖于调用构造函数后修改它的人,因此您需要进行复制。 - Mike Samuel
显示剩余2条评论

2

值得思考为什么不可变性是可取的——通常是因为您拥有数据,需要假定这些数据不会改变。 一种实现方法是创建一个最终的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实现),但它确实封装了可变性,因此提供了相同的好处,同时允许一定程度的可扩展性。 (这与组合与继承的概念相关,尽管我不认为这种模式通常是如何使用的。)

我认为还有另一种可能值得考虑 - 您可以像您建议的那样制作三个子类,将它们全部设为final,并在抽象类上附加一个大注释,说明所有实现都应该是不可变的。这并非万无一失,但对于小型开发项目来说,复杂性的减少很可能值得冒这个风险。


谢谢你提供的代码。我也在想,如果将IT、QA和会计师都作为最终类而不是抽象员工,这样做是否可行? - user5078574
我相信你可以让它工作,但这意味着需要在其他地方大量切换代码来处理它们。而且很可能很难添加新的员工类型,需要更改处理这些对象的系统的每个部分。这表明它非常脆弱,很可能会在长期运行中给你带来痛苦(和费用)。 - hugh
话虽如此,如果您想采用低技术方法,您可以始终创建一个包含“员工类型”和每个子类型的可选字段的大型不可变Employee类。 (如果您想将对象持久化到SQL表中,则此方法非常有效,并且是JPA中存储子类的常见模式之一)。 - hugh
就我个人而言,我很乐意使用注释——文档也是完成任务的有效方式!在现实世界的场景中,我预计很少在线程之间共享实例,更有可能从持久存储中加载和保存它们,并担心如何处理不同线程更新数据库中相同记录的情况。 - hugh
我不会说“不理想”,更多的是理想主义。这是一个好的属性,但请确保您知道它对您的任务的价值,并且不要仅仅因为原则而陷入困境。 - hugh
显示剩余5条评论

2

java.lang.String 很特殊,非常特殊 - 它是一个在各个地方广泛使用的基本类型。特别是 Java 安全框架严重依赖于字符串,因此每个人都看到相同的字符串内容至关重要,也就是说,字符串必须是不可变的(即使在不安全的发布下!)(不幸的是,很多人盲目地将字符串的这些严格要求应用于他们自己的类中,经常是没有理由的)

即使如此,如果可以对 String 进行子类化,并且 String 中的所有方法都是 final 的,那么问题也不是很大,这样子类就无法操纵 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的许多建议都是基于这种用例的。这些建议对于大多数程序员,特别是新手,正在开发内部应用程序的人来说并不是真正有帮助的。


0

Java 17和"密封类和接口"特性

我认为,借助Java 17和"密封类和接口"特性,在使用继承时更容易遵循不可变性。

现在,我们可以限制能够继承父类的子类,只允许我们信任的类。

让我以建议实体和该特性为基础展示一个示例(请注意MutabilityBreaker,由于Employee类的限制,它无法编译):

/**
 * Class that we would like to be immutable.
 * Pay attention on 'permits' section
 */
public sealed class Employee permits Accountant, ITWorker {

    private final String id;

    public Employee(String id) {
        this.id = id;
    }

}

/**
 * Class that we would like to inherit
 * from {@link Employee} and not break immutability
 */
public final class Accountant extends Employee {

    private final List<String> qualifications;

    // I'm very responsible. I don't want to break immutability, so I
    // carefully handle input collection
    public Accountant(String id, List<String> qualifications) {
        super(id);
        this.qualifications = List.copyOf(qualifications);
    }

}

/**
 * Another class that we would like to inherit
 * from {@link Employee} and not break immutability
 */
public final class ITWorker extends Employee {

    private final List<String> skills;

    // I'm very responsible too, I don't want to break
    // immutability too, so I carefully handle input collection too
    public ITWorker(String id, List<String> skills) {
        super(id);
        this.skills = List.copyOf(skills);
    }

}

/**
 * Class that was written to break immutability
 * <p>
 * COMPILE ERROR: 'MutabilityBreaker' is not allowed in the sealed hierarchy
 */
public class MutabilityBreaker extends Employee {

    private final List<String> mutableCollection;

    // I'm not responsible, I don't want to follow immutability
    public MutabilityBreaker(String id, List<String> mutableCollection) {
        super(id);
        this.mutableCollection = mutableCollection;
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接