Java
面向对象的三大特征:封装、继承、多态;而封装和继承基本都是为多态而准备的。
封装
封装 Encapsulation [ɛnˈkæpsəˌletʃən]
:在面向对象语言中,封装特性是由类来体现的,我们将现实生活中的一类实体定义成类,其中包括属性和方法。将对象的实现细节隐藏起来,通过一些公用方法来暴露该对象的功能。
继承
继承 Inheritance[ɪnˈhɛrɪtəns]
:当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法。Java
的接口有多继承,而类没有多重继承,但是可以通过实现不同的接口或者多层次继承来体现多重继承。继承通过关键字 extends
来实现,实现继承的类称为子类,被继承的类称为父类,或者超类、基类。
super
和 this
super
代表父类对象,可以理解为是指向自己父类对象的一个引用,而这个父类指的是离自己最近的一个父类。this
代表对象本身,可以理解为指向对象本身的一个引用。
父类子类的初始化顺序
父类和子类的初始化顺序如下:
- 父类类加载过程:静态成员变量,静态代码块
- 子类类加载过程:静态成员变量,静态代码块
- 父类类实例化过程:普通成员变量,构造代码块,最后父类的构造方法
- 子类类实例化过程:普通成员变量,构造代码块,最后子类的构造方法
static
的成员变量和方法是属于类的,所以只会在类加载时初始化或执行一次,类实例化时不会重复初始化。
代码执行顺序具体参考这篇博文:Java 代码执行顺序
属性和方法的继承
子类继承父类后,可以继承父类的属性和方法。包含静态属性和方法,但是子类访问父类的属性和方法时受访问控制符限制。
多态
多态 Polymorphism[ˌpɒlɪ'mɔ:fɪzəm]
:子类对象直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,表现出多种行为的特征。
多态针对的是对象,而不是类。
向上转型和向下转型
- 规则
父类引用指向子类对象,但是子类引用不能指向父类对象。 - 向上转型
父类引用指向子类对象,也就是子类对象直接赋给父类引用,不用强制转换。子类对象会遗失父类中不同的方法,重写相同的方法。 - 向下转型
子类对象的父类引用赋给子类引用,要强制转换。父类对象强制转换赋给子类引用,是不安全的向下转型,编译正常但运行过程中报错。
多态存在的三个必要条件
- 继承
- 重写
- 父类引用指向子类对象
方法调用:分派
参考《深入理解 Java
虚拟机:JVM
高级特性与最佳实践 第 2 版》 第 8.3.2 章:分派,这一章中介绍了分派的概念,以及静态分派和动态分派的含义,给出的结论是 Java
是一门静态多分派和动态单分派的语言。
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(具体调用哪个方法)。Class
文件的编译过程中不包含链接步骤,所有的方法调用在 Class
文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。方法调用在类加载期间,甚至运行期间才能确定目标方法的入口地址(直接引用)。
变量静态类型和实际类型
先看一段代码:Human man = new Man()
。
- 静态类型
Static Type
其中Human
称为变量的静态类型,或者叫外观类型Apparent Type
。静态类型仅仅在使用时有可能会出现改变,但在编译期能够明确最终的静态类型。也就是说,静态类型决定了变量拥有哪些成员属性和方法。 - 实际类型
Actual Type
其中Man
称为变量的实际类型。实际类型只能再运行期才能确定,编译器无法判定对象的实际类型是什么。也就是说,实际类型决定了变量只有在运行时才能确定具体执行哪个方法。
1 | // 实际类型变化,运行时才能确定 |
根据内存模型,man
是存储在虚拟栈中,属于局部变量表中的引用变量;而 new Man()
表示在堆中分配一块存储区,引用变量的值为指向这块堆的指针。
重载与重写
方法的重载和重写是 Java
的多态性的不同表现。重写是父类和子类之间多态性的体现,重载是单个类中多态性的体现。
- 重载
overloading
单个类中定义了多个同名的方法,它们有不同的参数类型或参数个数或参数次序,则该方法被重载。不能通过返回类型,访问权限,抛出异常等进行重载。
编译器在编译阶段通过参数的静态类型来作为重载的判断依据。
1 | public void sayHello(Human guy){...} |
- 重写
Override
子类中定义的方法和父类中有相同的名称和参数,则该方法被重写。
只有在运行时根据实际类型,虚拟机才能确定调用哪个重写的方法。
1 | class Man extends Human{ |
静态方法是类的方法,不存在重写这个说法(无法被
Override
注释),静态方法在编译期间根据静态类型进行了绑定。强烈建议直接使用类来调用静态变量和静态方法,而不是使用对象来调用,养成好习惯。(虽然Java
语法通过对象来访问,但是这种方式会使得静态属性并不突出)
静态分派
依赖静态类型来定位方法具体执行哪个版本(重载时),这个分派动作称为静态分派,典型应用为重载。在静态分派过程中,如果没有指定显示的静态类型,会发生类型的自动转换来匹配最可能的类型, 如:sayHello('a')
,如果没有明确重载 sayHello(char c)
方法,会按照 :char -> int -> long -> float -> double ->Character ->Serializable --> Object
的顺序转型。其中:
char
转为int
表示a
除了代表字符串,还可以代表数字 97 。char
转为它的封装类型Character
是一次自动装箱过程。char
转为Serializable
是因为Character
实现了序列化和可比较。但是如果同时重载了sayHello(Serializable s)
和sayHello(Comparable c)
会提示类型模糊,编译报错。
动态分派
在运行期根据实际类型确定方法执行哪个版本(重写),这个分派过程称为动态分派,典型应用为重写。
单分派和多分派
方法的接收者和方法的参数统称为方法的宗量。根据分派基于宗量多少,可以将分派分为单分派和多分派。经典示例:
1 | public class TestDispatcher { |
这个经典示例中将重载和重写都体现出来了。
编译时
编译时多态:即静态分派。编译时会确定下来,去执行Father.eat(Apple)
和Father.eat(Orange)
,也就是说即确定了方法的接收者Father
又确定了参数Apple/Orange
。方法的接收者和参数两个宗量都参与了判断,所以静态分派是多分派类型。这种只基于两种宗量来判断的即为双分派。运行时
运行时多态:即动态分派。运行时,因为参数已经确定,所以只需要确定接受者是Father
还是它的子类Son
?只需要方法接收者这一个宗量参与判断,所以动态分派是单分派类型。
至此,也验证了结论:Java
是一门静态多分派和动态单分派的语言。
向上转型注意事项
特点
向上转型:Human human = new Man()
,根据父类子类初始化顺序可以得出:子类实例化时会先将父类实例化,也就是说子类能直接访问父类成员变量和方法(受访问控制符限制)。
- 成员变量
成员变量human
的静态类型为Human
,所以human
当前只能访问Human
的成员变量和方法。 - 成员方法分派
成员变量human
的实际类型为Man
,所以human
在执行成员方法时会考虑到动态分派:是否被子类重写?如果重写了则执行子类Man
重写后的方法。 - 静态方法
无法重写,在编译期根据静态类型进行绑定,强烈建议静态方法使用类来调用而不是对象。
示例
1 | // 1. Parent |
示例演示了父类子类的初始化顺序,super, this
的使用,向上转型成员变量的访问,静态变量及静态方法的访问,普通方法的重写。
抽象类
Java
中包含抽象类,抽象方法。抽象类表明这个类只能被继承,抽象方法表明这个方法必须由子类实现。
规则
- 抽象类和抽象方法必须使用
abstract
修饰 - 有抽象方法的类必须被定义为抽象类;但是反过来抽象类可以没有抽象方法
- 抽象类(即使不包含抽象方法)不能被实例化,即无法通过
new
来构造抽象类的实例 - 抽象类可以包含属性、方法(普通或者抽象方法)、构造器、初始化块、内部类、枚举类 6 种成分。抽象类的构造器不能用于创建实例,主要用于供子类调用
注意事项
- 抽象方法和空方法是两个不同的概念,抽象方法没有方法体(即花括弧
{}
);而空方法是指方法体内为空 abstract
不能修饰属性和构造器,即Java
中没有抽象属性的说法,也没有抽象构造器static
修饰的方法是属于类的,所以如果该方法同时被定义为abstract
的会导致编译错误。即abstract
不能和static
同时修饰方法private
访问控制符修饰的方法,子类无法访问,所以该方法如果被定义为abstract
会导致子类无法实现。即abstract
不能和private
同时修饰方法
作用和意义
抽象方法是定义一种或者一类事物必须有的一种技能,但是这种技能对于各个继承者的表现形式不一样,就把它定义为抽象方法。抽象类将事物的共性的东西提取出来,抽象成一个高层的类。如果一个类中没有包含足够的信息来描绘一个具体的对象,我们将这样的类定义为抽象类。抽象类往往用来表示对问题领域的抽象概念,看上去行为不同,但是本质上相同的具体概念的抽象。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。比如三角形、圆形、长方形等都属于形状。
接口
接口 Interface
是一组抽象方法的集合。接口用于从多个相似类中抽象出规范。
规则
- 接口可以多重继承接口,但是不能继承类
- 接口因为定义的是规范,所以不能包含构造器和初始化块
- 接口可以包含属性、方法、内部类(包含内部接口、枚举类),它们默认且也只能是
public
访问权限 - 接口的属性变量都是常量(默认会自动添加
public static final
),方法都是抽象方法(默认会自动添加public abstract
;Java 8
接口增强中已经支持非抽象的默认方法,使用defualt
修饰)
注意事项
- 接口中都是抽象方法,所以不能使用
static
修饰(Java 8
接口增强中已经支持静态方法) - 接口不能显示继承任何类
- 接口的实现类使用
implements
关键字 - 实现接口方法时,必须使用
public
修饰
作用和意义
接口体现的是一种规范和实现分离的设计理念,可以很好的降低程序各模块之间的耦合,通常可以用于面向接口编程。
Java 8
接口增强
Java 8
接口增强:支持默认方法和静态方法,其中默认方法需要增加 default
关键字修饰。
1 | interface MyInterface { |
抽象类和接口的异同
相同点:抽象类和接口都不能被实例化,只能由子类继承或者其他类实现。
不同点:
- 抽象类可以包含静态方法和普通方法,接口不可以(但是
Java 8
接口增强后是可以的) - 接口不包含构造器和初始化代码块
- 接口支持多重继承,抽象类只能单继承
常见问题
继承时出现同名成员变量和方法
- 子类父类成员变量同名
父类的成员变量会被屏蔽,子类访问该同名成员变量显示的是子类的,可以通过super
来访问父类的同名成员变量。 - 子类父类成员方法同名
子类会重写父类的同名方法,通过super
访问父类同名成员方法。
如果出现同名成员变量和方法,父类的成员变量和方法会被子类屏蔽(重写),在子类中可以通过 super
来访问。
静态属性和静态方法是否可以被继承?是否可以被重写?
- 子类继承父类后,参考子类初始化顺序可知,继承了父类所有属性和方法。但是子类访问父类属性和方法时,会受访问控制符限制
- 重写是指方法重写,多态针对的是对象而不是类。静态方法属于类,并不属于对象,所以不存在重写这一说。对象在调用静态方法时,会根据静态类型直接绑定
在实际编码中,强烈建议直接使用类来调用静态属性和静态方法,养成好习惯。
参考文档
- 深入理解
Java
虚拟机:JVM
高级特性与最佳实践 第 2 版 - 分派
- Java中为什么静态方法不能被重写?
- 图解Java继承内存分配
- JAVA的多态直观的解释
- Java的三大特性——多态
- Java中为何要定义抽象类