实现不可变类时如何禁止子类化?

实现不可变类时要求禁止子类化。本文先讲禁止子类化的方式,最后解释为什么要禁止子类化。

如何禁止子类化

常用姿势

最简单的手段是将类声明为final,如String、Integer等常用的值类。但这样缺乏灵活性:不仅禁止了用户的子类化,开发者也无法利用子类化减少编码工作。

尽管这种手段完全没有变通,却是我们使用最多的一种。只有你需要上述灵活性的时候,再去考虑下述方式。

不常用但你需要掌握的姿势

还有一种不常用,但更灵活的方法:静态工厂方法+私有构造器。

完全禁止子类化(效果类似于final修饰)

如果希望Parent3完全不可子类化,除了用final修饰Parent3以外,还可以用private修饰其所有构造方法,这样Child3因无法调用父类的构造方法,也无法通过编译:

1
2
3
4
5
6
7
8
9
10
11
public class Parent3 {
private Parent3() {
}
}
// 无法调用父类的构造方法,因此无法通过编译,即Parent3无法子类化
public class Child3 extends Parent3 {
private Child3() {
super();
}
}

通过静态工厂方法构造Parent3的实例。

更灵活的子类化限制

但是,如果放松Parent3构造方法的访问权限, 我们还能得到更灵活的子类化限制。比如允许包级私有的子类化:

1
2
3
4
5
6
7
8
9
10
11
public class Parent5 {
Parent5() {
}
}
// 只要Child5与Parent5定义在同一个包内,就可以子类化
public class Child5 extends Parent5 {
Child5() {
super();
}
}

需要注意的是,Java的覆写机制要求覆写方法(Child5())的权限不低于被覆写方法(Parent5())。这造成了一种危险:如果将Child5()声明为public,那么Child5变得可子类化,间接实现了Parent5的子类化

PS:以上实现不能定义为内部类,如果有疑问,你需要回忆private的语义。

为什么要禁止子类化

如果允许子类化,在发生多态的情况下,通过覆写子类的访问器,可以让子类冒充父类,让父类“看起来”是可变的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ImmutableParent {
private final int imVal;
public ImmutableParent(int imVal) {
this.imVal = imVal;
}
public int getImVal() {
return imVal;
}
}
public class MutableChild extends ImmutableParent {
private int mVal;
public MutableChild(int imVal) {
super(imVal);
mVal = imVal;
}
// 覆写父类的不可变字段 imVal 的访问器, 发生多态时子类实例就能伪装成父类实例, 让用户访问可变字段 mVal
@Override
public int getImVal() {
return mVal;
}
// 而伪装者撕下面具时(改用子类引用), 就能随意修改可变字段 mVal
public void setImVal(int mVal) {
this.mVal = mVal;
}
}

总结

上述方式从语法层面保证了不可变类的禁止子类化。尽管我们能通过其他办法在多态访问时判断当前对象是父类还是子类的实例,但哪种方式更恰当呢?显然是前者,两个理由:

  1. 用合适的方式做合适的事
  2. 语法优于约定
扫描微信关注我
微信公众号二维码
本文链接:实现不可变类时如何禁止子类化?
作者:猴子007
出处:https://monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。
我是猴子007,<br>一只非常特殊的动物,<br>可以从事程序的开发、维护,<br>经常因寻找香蕉或母猿而无心工作。