深入理解多态,为什么说静态成员无多态特性?

来源:互联网 发布:看不见的城市 知乎 编辑:程序博客网 时间:2024/09/21 06:17

这是前不久做的一道关于静态成员与静态方法特性的题,真的不做下题都不知道自己的基础究竟是有多浅薄,当然这里指对多态的理解

这里写图片描述

一、先回顾下静态成员与静态变量吧,static关键字

参考书籍《Java编程思想》
参考博客Java中static作用及用法详解
我们知道,作为面向对象设计的明星程序语言,Java中的一切皆对象,而且每个对象都有其类型(class),决定了该类型对象的外观与行为(成员变量与成员函数)。通常,当创建一个类的时候,只是描述对象的外观与行为,中看不中用;只有在对象被new出来之后,才会在堆中存储该Java对象,具有实际使用价值。换句话说,只有对象才有实际作用。
然而有时候,我们想不创建对象,就可以直接访问类中某个成员或者调用某个成员方法,于是就有了static关键字。当声明一个成员是static时,就意味着该成员可以通过类名直接访问,不与任何对象实例关联,是属于类的,为所有对象实例共享。

1、关于static的修饰作用——成员变量、成员方法、代码块

(1)对成员变量的影响
对于类中的non-static成员变量,每实例化一个对象,就会分配相应的存储空间,可以有很多份,并且互不影响
而对于类中的static成员变量,由于是类的数据,在创建类的时候就已经分配了存储空间,只有一份,所有对象共享

(2)对成员方法的影响
对于类中的non-static成员方法,其隐式含有this关键字——在方法内部获取当前对象的引用;而static方法,没有this关键字。
因此non-static成员方法因具有this可以访问当前对象中的所有变量(包括static变量)以及static方法
而static方法就只能访问static变量而不能访问或调用non-static成员

(3)对代码块的影响
static代码块在类加载时就会自动执行,且只会执行一次

2、关于初始化

静态初始化只在类首次加载的时候进行,而且是必要时刻(即首次生成该类的一个对象时或者首次访问静态数据时),此后不会再次被初始化

3、关于继承

static方法内不能使用super关键字,类似于this,即不可以访问父类的non-static变量或调用父类的non-static方法。
但是不妨碍子类访问父类的static变量或static方法。
除此之外父类的static方法不允许被子类方法重写

4、常见static方法

main方法,作为程序入口,需要独立于任何对象实例存在

5、关于构造方法是不是static方法的论证(仍有争议)

来源于知乎@RednaxelaFX的回答知乎传送门
其本人博客实例构造器是不是静态方法

简单说:
从Java语言层面看,构造器不是静态方法。事实上规范专门规定了构造器不是方法。
从JVM层面看,构造器属于特殊的初始化方法,但仍然不被归类为静态方法。
无论从哪个层面看,构造器都是可以静态分派的(statically dispatched)。但要特别注意的是,并非所有可以静态分派的代码都是静态方法,这是规范里有定义的术语。
无论从哪个层面看,构造器都要接收从调用方传入的隐藏this参数。Java的静态方法不需要接收该参数,而实例方法要接收该参数。这方面构造器更接近于可以静态分派的实例方法,例如私有实例方法。这就是为什么从构造器可以调用this上的实例方法。
书中的Dog例子,确实当Dog被首次实例化或其静态方法/静态变量被首次访问时,会触发Dog类的初始化。但构造器在此并不可以归类到静态方法的一侧。事实上构造器并不能被单独调用,而必须用在new表达式里,此时触发类初始化的正是new,而不是对构造器的调用——new与构造器调用是两码事。

二、对static有一定了解之后,再来了解下多态特性

一说到面向对象程序设计三大特性,马上就能反应过来是,封装、继承、多态,在这边文章中主要讲多态。

1、什么是多态特性

主要体现在方法调用上,同一个方法根据其对象类型的不同而采取不同的方法行为。
得益于继承,对象既可以作为自己本身的类型使用,也可以作为它的基类型使用(向上转型),因此对基类型对象起作用的方法就可以用于其所有的导出类对象中并因为导出类对象的不同而有不同的方法行为。

class Animals {    public void sound(){        System.out.println("动物叫声");    }}class Dog extends Animals{    public void sound(){        System.out.println("汪汪汪");    }}class Cat extends Animals{    public void sound(){        System.out.println("喵喵喵");    }}public class Test2{    public static void animalsSound(Animals a){        a.sound();    }    public static void main(String[] args) {        Animals dog = new Dog();        Animals cat = new Cat();        animalsSound(dog);        animalsSound(cat);    }}

运行结果:
这里写图片描述

如上述示例,对于animalsSound(Animals a)方法,虽然传入的是Animals类型的引用Dog与Cat实例,调用其sound()方法,但是却根据Dog与Cat类型的不同而有不同的sound()方法实现,这就是表现出来的多态特性

2、如何使用多态特性

(1)通过继承重写相应方法(如上述示例)
(2)通过实现接口

3、为什么要使用多态(多态的好处)

整理自网络
(1)可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
(2)可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
(3)接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。如图8.3 所示。图中超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。
(4)灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
(5)简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

4、如何判断是否呈现出多态(多态的存在前提)

参考知乎问题中 @程序狗@Intopass@NightSilence的回答
(1)要有继承关系
(2)子类重写父类的方法
(3)父类引用指向子类实例

有些地方,把方法重载也列入了多态的一种呈现,但个人也认为这种归类不够妥当。
因为,方法重载中的方法由于参数表的不同,本质上已经不是同一个方法了,而是多个方法。而多态的涵义,就是调用同一个方法但是却有不同的实现行为。
进一步说,根据后面会提到的多态特性的实现机制——动态绑定,在运行的过程中确定对象的类型并调用相应的方法。方法重载由于方法本身就不一样,在编译的时候就可以确定调用哪个方法了,并不需要动态绑定,也就不属于多态。知乎Java的函数重载为什么采用静态绑定
而且,oracle官方文档的介绍,也验证了上述三个条件是多态的存在前提。

ps:在网络上也有很多关于多态的表现,诸如
这里写图片描述
个人目前还是只认可子类型多态

三、为什么说静态成员无多态特性?

其实,在分别弄清楚stiatc与多态的本质后,已经不难回答这个问题了。首先,要明确,static成员,不管是static变量还是static方法,都是属于类的,不与任何对象实例相关联,而多态最起码得要有多个对象的存在,从面向对象的角度就可以解释这个问题。其次,static方法不允许被重写,也就不存在动态绑定之类的一系列问题。

四、动态绑定以及实现机制

参考博客Java动态绑定和多态
参考博客Java高级-动态绑定实现机制

上述内容或多或少的提及了动态绑定的概念,现在我们进一步了解。
由于子类重写父类的方法,方法都是一样的,只是对象所属类型不一样了,而对象是需要运行时才可以创建的,也就是说,编译器只有一个父类引用时是没有办法确定到底要方法调用哪一个方法主体。所以就只能通过在运行时根据对象的实际类型再进行方法调用与方法主体的绑定,也叫做动态绑定或运行时绑定。而在编译时就可以确定的叫做静态绑定或前期绑定(C语言就是这样的)

对于java当中的方法而言,除了final,static,private和构造方法是前期绑定外,其他的方法全部为动态绑定

现在我们可以知道,要实现动态绑定,就需要在对象中内置某种“类型信息”,以便在运行时确定对象的准确类型。
看下Java对象在虚拟机中的内存模型

就可以知道,JVM在运行时通过一种特殊结构指针来查找方法表中的类型数据指针指向的类型数据,来确定对象的准确类型,进而准确调用方法主体。
现在也清楚了,Java中的运行时类型识别RTTI的机制——在运行期间对类型进行检查的实现原理了。

五、后记

上述总结是多方面查找资料、博客所得,真心觉得Google比百度好用多了,如有错误,欢迎指正,大家一起讨论学习,感谢

0 0