java为什么不允许父类强转子类?
26 个回答
刚看题目时一头雾水,为啥说不可以,明明可以呀。这不是个编译期错误,只要你的
SocialUserNoIdROFactory.newSocialUserNoIdRO
返回的实例刚好是个
SocialUserRO
,这样向下转型技术上毫无问题呀。
后来看了一下题主在其他回答下的评论,似乎意思是指返回的实例就是一个父类实例。题主希望强制转型后这个实例就原地变身变成了一个子类实例,原来父类实例已经赋值的属性继续保留原值,子类中多出来的属性就设为null(或者某种默认值)。问如果允许这样的语言特性会有什么问题。
先不谈“面向对象设计理论”这些虚的东西,我们可以看看在技术上或者开发实践上会出现什么问题。
这里其实有两种可能:一是这种强制转型后,这个父类实例本身直接变成了子类实例;二是转型后,原来的父类实例不变,语言系统自动创建一个新的子类实例并且根据父类实例的已有属性进行初始化。
我们分别来看这有什么问题。
第一种情况(原地变身):
那就是说,如果我拿着一个父类实例,把这个实例作为传入参数调用你写的一个函数,因为你的函数里对它进行了向下转型,结果调用完毕后我拿着的这个实例里面无厘头额外多储存了一堆子类状态(即使你没赋值,null本身也要占用一个额外的内存单位)。然后我再拿这个实例去调用你另一个函数,你把它向下转型成另一个子类,结果调用完毕后,更离谱,这个实例里在我毫不知情的情况下同时带了两个根本不兼容的不同子类的状态。这其实就是运行期动态隐性mixin了,那现在事实上我现在拿着一个同时是父类,子类A,子类B的实例,我现在调用这个实例上的一个方法,到底应该跑哪个类的实现?题主可能觉得,你手上通过一个类型是父类的变量指向这个实例,当然是调用父类上的实现了。函数里向下转型后,我通过一个类型是子类A的变量指向这个实例,那就是调用子类A上的实现,完美!然而这种方案实际上说的是【方法分派】(系统决定调用哪个实现的逻辑)不再是由运行期的实例类型决定(动态分派),而是由编译期的变量类型决定(静态分派)。这事实上完全破坏了面向对象的多态特性。
当然也有很多语言是支持mixin的,对这种一个实例同时带着多个类型的实现逻辑还是有解决方案的。但即使在程序中显式写死的静态mixin已经搞得颇为复杂了(所以Java一直没引入,你感兴趣可以去读一读隔壁Scala的程序代码)。为了少写几句属性复制代码,要支持“运行期动态隐式mixin“实在是得不偿失。
第二种情况(隐含复制):
这事实上就是引入了一种隐含的创建子类实例的方式。那问题就来了,这种创建方式究竟跑不跑子类构造器?如果子类没有默认构造器,跑哪个构造器?是先跑子类构造器还是先复制父类属性?先跑子类构造器,有些子类的内部实现假设并且保证了根本不可能出现某种值而父类没有这个限制把这种值覆盖进去了怎么办?先跑复制父类属性,子类构造器又改动了某些父类的值破坏了数据完整性怎么办?这里太多变数,仅仅为了省一两行代码引入这么多的不确定性同样得不偿失。
此外,Java这种依赖运行期对象实例状态的语言(相对于无副作用的函数式语言),“对象的生命周期“是一个非常重要的考察要素,而Java不用显式销毁对象,那么创建动作其实就是考察对象生命周期的一个关键标志(也就是你什么时候创建一个新的对象实例把旧的对象实例的引用冲掉)。我们在生产环境排错时一个非常常见的错误就是程序前面拿着一个对象实例传来传去不停往里面set数据,结果传着传着,到了出错的位置,发现根本就不是原本用来收集数据的那个实例。究竟实例是什么时候被换掉了,得查半天。现在为了省两行属性复制的代码,又双叒叕引入一种新的还是隐含的创建对象方式。我觉得最终结果就是所有项目组在编译期代码风格检查里写死不准使用向下转型。