java为什么不允许父类强转子类?

如果允许会有什么问题吗? 我子类继承父类,我想把父类对象的属性给到子类上,如果可以强转就很方便。像下面这样,只需要给子类多余的字段设置值就好了 pub…
关注者
29
被浏览
33,439

26 个回答

刚看题目时一头雾水,为啥说不可以,明明可以呀。这不是个编译期错误,只要你的 SocialUserNoIdROFactory.newSocialUserNoIdRO 返回的实例刚好是个 SocialUserRO ,这样向下转型技术上毫无问题呀。

后来看了一下题主在其他回答下的评论,似乎意思是指返回的实例就是一个父类实例。题主希望强制转型后这个实例就原地变身变成了一个子类实例,原来父类实例已经赋值的属性继续保留原值,子类中多出来的属性就设为null(或者某种默认值)。问如果允许这样的语言特性会有什么问题。

先不谈“面向对象设计理论”这些虚的东西,我们可以看看在技术上或者开发实践上会出现什么问题。

这里其实有两种可能:一是这种强制转型后,这个父类实例本身直接变成了子类实例;二是转型后,原来的父类实例不变,语言系统自动创建一个新的子类实例并且根据父类实例的已有属性进行初始化。

我们分别来看这有什么问题。

第一种情况(原地变身):

那就是说,如果我拿着一个父类实例,把这个实例作为传入参数调用你写的一个函数,因为你的函数里对它进行了向下转型,结果调用完毕后我拿着的这个实例里面无厘头额外多储存了一堆子类状态(即使你没赋值,null本身也要占用一个额外的内存单位)。然后我再拿这个实例去调用你另一个函数,你把它向下转型成另一个子类,结果调用完毕后,更离谱,这个实例里在我毫不知情的情况下同时带了两个根本不兼容的不同子类的状态。这其实就是运行期动态隐性mixin了,那现在事实上我现在拿着一个同时是父类,子类A,子类B的实例,我现在调用这个实例上的一个方法,到底应该跑哪个类的实现?题主可能觉得,你手上通过一个类型是父类的变量指向这个实例,当然是调用父类上的实现了。函数里向下转型后,我通过一个类型是子类A的变量指向这个实例,那就是调用子类A上的实现,完美!然而这种方案实际上说的是【方法分派】(系统决定调用哪个实现的逻辑)不再是由运行期的实例类型决定(动态分派),而是由编译期的变量类型决定(静态分派)。这事实上完全破坏了面向对象的多态特性。

当然也有很多语言是支持mixin的,对这种一个实例同时带着多个类型的实现逻辑还是有解决方案的。但即使在程序中显式写死的静态mixin已经搞得颇为复杂了(所以Java一直没引入,你感兴趣可以去读一读隔壁Scala的程序代码)。为了少写几句属性复制代码,要支持“运行期动态隐式mixin“实在是得不偿失。

第二种情况(隐含复制):

这事实上就是引入了一种隐含的创建子类实例的方式。那问题就来了,这种创建方式究竟跑不跑子类构造器?如果子类没有默认构造器,跑哪个构造器?是先跑子类构造器还是先复制父类属性?先跑子类构造器,有些子类的内部实现假设并且保证了根本不可能出现某种值而父类没有这个限制把这种值覆盖进去了怎么办?先跑复制父类属性,子类构造器又改动了某些父类的值破坏了数据完整性怎么办?这里太多变数,仅仅为了省一两行代码引入这么多的不确定性同样得不偿失。

此外,Java这种依赖运行期对象实例状态的语言(相对于无副作用的函数式语言),“对象的生命周期“是一个非常重要的考察要素,而Java不用显式销毁对象,那么创建动作其实就是考察对象生命周期的一个关键标志(也就是你什么时候创建一个新的对象实例把旧的对象实例的引用冲掉)。我们在生产环境排错时一个非常常见的错误就是程序前面拿着一个对象实例传来传去不停往里面set数据,结果传着传着,到了出错的位置,发现根本就不是原本用来收集数据的那个实例。究竟实例是什么时候被换掉了,得查半天。现在为了省两行属性复制的代码,又双叒叕引入一种新的还是隐含的创建对象方式。我觉得最终结果就是所有项目组在编译期代码风格检查里写死不准使用向下转型。

我认为这不是一个技术问题,而是一个设计理念的问题,或者说是设计规则,或者说说逻辑问题。技术人员不能总是想着解决技术层面上的问题,很多时候,更需要考虑的是整体设计上的逻辑的合理性。

我简单阐述一下父子类的区别。对于java这样的面向对象语言来说,继承关系,本质上来讲是一种代码复用的手段,这是没错的,但是继承是一种非常有限的代码复用方式,它是有限制的。从逻辑上来讲,子类和父类之间存在一个很强的而且很特殊的关系。就是这种父子关系,有些书上喜欢解释成“是一个”这样的说法,但我觉得这种说法过于简化,而且似乎比较适应西方人的思维方式,中国人还是不太好理解。而且父子类的设计又涉及面向对象技术的抽象能力,因此,这个地方是十分容易产生概念混乱的。混乱的概念就会导致混乱的用法,自然就会出现有时需要把父类强转成子类的这种完全不符合逻辑,但是可能符合应用场景的问题……。

首先你要理解,类的设计,除了无所谓的数据类,涉及业务的类的设计在面向对象世界里面是很顶层的设计。如果你把面向对象的设计理解成为法律体系的话,那么类的设计,包括类之间的继承关系,这就如同宪法,是对整个社会的一种约束。本质上来说就是定义边界用的,那么在做这种顶层设计的时候,使用继承就需要慎重,因为这样的东西是不能轻易改动的。你随便乱动类之间的层次关系就很容易导致“世界”的动荡……,现实世界中一个国家天天乱改宪法,你觉得这个国家可能是稳定的么?

因此,在做继承关系的设计的时候,要非常的慎重,一定要运用抽象类和接口来虚化很多不确定的东西,这种做法往往也被称为封装变化。

后来出现的微服务技术就能很好的化解这样的问题,微服务的出现可以极大的简化顶层设计。我服务化已知的部分就行了,未知的部分后面搞明白了在做。不同模块之间的通讯就自然而然的使用接口解决了。

在回到问题上来。从面向对象设计的逻辑上面来说,子类虽然可以属于父类的类型,但是子类往往会比父类多(很多)的要素(属性和方法)。面向对象系统其实约束的是方法,而不是属性。所以本质上来说,从面向对象设计的角度来看,继承应该做的是改变属性,和改变方法的内容而不是增加方法。所以从这个意义上去理解的话,继承关系并不是让你用来做简单的扩展和代码复用的。这种场景你应该使用的是组合技术,而不是继承。所以从面向对象所描述的逻辑内涵上来讲,子类应该是一个全新的类型,它拥有和父类一样的方法签名,但是却有不同的实现,而当父与子都顶着父亲的类型工作的时候,面向对象系统却能够正确的识别哪个是父亲,哪个是孩子,这种技术叫做“多态”。个人感觉这个就是强行发明出来的概念而已,因为这种识别对于面向对象技术本身来说是自然而然的。

基于我上面所述的原因,java是不会允许把父类转成子类的,我相信,也没有任何一个面向对象系统会允许你干这种荒唐的事情。毕竟使用编程语言的人不是上帝,写编译器的人才是……。