Earth Guardian

You are not LATE!You are not EARLY!

0%

Java 面向对象的特征

Java 面向对象的三大特征:封装、继承、多态;而封装和继承基本都是为多态而准备的。

封装

封装 Encapsulation [ɛnˈkæpsəˌletʃən] :在面向对象语言中,封装特性是由类来体现的,我们将现实生活中的一类实体定义成类,其中包括属性和方法。将对象的实现细节隐藏起来,通过一些公用方法来暴露该对象的功能。

继承

继承 Inheritance[ɪnˈhɛrɪtəns] :当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法。Java 的接口有多继承,而类没有多重继承,但是可以通过实现不同的接口或者多层次继承来体现多重继承。继承通过关键字 extends 来实现,实现继承的类称为子类,被继承的类称为父类,或者超类、基类。

superthis

  • 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
2
3
4
5
6
// 实际类型变化,运行时才能确定  
Human man = new Man();
man = new Woman();
// 静态类型变化,使用时就确定了
sr.sayHello((Man) man);
sr.sayHello((Woman) man);

根据内存模型,man 是存储在虚拟栈中,属于局部变量表中的引用变量;而 new Man() 表示在堆中分配一块存储区,引用变量的值为指向这块堆的指针。

重载与重写

方法的重载和重写是 Java 的多态性的不同表现。重写是父类和子类之间多态性的体现,重载是单个类中多态性的体现。

  • 重载 overloading
    单个类中定义了多个同名的方法,它们有不同的参数类型或参数个数或参数次序,则该方法被重载。不能通过返回类型,访问权限,抛出异常等进行重载。
    编译器在编译阶段通过参数的静态类型来作为重载的判断依据。
1
2
3
4
5
public void sayHello(Human guy){...}
public void syaHello(Man guy){...}
// 根据静态类型的特点,调用的是 sayHello(Human)
Human man = new Man();
sayHello(man);
  • 重写 Override
    子类中定义的方法和父类中有相同的名称和参数,则该方法被重写。
    只有在运行时根据实际类型,虚拟机才能确定调用哪个重写的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Man extends Human{
@Override
void sayHello(){...}
}
class Woman extends Human{
@Override
void sayHello(){...}
}
Human man = new Man();
Human woman = new Woman();
// 根据实际类型的特点,调用子类的方法
man.sayHello();
woman.sayHello();

静态方法是类的方法,不存在重写这个说法(无法被 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
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
34
35
36
37
public class TestDispatcher {
static class Apple {}
static class Orange {}

public static class Father {
public void eat(Apple apple) {
System.out.println("Father eat apple.");
}

public void eat(Orange orange) {
System.out.println("Father eat orange.");
}
}

public static class Son extends Father {
@Override
public void eat(Apple apple) {
System.out.println("Son eat apple.");
}

@Override
public void eat(Orange orange) {
System.out.println("Son eat orange.");
}
}

public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.eat(new Apple());
son.eat(new Orange());
}
}

// Result
Father eat apple.
Son eat orange.

这个经典示例中将重载和重写都体现出来了。

  • 编译时
    编译时多态:即静态分派。编译时会确定下来,去执行 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
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// 1. Parent
public class Human {
private String mHumanPrivateStr = "HumanPrivateStr";
String mHumanDefaultStr = "HumanDefaultStr";
protected String mHumanProtectedStr = "HumanProtectStr";
public String mHumanPublicStr = "HumanPublicStr";
public String mSameNameStr = "HumanSameNameStr";

private static String staticHumanPrivateStr = "StaticHumanPrivateStr";
public static String staticHumanPublicStr = "StaticHumanPublicStr";

{
System.out.println("Human normal code block.");
}

static {
System.out.println("Human static code block.");
}

public Human(){
System.out.println("Human Constructor.");
}

private void privateMethod(){
System.out.println("Human: privateMethod.");
}

public void publicMethod(){
System.out.println("Human: publicMethod.");
}

public void publicNormalMethod(){
System.out.println("Human: publicNormalMethod.");
}

private static void staticPrivateMethod(){
System.out.println("Human: staticPrivateMethod.");
}

public static void staticPublicMethod(){
System.out.println("Human: staticPublicMethod.");
}

public static void staticPublicNormalMethod(){
System.out.println("Human: staticPublicNormalMethod");
}
}

// 2. Sub
public class Man extends Human{
private String mManPrivateStr = "ManPrivateStr";
String mManDefaultStr = "ManDefaultStr";
protected String mManProtectedStr = "ManProtectStr";
public String mManPublicStr = "ManPublicStr";
public String mSameNameStr = "ManSameNameStr";

private static String staticManPrivateStr = "StaticManPrivateStr";
public static String staticManPublicStr = "StaticManPublicStr";

{
System.out.println("Man normal code block.");
}

static {
System.out.println("Man static code block.");
}

public Man() {
System.out.println("Man Constructor.");
}

@Override
public void publicMethod() {
System.out.println("Man: publicMethod");
}

private static void staticPrivateMethod(){
System.out.println("Man: staticPrivateMethod.");
}

public static void staticPublicMethod(){
System.out.println("Man: staticPublicMethod.");
}

public void testSuperAndThis(){
System.out.println("Man::testSuperAndThis, super.mSameNameStr = "
+ super.mSameNameStr);
super.publicMethod();
System.out.println("Man::testSuperAndThis, this.mSameNameStr = "
+ this.mSameNameStr);
this.publicMethod();
}
}

// 3. Test
public class TestOO {
public static void main(String[] args) {
Human human = new Man();
System.out.println("mSameNameStr = " + human.mSameNameStr);
System.out.println("staticHumanPublicStr = "
+ human.staticHumanPublicStr);
// 不建议使用对象访问静态变量,当前只是用来演示
// System.out.println("staticHumanPublicStr = "
+ Human.staticHumanPublicStr);
human.publicMethod();
human.staticPublicMethod();
// 不建议使用对象访问静态方法,当前只是用来演示不会重写
// Human.staticPublicMethod();

System.out.println("******The subclass can access the member
and method of the parent.######");
Man man = new Man();
System.out.println("mHumanPublicStr = " + man.mHumanPublicStr);
man.publicNormalMethod();
System.out.println("staticHumanPublicStr = "
+ man.staticHumanPublicStr);
// 不建议使用对象访问静态变量,当前只是用来演示可以访问父类静态变量
// System.out.println("staticHumanPublicStr = "
// + Human.staticHumanPublicStr);
man.staticPublicNormalMethod();
// 不建议使用对象访问静态方法,当前只是用来演示可以访问父类静态方法
// Human.staticPublicNormalMethod();

System.out.println("*******Test super and this ###########");
man.testSuperAndThis();
}
}

// 4. Result
Human static code block.
Man static code block.
Human normal code block.
Human Constructor.
Man normal code block.
Man Constructor.
mSameNameStr = HumanSameNameStr
staticHumanPublicStr = StaticHumanPublicStr
Man: publicMethod
Human: staticPublicMethod.
******The subclass can access the member and method of the parent.######
Human normal code block.
Human Constructor.
Man normal code block.
Man Constructor.
mHumanPublicStr = HumanPublicStr
Human: publicNormalMethod.
staticHumanPublicStr = StaticHumanPublicStr
Human: staticPublicNormalMethod
*******Test super and this ###########
Man::testSuperAndThis, super.mSameNameStr = HumanSameNameStr
Human: publicMethod.
Man::testSuperAndThis, this.mSameNameStr = ManSameNameStr
Man: publicMethod

示例演示了父类子类的初始化顺序,super, this 的使用,向上转型成员变量的访问,静态变量及静态方法的访问,普通方法的重写。

抽象类

Java 中包含抽象类,抽象方法。抽象类表明这个类只能被继承,抽象方法表明这个方法必须由子类实现。

规则

  • 抽象类和抽象方法必须使用 abstract 修饰
  • 有抽象方法的类必须被定义为抽象类;但是反过来抽象类可以没有抽象方法
  • 抽象类(即使不包含抽象方法)不能被实例化,即无法通过 new 来构造抽象类的实例
  • 抽象类可以包含属性、方法(普通或者抽象方法)、构造器、初始化块、内部类、枚举类 6 种成分。抽象类的构造器不能用于创建实例,主要用于供子类调用

注意事项

  • 抽象方法和空方法是两个不同的概念,抽象方法没有方法体(即花括弧 {});而空方法是指方法体内为空
  • abstract 不能修饰属性和构造器,即 Java 中没有抽象属性的说法,也没有抽象构造器
  • static 修饰的方法是属于类的,所以如果该方法同时被定义为 abstract 的会导致编译错误。即 abstract 不能和 static 同时修饰方法
  • private 访问控制符修饰的方法,子类无法访问,所以该方法如果被定义为 abstract 会导致子类无法实现。即 abstract 不能和 private 同时修饰方法

作用和意义

抽象方法是定义一种或者一类事物必须有的一种技能,但是这种技能对于各个继承者的表现形式不一样,就把它定义为抽象方法。抽象类将事物的共性的东西提取出来,抽象成一个高层的类。如果一个类中没有包含足够的信息来描绘一个具体的对象,我们将这样的类定义为抽象类。抽象类往往用来表示对问题领域的抽象概念,看上去行为不同,但是本质上相同的具体概念的抽象。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。比如三角形、圆形、长方形等都属于形状。

接口

接口 Interface 是一组抽象方法的集合。接口用于从多个相似类中抽象出规范。

规则

  • 接口可以多重继承接口,但是不能继承类
  • 接口因为定义的是规范,所以不能包含构造器和初始化块
  • 接口可以包含属性、方法、内部类(包含内部接口、枚举类),它们默认且也只能是 public 访问权限
  • 接口的属性变量都是常量(默认会自动添加 public static final),方法都是抽象方法(默认会自动添加 public abstractJava 8 接口增强中已经支持非抽象的默认方法,使用 defualt 修饰)

注意事项

  • 接口中都是抽象方法,所以不能使用 static 修饰(Java 8 接口增强中已经支持静态方法)
  • 接口不能显示继承任何类
  • 接口的实现类使用 implements 关键字
  • 实现接口方法时,必须使用 public 修饰

作用和意义

接口体现的是一种规范和实现分离的设计理念,可以很好的降低程序各模块之间的耦合,通常可以用于面向接口编程。

Java 8 接口增强

Java 8 接口增强:支持默认方法和静态方法,其中默认方法需要增加 default 关键字修饰。

1
2
3
4
5
6
7
8
9
interface MyInterface {
default void defaultMethod() {
System.out.println("default method invoked! ");
}

static void staticMethod() {
System.out.println("static method invoked! ");
}
}

抽象类和接口的异同

相同点:抽象类和接口都不能被实例化,只能由子类继承或者其他类实现。
不同点:

  • 抽象类可以包含静态方法和普通方法,接口不可以(但是 Java 8 接口增强后是可以的)
  • 接口不包含构造器和初始化代码块
  • 接口支持多重继承,抽象类只能单继承

常见问题

继承时出现同名成员变量和方法

  • 子类父类成员变量同名
    父类的成员变量会被屏蔽,子类访问该同名成员变量显示的是子类的,可以通过 super 来访问父类的同名成员变量。
  • 子类父类成员方法同名
    子类会重写父类的同名方法,通过 super 访问父类同名成员方法。

如果出现同名成员变量和方法,父类的成员变量和方法会被子类屏蔽(重写),在子类中可以通过 super 来访问。

静态属性和静态方法是否可以被继承?是否可以被重写?

  • 子类继承父类后,参考子类初始化顺序可知,继承了父类所有属性和方法。但是子类访问父类属性和方法时,会受访问控制符限制
  • 重写是指方法重写,多态针对的是对象而不是类。静态方法属于类,并不属于对象,所以不存在重写这一说。对象在调用静态方法时,会根据静态类型直接绑定

在实际编码中,强烈建议直接使用类来调用静态属性和静态方法,养成好习惯。

参考文档