OOP丨《Java编程思想》阅读笔记Chapter 5 : 初始化与清理
《Java编程思想》Chapter 5 : 初始化与清理
Initialization and Cleanup(初始化和清理)
编程方式中的设计安全的两个重要问题
本章就将讨论这两个重要的问题
- 1. 基础概念
- 2. 用构造器确保初始化
- 2.1. 构造器的命名来源
- 2.2. 构造器的定义
- 3. 方法重载
- 3.1. 为什么我们需要方法重载?
- 3.2. 方法重载的基础实例
- 3.3. 区分重载方法
- 4. this关键字
- 4.1. this的一般用法
- 4.2. static含义
- 5. 清理:终结处理和垃圾回收
- 5.1. 为什么?
- 5.2.
finalize()
的用途何在? - 5.3. 终结条件
- 5.4. 垃圾回收器如何工作
- 6. 成员初始化
- 6.1. 指定初始化
- 7. 构造器初始化
- 7.1. 初始化顺序
- 7.2. 静态数据的初始化
- 8. 数组初始化
- 8.1. 数组引用的定义
- 8.2. 数组的初始化方法
- 9. 可变参数列表
- 9.1. 介绍
- 9.2. 注意点
- 10. 枚举类型
- 10.1. 枚举的使用
- 10.2. 枚举的特性
- 10.3. 枚举的理解以及常用使用场景
1. 基础概念
-
Constructor(构造器)
C++引入的概念,在对象被创建时自动调用的特殊方法
Java同样采用了构造器的概念 -
垃圾回收器
Java提供的内存资源回收机制,自动回收不再使用的内存
2. 用构造器确保初始化
假如没有构造器,我们会如何初始化对象呢?
一个比较常规的想法就是为每个Class定义一个initialize()
方法
然后在每个对象创建时调用这个方法
而在之后创建Object时,我们必须记得调用此方法
但通过提供构造器,Class的设计者可以确保每个Object都得到正确的初始化
2.1. 构造器的命名来源
问题:既然我们需要一个方法作为构造器,如何取名?
考虑点:
- 防止与任何其他方法冲突
- 构造器由编译器负责,需要让编译器能够自动知道调用哪个方法
C++采用了与Class同名的方法作为构造器
Java也继承了这种方式
2.2. 构造器的定义
当我们定义了一个Class,如果我们没有定义构造器,编译器会自动为我们生成一个默认构造器
但如果我们定义了一个构造器,编译器就不会再生成默认构造器
因此如果我们定义了一个带参数的构造器
那么我们之后创建Object时,就必须传入参数
3. 方法重载
3.1. 为什么我们需要方法重载?
一般来说当我们定义了一个方法
它所能接受的参数和处理的对象、逻辑都是固定的
这就要求我们如果要处理不同的对象,就必须定义不同的方法
按照一般的思路(例如C语言),我们就需要为这些对象分别分配不同的标识符
但是对于我们实际生活中,例如“清洗”这个操作
我们一般只会使用“清晰”+“对象”这两个信息来确定我们要进行的操作
而不会使用“清洗类1的方法清洗”+“对象1”这种方式
因此我们产生了一个想法就是:
我们可以使用某一个通用的标识符,定义多个不同的方法,来处理不同的对象等
这就是方法重载概念的主要来源
3.2. 方法重载的基础实例
Constructor就是一个重要例子
C++和Java中,构造器是强制重载方法名的另一个原因
因为Constructor的名字已经与Class名绑定
如果没有重载,我们就只能拥有一种构造方法了
这显然是不满足我们的需求的
3.3. 区分重载方法
由于我们重载的需求,此前用于区分方法的标识符失去了唯一性
那我们需要一些别的部分来区分这些方法
根据参数区分重载方法:
- 参数顺序
实际上只有参数的顺序不同的方法,也可以被认为是不同的方法
但是不建议这样做 - 参数类型
重载方法的参数类型不同,此时方法可以被区分
注意:传入的参数类型满足以下转换规则
存在对应时,直接取对应;存在更大类型时,向更大类型转化;仅存在类型转换时,必须传入时就进行显式转换 - 参数数量
或许我们会想通过返回值来区分方法
但是我们需要回想,即使方法定义返回值,我们也不一定会使用这个返回值
这时候我们如何区分只有返回值不同的方法呢?
因此Java不允许通过返回值来区分方法
4. this关键字
class
有多个对象
当调用class
中的方法时,我们如何知道是哪个对象调用?
Java的解决方法是让编译器自动传入操作对象的引用
但是因为是隐式传入,我们没有标识符来表示这个引用
因此Java提供了 this
关键字 来表示这个引用
但我们同样发现,一般来说
我们在调用同一class
的其他方法或实例变量时,我们一般不会使用this
关键字
这是因为编译器会帮我们自动添加
4.1. this的一般用法
- 返回调用对象的引用
当我们需要返回调用对象的引用时,我们必须使用this
关键字
一个相关使用场景:一条语句中对同一个对象进行多次操作 - 为了将自身传递给外部方法
也许某个工具方法在对象所属class
之外
为了将调用对象传递至外部方法,需要使用this
关键字 - 在构造器中调用构造器
一个class
中定义了多个构造器
我们可以在一个构造器的开头调用另一个构造器
注意:1.只能在开头;2.只能调用一个 - 区分同名的方法传入参数与调用对象的实例变量
当我们需要传入参数与调用对象的实例变量同名时
我们可以使用this
关键字来区分
4.2. static含义
当我们了解了this
关键字
我们就可以更好理解static
关键字的含义了
借助我们上面的知识,我们可以如下说:
static(静态)
方法就是没有this
的方法
static
方法的特性:
- 可以在没有创建
object
的情况下,直接通过class
调用
这就有点类似于全局函数的概念了 - 不能调用非
static
方法和实例变量
因为非static
方法和实例变量需要this
关键字来调用
但可以通过传入对象引用来调用或者在static
方法中创建一个object
来使用,但为什么不改成非static
方法呢?
5. 清理:终结处理和垃圾回收
Java中的垃圾回收机制是Java的一个重要特性,也是Java的一个优势
它负责回收无用对象占据的内存
但是它一直有用吗?
也存在特殊情况,即对象(并非使用new
)获得了特殊的内存资源,它无法被垃圾回收机制回收
因为垃圾回收机制只能回收由new
创建的对象
为了应对这种情况,Java允许在class
中定义一个finalize()
方法
而我们可以同样分析一下C++采用的内存管理方式:
C++中存在析构函数的概念,是在销毁对象必须使用函数
在一般情况下,C++程序(没有缺陷的理想情况下)中的对象一定回归销毁
但Java中,对象并非总是被垃圾回收
可以总结为以下两点:
- 对象可能不被垃圾回收
- 垃圾回收并不等于“析构”
5.1. 为什么?
很有可能,只要程序没有面临存储空间使用殆尽的情况,对象所占用的空间就总是得不到释放
若这种情况下程序执行完毕,垃圾会抽起一直没有起作用,则会随着程序的退出,直接将资源全部交还操作系统
这个策略的目的是节省垃圾回收本身造成的开销
只要我不用,那就不会产生开销
5.2. finalize()
的用途何在?
显然,通过上述分析,我们知道finalize()
方法不应该作为通用的清理方法
这与第三点有关:
3. 垃圾回收只与内存有关
即,垃圾回收器被使用的唯一原因就是为了回收程序不再使用的内存
只要是通过Java通常做法来分配内存创建对象,那么它就可以被垃圾回收器释放
因此特殊情况就来自于非通常做法的内存分配
而这种情况主要发生在本地方法中
此处不多赘述
但不管是“垃圾回收”还是“终结”,都不一定会发生
如果JVM未发生内存耗尽的情形,就不会浪费时间取执行垃圾回收
5.3. 终结条件
来自于finalize()
方法的一个有趣用法 :
用于最终发现对象中是否存在没有被适当清理的部分
即,当对象被清理时,其应该处于某种状态,使得其内存可以被安全释放(例如打开文件的对象应该关闭文件)
人如果没有妥善处理,程序就会存在很隐晦的缺陷
在finalize()
方法中设置检验和提醒可以用于检测这种情况
尽管它并不总是被执行
但是一旦实行,就可能据此找出问题所在!
5.4. 垃圾回收器如何工作
- TODO: 此处暂时还难以理解
主要介绍了Java垃圾回收器以及其堆内存上的一些浅显的工作原理以及优化思想
6. 成员初始化
Java尽力保证:所有变量在使用前都能得到恰当的初始化
Java程序中
创建局部变量时,若未进行初始化就进行使用,会以编译时错误报告
编译器自然可以为其赋上一个默认值,但实际上往往是程序员疏忽导致的未初始化
要求强制提供一个初始值,更利于找出程序缺陷
示例:
public static void main(String[] args)
{int i ;i ++ ;
}
错误: 可能尚未初始化变量ii ++ ;^
1 个错误
但与局部变量不同
class
的实例变量,即使没有人为的初始化,也会保证具有一个初始值
如下:
Data Type | Initial Value |
---|---|
boolean | false |
char | ‘\u0000’ |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
reference | null |
6.1. 指定初始化
如果想为某个变量赋初值
一个直接的办法就是在定义类实例变量时就进行赋值
对于非基本类型的实例变量,也同样适用
注意:C++中这个操作是不合法的
class InitialValues
{boolean t = true ;char c = 'x' ;byte b = 47 ;short s = 0xff ;int i = 999 ;long l = 1 ;float f = 3.14f ;double d = 3.14159 ;ExampleClass e = new ExampleClass() ;
}
此处,我们甚至可以通过调用某方法来提供初值
不过需要注意初始化的顺序
以上所述的方法,简单而直观
但是限制也很明显,每个对象所具有的初值都是固定的
当我们需要更大的灵活性时,可能需要另作考虑
7. 构造器初始化
构造器初始化即适用构造器进行object
中实例变量的初始化
但值得注意的时,构造器初始化无法组织自动初始化的进行
自动初始化将在构造器被调用之前发生
7.1. 初始化顺序
class
内,实例变量的定义先后,决定了初始化的顺序
此外,即使实例变量的定义散布于方法之间(即不一定位于class
开头),也会在任何方法被调用之前得到初始化
7.2. 静态数据的初始化
先前所属
static
关键字,不止可用于修饰方法
同样可以用于修饰实例变量
当然,不能应用于局部变量
静态初始化只有在必要的时刻才会进行
即当第一个对象被创建
或第一次静态数据被访问
时才会初始化静态实例变量
注,上述后者包括静态实例变量和静态方法的使用
顺序:
在一个尚未初始化的class
中
初始化的顺序是先静态数据,后“非静态”数据
显式的静态初始化:
public class Spoon
{static int i ;static {i = 1 ;}
}
以上第二个static
的内容就是静态初始化块
它会保证在class
第一次加载时被执行,且只会执行这一次
注:以上显式的静态初始化的方法也可以用于非静态实例的初始化
把static
去掉即可
其可以保证不管调用哪个显式构造器,其中的操作都会发生
8. 数组初始化
8.1. 数组引用的定义
与普通对象引用的定义类似
需要注意的是,数组引用的定义,[]
的位置有两种风格
int[] a1 ;
int a2[] ;
第一种更为直观,比较推荐
第二种是适应C/C++的风格
8.2. 数组的初始化方法
-
定义数组处进行指定初始化
int[] a = { 1, 2, 3, 4, 5} ;
编译器不允许指定数组的大小,但在这种方法中
存储空间的分配(等于使用new
)将有编译器负责 -
使用
new
在数组里创建元素int[] a ; a = new int[5] ; // or int[] b = new int[5] ; // when possible, more recommended
值得一提的是,首先数组的大小可以是变量
其次如果我们创建的是非基本类型的数组,
9. 可变参数列表
示例代码
VarArgs.java
9.1. 介绍
可变参数列表,一种方便的语法用来创建对象并调用
可以应用于参数个数或类型为止的场合
语法:
type... name
优点:
因为可变参数的引入,不再需要显式地编写数组语法
指定参数时,编译器实际上回我为我们填充数组
9.2. 注意点
允许参数范围:
- 可以为空
具有可选的尾随参数时,很有效 - 可以有一个或多个
- 可以直接传入个体参数,也可以传入数组
可变参数对重载的影响:
根据大致理解,带来的问题主要是可能的歧义导致的编译错误
应该总是只在重载方法的第一个版本上使用可变参数列表
10. 枚举类型
enum
关键字,于Java SE5中被引入
定义示例:
public enum Spiciness
{NOT, MILD, MEDIUM, HOT, FLAMING
}
10.1. 枚举的使用
enum
的用法:
创建类型引用,赋值给某个实例
10.2. 枚举的特性
enum`被创建时,编译器会自动添加一些有用的特性:
toString()
方法
方便用于显示某个enum实例的名字ordinal()
方法
返回一个int
值,表示enum
实例在声明时的次序
从0开始static values()
方法
返回一个包含所有enum
实例的数组
顺序与enum
实例的次序相同
使用示例:
EnumTest.java
10.3. 枚举的理解以及常用使用场景
枚举的理解:
相较于C中的类似于宏定义的枚举,Java的enum
更为强大
在java中,enum
可以理解为一种特殊的class
因此他当初还可以拥有方法