0%

Java编程思想#复用类

“类”有“广义”和“狭义”之分。“广义的类”包括:狭义的类(Class),抽象类(Abstract Class),接口(Interface),枚举(Enum),注解(Annotation)。为了进行区分,在接下来的叙述中,“类”指代“广义的类”,“具体类”指代“狭义的类”。

一、类的本质分类

根据以上叙述可知,“类”包括:具体类(Class),抽象类(Abstract Class),接口(Interface),枚举(Enum),注解(Annotation)。但是本质上“类”只包括“具体类和接口”两种,原因描述如下:

  • “抽象类”本质上是“含有抽象方法的具体类”
  • “枚举”本质上是“具体类”,可以使用javap命令进行查看
  • “注解”本质上是“接口”,可以使用javap命令进行查看

而内部类只是处于类中的“具体类,抽象类,接口,枚举,注解”的特定称呼而已。


在接下来的描述中,“类”就只包含“具体类”和“接口”。

二、复用类的3种方式

2.1、组合

复用现有类的功能。

2.2、继承

复用现有类的形式。

2.3、代理

组合和继承之间的中庸之道,我们将一个实例对象(只有具体类才具有实例对象)作为字段置于所要构造的新类(具体类)中(就像组合),但与此同时在新类中暴露该实例对象的某个方法子集(就像继承)。

三、继承

关于“继承”有以下几点描述:

  • 当创建一个类时,总是在继承,除非已明确指出要从其他类继承,否则就是在隐式地从Java的标准根类Object继承 (接口跟Object类的关系在“接口”章节进行详细介绍)
  • 继承时,会自动继承基类中可被继承的字段和方法
  • 继承时,可以继承基类方法,覆盖基类方法,重载基类方法,可以通过@Override注解断言“覆盖基类方法”,从而防止意外重载基类的方法
  • 生成导出类实例对象(导出类为具体类)时,会有基类子实例对象(基类为具体类)生成。当创建了一个导出类的实例对象时,该实例对象包含了一个基类的子实例对象,这个子实例对象与你用基类直接创建的实例对象是一样的。二者区别在于,后者来自外部,而基类的子实例对象被包装在导出类实例对象内部
  • 在生成导出类实例对象时,对基类子实例对象的正确初始化也是至关重要的,这个只能通过调用基类构造器来完成。JVM会在导出类的构造器中插入对基类构造器的调用,只有基类构造器才能完成基类子实例对象的正确初始化。如果基类构造器为带参构造器,那么在导出类构造器中必须通过super语句显式调用基类的带参构造器

四、完备介绍“初始化与清理”

在包含“具体类,抽象类,接口,枚举,注解”(本质上只有“具体类”和“接口”),且存在继承关系的背景下,对Java编程思想#初始化与清理作补充,完备介绍“初始化与清理”相关内容。
一个综合复杂的继承关系示例图如图1所示。

图1

4.1、完备介绍“初始化”

4.1.1、完备介绍“类对象初始化”

触发时机:第一次访问类的“静态字段,静态方法,构造方法”,Class.forName(相应类类路径)语句第一次执行。类对象初始化最多执行一次。
触发后初始化过程:

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
public void initializeForClassObject(当前类) {
当前类在之前已经完成初始化则直接返回

if (当前类是具体类) {
if (存在当前类的“父类具体类”){
//递归进行“父类具体类”的类对象初始化
initializeForClassObject(父类具体类)

1、默认初始化(有可能在此之前已经完成,那么不再重复)
2、定义初始化
3、静态初始化语句初始化
} else{
//即当前类是java.lang.Object

1、默认初始化(有可能在此之前已经完成,那么不再重复)
2、定义初始化
3、静态初始化语句初始化
}
} else {
//即当前类是接口

1、默认初始化(有可能在此之前已经完成,那么不再重复)
2、定义初始化
}
}

备注:
类对象的默认初始化有可能已经先于其他初始化(定义初始化,静态初始化语句初始化)完成,具体机制跟虚拟机的具体实现相关,且默认初始化不是整个初始化流程的关键所在,因此这里不再深究。

4.1.2、完备介绍“实例对象初始化”

触发时机:创建实例对象(再次提醒:只能通过构造器创建实例对象,接口没有实例对象,只能对应于具体类)。
触发后初始化过程:

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
public void initializeForInstanceObject(当前具体类) {
首先分配当前具体类和所有祖先具体类的内存,并进行默认初始化

//接下来执行构造器方法
构造方法(当前具体类)
}

public void 构造方法(当前具体类) {
if (构造器方法中首语句是this语句) {
调用this语句指代的构造器方法——构造方法(当前具体类)
执行“构造器方法中其他语句(不能再包含this语句)”
} else {
if (本类是Object类) {
执行“定义初始化”(构造方法中隐式包含相应的触发执行语句)
执行“实例初始化语句初始化”(构造方法中隐式包含相应的触发执行语句)
执行“构造器方法中其他语句”
} else {
隐式或显式调用super语句,调用父类具体类的构造方法——构造方法(父类具体类)

执行“定义初始化”(构造方法中隐式包含相应的触发执行语句)
执行“实例初始化语句初始化”(构造方法中隐式包含相应的触发执行语句)
执行“构造器方法中其他语句”
}
}
}

4.2、完备介绍“清理”

Java编程思想#初始化与清理可知,当我们需要清理一些自定义的资源时,不能依赖finalize方法,得自己编写特殊的清理方法。在继承场景下,另外还需要注意这些清理方法的调用顺序:跟资源初始化顺序相反,防止出现“在清理资源时,该资源所依赖资源已经被提前清理,而导致错误”的情形。

五、其他

5.1、super关键词

跟this关键词类似,关于this关键词可参见Java编程思想#初始化与清理。super关键词有两个用途:“指代基类实例对象”和“在构造器中调用基类构造器”。
跟this关键词不一样的是,可执行return this;语句,不可执行return super;语句,可认为这个区别基于“基类的子实例对象被包装在导出类实例对象内部”的特性。
使用super调用基类构造器有以下限制:

  • “super调用基类构造器”只能在构造器中使用
  • “super调用基类构造器”只能置于构造器最起始处
  • “super调用基类构造器”在同一个构造器中至多使用1次

5.2、java命令能够作用于具有包访问权限的类生成的class文件

现在有一个“Main.java”文件,其中内容如下:

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}

class SecondClass {
public static void main(String[] args) {
System.out.println("Hello World");
}
}

执行javac Main.java命令,得到“Main.class”和“SecondClass.class”两个文件。执行java Main命令,得到打印的“Hello World”内容;执行java SecondClass命令,也能够得到打印的“Hello World”内容。
以上说明了java命令不只能够作用于具有public访问权限的类生成的class文件,也能够作用于具有包访问权限的类生成的class文件。


参考文献: [1]https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html [2]https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html
您的支持将鼓励我继续分享!