6.10.3 finalize方法

Java默认垃圾回收机制

在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在没有明确指定清理资源的情况下,Java提供了默认机制来清理该对象的资源,这个机制就是finalize()方法。该方法是定义在Object类里的实例方法,方法原型为:
protected void finalize() throws Throwable
finalize方法返回后,对象消失,垃圾回收机制开始执行。方法原型中的throws Throwable表示它可以抛出任何类型的异常。
任何Java类都可以重写Object类的finalize()方法,在该方法中清理该对象占用的资源。

finalize方法不一定会被调用

如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的finalize方法来清理资源。
只有当程序认为需要更多的额外内存时,垃圾回收机制才会进行垃圾回收。因此,完全有可能出现这样一种情形:

  • 某个失去引用的对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的finalize()方法也不会得到调用

finalize方法特点

finalize()方法具有如下4个特点:

  1. 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用。
  2. finalize()方法何时被调用,是否被调用具有不确定性,不要把finalize()方法当成一定会被执行的方法.
  3. JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态。
  4. JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。

由于finalize方法并不一定会被执行,因此如果想清理某个类里打开的资源,则不要放在finalize方法中进行清理

如何强制调用可恢复对象的finalize()方法

finalize()方法要等到系统垃圾回收的时候才会执行,但是可能系统都不会进行垃圾回收。如果有必要,可以通过如下两个方法来强制执行所有可恢复对象的finalize()方法:

  • Runtime.getRuntime().runFinalization();
  • System.runFinalization();

实例 可恢复状态状态对象重新变成可达状态

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
public class FinalizeTest {
private static FinalizeTest ft = null;

public void info() {
System.out.println("测试资源清理的finalize方法");
}

public static void main(String[] args) throws Exception {
// 创建FinalizeTest对象立即进入可恢复状态
new FinalizeTest();
// 通知系统进行资源回收
System.gc(); // ①
// 强制垃圾回收机制调用可恢复对象的finalize()方法
// Runtime.getRuntime().runFinalization(); // ②
System.runFinalization(); // ③
// 执行对象的方法,如果注释掉上面的`System.gc();`,则不会强制垃圾回收,finalize方法不会执行,ft为null,则下面语句将会抛出空指针异常.
// 如果注释掉上面的`System.runFinalization();`,则finalize方法要等到系统进行垃圾回收时才会执行,系统可以不会进行垃圾回收,下面语句还是会抛出空指针异常.
// `System.runFinalization();`将会强制执行所有可恢复对象的finalize方法.
// 下面语句将会抛出空指针异常
ft.info();
}

public void finalize() {
// 重新引用 可恢复状态的对象,该对象将重新变成可达状态
ft = this;
}
}

6.10.2 强制垃圾回收

当一个对象失去引用后,系统何时调用它的finalize方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,绝不能控制它何时被回收。
程序无法精确控制Java垃圾回收的时机,但依然可以强制系统进行垃圾回收,这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。
强制系统垃圾回收有如下两种方式:

  • 调用System类的gc()静态方法:System.gc()
  • 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GcTest {
public static void main(String[] args) {
for (int i = 0; i < 40; i++) {
new GcTest();
// 下面两行代码的作用完全相同,强制系统进行垃圾回收
// System.gc();
Runtime.getRuntime().gc();
}
}

public void finalize() {
System.out.println("系统正在清理GcTest对象的资源...");
}
}

编译

1
javac -encoding UTF-8 GcTest.java

运行

1
java -verbose:gc GcTest

运行效果

1
2
3
4
5
6
7
8
9
10
[GC (System.gc())  2004K->792K(125952K), 0.0008571 secs]
[Full GC (System.gc()) 792K->644K(125952K), 0.0045574 secs]
[GC (System.gc()) 644K->708K(125952K), 0.0002769 secs]
[Full GC (System.gc()) 708K->644K(125952K), 0.0024457 secs]
[GC (System.gc()) 644K->708K(125952K), 0.0002579 secs]
[Full GC (System.gc()) 708K->644K(125952K), 0.0022112 secs]
[GC (System.gc()) 644K->740K(125952K), 0.0003019 secs]
[Full GC (System.gc()) 740K->644K(125952K), 0.0024142 secs]
系统正在清理GcTest对象的资源...

垃圾会后机制会尽快进行垃圾回收

这种强制垃圾回收只是建议系统立即进行垃圾回收,系统完全有可能并不立即进行垃圾回收,垃圾回收机制也不会对程序的建议完全置之不理:垃圾回收机制会在收到通知后,尽快进行垃圾回收。

6.10 对象与垃圾回收

Java的垃圾回收是Java语言的重要功能之一。当程序创建对象数组引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区中,当这块内存不再被任何引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。

垃圾回收机制特征

垃圾回收机制具有如下特征:

  1. 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接网络IO等资源)。
  2. 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存。
  3. 在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能使该对象重新复活,从而取消垃圾回收

6.10.1 对象在内存中的状态

当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种。

  1. 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
  2. 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。
    • 如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。
  3. 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。
    • 只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

这里有一张图片

一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或被其他对象的实例变量引用。
当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;
当某个对象被其他对象的实例变量引用时,只有当该对象被销毁后,该对象才会进入可恢复状态。

6.9.5 包含抽象方法的枚举类

枚举类不能使用abstract修饰类

枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(因为系统自动会为它添加abstract关键字),

枚举类可以定义抽象方法,每个枚举值都要实现这个抽象方法

但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误

实例

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
public enum Operation {
// 枚举值必须在第一行定义
PLUS {
// 枚举值必须实现枚举类中的抽象方法
public double eval(double x, double y) {
return x + y;
}
},
MINUS {
public double eval(double x, double y) {
return x - y;
}
},
TIMES {
public double eval(double x, double y) {
return x * y;
}
},
DIVIDE {
public double eval(double x, double y) {
return x / y;
}
};

// 为枚举类定义一个抽象方法
// 这个抽象方法由不同的枚举值提供不同的实现
public abstract double eval(double x, double y);

public static void main(String[] args) {
System.out.println(Operation.PLUS.eval(3, 4));
System.out.println(Operation.MINUS.eval(5, 4));
System.out.println(Operation.TIMES.eval(5, 4));
System.out.println(Operation.DIVIDE.eval(5, 4));
}
}

运行效果:

1
2
3
4
7.0
1.0
20.0
1.25

6.9.4 实现接口的枚举类

枚举类也可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口中的所有抽象方法。

1. 在枚举类中实现接口

枚举类实现接口与普通类实现接口完全一样:使用implements实现接口,并实现接口里包含的抽象方法即可。

如果由枚举类来实现接口里的方法,则每个枚举值在调用该方法时都有相同的行为方式(因为方法体完全一样)。

2. 在枚举值中实现接口

如果需要每个枚举值在调用接口方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该接口方法,,从而让不同的枚举值调用该方法时具有不同的行为方式。

枚举值后面的花括号部分实际上是匿名内部类的类体部分。在这种情况下,当创建枚举值时,并不是直接创建枚举类的实例,而是相当于创建枚举类的匿名子类的实例

实例

目录结构

1
2
3
4
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\06\6.9\interface
├─Gender.java
├─GenderDesc.java
└─Test.java

接口

1
2
3
4
public interface GenderDesc
{
void info();
}

实现接口的枚举类

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
public enum Gender implements GenderDesc {
// 调用构造器实例化成员变量
MALE("男")
// 创建当前类的匿名子类
{
// 匿名子类中重写父类Gender的方法
public void info() {
System.out.println("这个枚举值代表男性");
}
},
FEMALE("女")
// 创建当前类的匿名子类
{
// 匿名子类中重写父类Gender的方法
public void info() {
System.out.println("这个枚举值代表女性");
}
},
// 可以不创建匿名子类,则这个枚举默认使用枚举类的info方法
No_MALE_OR_Female("不男不女");

// 定义成员变量
private final String name;

// 枚举类的构造器只能使用private修饰
private Gender(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

// 在枚举中实现接口,这个必须要有
public void info() {
System.out.println("这是一个用于用于定义性别的枚举类");
}
}

测试类:通过枚举实例调用接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
// 获取三个枚举实例
Gender male = Gender.valueOf("MALE");
Gender female = Gender.valueOf("FEMALE");
Gender no_male_or_female = Gender.valueOf("No_MALE_OR_Female");
// 调用的是枚举值实现的info方法
male.info();
female.info();
// 调用的是枚举类实现的info方法
no_male_or_female.info();
}
}

运行效果:

1
2
3
这个枚举值代表男性
这个枚举值代表女性
这是一个用于用于定义性别的枚举类

6.9.3 枚举类的成员变量 方法和构造器

枚举类也是一种类,只是它是一种比较特殊的类,因此枚举一样可以定义成员变量、方法和构造器

枚举列成员变量使用private final修饰

  • 建议将枚举类的成员变量都使用private final修饰。

定义私有带参构造器来初始化成员变量

  • 如果将所有的成员变量都使用了final修饰符来修饰,所以要在构造器里为这些成员变量指定初始值,因此应该为枚举类显式定义带参数的构造器
  • 当然也可以在定义成员变量时指定默认值,或者在初始化块中指定初始值,但这两种情况并不常见。

一旦为枚举类显式定义了带参数的构造器,列出枚举值时就必须对应地传入参数

枚举类构造器不用显示调用

枚举类的构造器要定义为private,由枚举类的第一行代码定义的枚举类对象隐式调用。在枚举类第一行代码中列出的枚举值:MALE("男"), FEMALE("女");时,实际上就是隐式的调用构造器来创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构造器。

在前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是因为前面的枚举类包含系统提供的默认无参数的构造器。

枚举类的构造器默认就是私有的,所以,定义构造器的时候可以省略private关键字。

如何获取枚举类对象

通过枚举类的valueOf静态方法即可获得枚举类的实例,调用的格式为:类名.valueOf("枚举实例名")

如何调用枚举类的实例方法

  1. 通过枚举类的valueOf方法获取到枚举类对象后,就可以通过该对象的实例方法了,调用的格式为枚举类对象.实例方法()
  2. 通过枚举类还有一个更简单的调用实例方法的形式,调用的格式为:枚举类类名.实例.实例方法().

如果获取枚举类对象的成员变量

  1. 在枚举类中定义getter方法
  2. 通过调用getter方法来获取成员变量,推荐使用枚举类类名.实例.getter()方法这种调用形式。

实例

目录结构

1
2
3
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\06\6.9\best
├─Gender.java
└─Test.java

枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum Gender {
// 隐式调用带参构造器为name成员变量赋值
MALE("男"), FEMALE("女");

// 相当于如下代码
// public static final Gender MALE new Gender("男");
// public static final Gender FEMALE new Gender("女");
// 定义成员变量
private final String name;

// 枚举类的构造器由第一行定义的枚举实例显示调用,所以只能使用private修饰
private Gender(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

测试类:获取枚举实例中的成员变量值

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
Gender gender = Gender.valueOf("MALE");
System.out.println("枚举实例MALE的name成员变量的值为:" + gender.getName());
System.out.println("枚举实例MALE的name成员变量的值为:" + Gender.MALE.getName());
// 这样只能获取到枚举类对象
System.out.println("枚举实例MALE的name成员变量的值为:" + Gender.MALE);
}
}

运行结果如下:

1
2
3
枚举实例MALE的name成员变量的值为:男
枚举实例MALE的name成员变量的值为:男
枚举实例MALE的name成员变量的值为:MALE

可以看到这两种方式都可以正确调用getter方法,而且二中形式调用起来更方便.

6.9.2 枚举类入门

Java5新增了一个enum关键字(它与classinterface关键字的地位相同),用以定义枚举类。
枚举类是一种特殊的,它一样可以有自己的成员变量方法,可以实现一个或者多个接口,也可以定义自己的构造器
一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。

枚举类与普通类的区别

  • 使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object,因此枚举类不能显式继承其他父类。枚举类可以实现一个或多个接口,其中java.lang.Enum类实现了java.lang.Serializablejava.lang.Comparable两个接口。
  • 使用enum定义非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类
  • 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。

定义枚举类

定义枚举类时,需要:

  • 显式列出所有的枚举值,
  • 所有的枚举值之间以英文逗号(,)隔开,
  • 枚举值列举结束后以英文分号作为结束。
1
2
3
4
5
public enum SeasonEnum
{
//在第一行列出4个枚举实例
SPRING,SUMMER,FALL,WINTER;
}

如上面的SPRING,SUMMER,FALL,WINTER;所示,。这些枚举值代表了该枚举类的所有可能的实例。

使用枚举类的实例

如果需要使用该枚举类的某个实例,则可使用EnumClass.variable的形式,如SeasonEnum.SPRING

遍历枚举类

通过枚举类的values方法可以得到枚举类的所有实例数组,然后可以通过循环迭代输出了枚举类的所有实例。

1
2
3
4
public enum SeasonEnum {
// 在第一行列出4个枚举实例
SPRING, SUMMER, FALL, WINTER;
}
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
public class EnumTest {
public void judge(SeasonEnum s) {
// switch语句里的表达式可以是枚举值
switch (s) {
case SPRING:
System.out.println("春暖花开,正好踏青");
break;
case SUMMER:
System.out.println("夏日炎炎,适合游泳");
break;
case FALL:
System.out.println("秋高气爽,进补及时");
break;
case WINTER:
System.out.println("冬日雪飘,围炉赏雪");
break;
}
}

public static void main(String[] args) {
// 返回枚举类的所有实例数组
SeasonEnum[] seasonEnums = SeasonEnum.values();
// 遍历所有的枚举类实例
for (int i = 0; i < seasonEnums.length; i++) {
System.out.println(seasonEnums[i]);
}
System.out.println("------------------------------");
// 枚举类默认有一个values方法,返回该枚举类的所有实例
for (SeasonEnum s : SeasonEnum.values()) {
System.out.println(s);
}
// 使用枚举实例时,可通过EnumClass.variable形式来访问
new EnumTest().judge(SeasonEnum.SPRING);
}
}

JDK1.5之后switch的控制表达式可以用是枚举

  • JDK1.5switch的控制表达式可以是任何枚举类型。
  • switch控制表达式使用枚举类型时,后面case表达式中的值可以直接使用枚举值的名字,无须添加枚举类作为限定。

java.lang.Enum类方法

前面已经介绍过,所有的枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中所包含的方法。 java.lang Enum类中提供了如下几个方法。

方法 描述
int compareTo(E o) 该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象定义在指定枚举对象之后,则返回正整数;如果该枚举对象定义在指定枚举对象之前,则返回负整数,否则返回零。
String name() 返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数程序员应该优先考虑使用toString方法,因为toString方法返回更加用户友好的名称。
String toString 返回枚举常量的名称,与name方法相似,但toString()方法更常用。
int ordinal() 返回枚举值在枚举类中的索引值(也就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
public static<T extends Enum<T>> T valueOf( Class<T> enumType, String name) 这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。

6.9 枚举类

什么是枚举类

枚举类就是实例有限而且固定的类。
比如季节类,它只有4个对象;再比如星期类,只有7个对象。

6.9.1 手动实现枚举类

  • 直接使用简单的静态常量来表示枚举
  • 通过定义类的方式来实现

JavaJDK1.5后就增加了对枚举类的支持

6.8.5 使用Lambda表达式调用Arrays的类方法

Arrays类的有些方法需要ComparatorXxxOperatorXxxFunction等接口的实例,这些接口都是函数式接口,因此可以使用Lambda表达式来调用Arrays的方法。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Arrays;

public class LambdaArrays {
public static void main(String[] args) {
String[] strArray = new String[] { "Java", "Hello", "World", "HTML", "JavaScript" };

Arrays.parallelSort(strArray, (o1, o2) -> o1.length() - o2.length());
System.out.println("排序:"+Arrays.toString(strArray));

int[] intArray = new int[] { 3, -4, 25, 16, 30, 18 };
// left代表数组中前一个所索引处的元素,计算第一个元素时,left为1
// right代表数组中当前索引处的元素
Arrays.parallelPrefix(intArray, (left, right) -> left * right);
System.out.println("累积:"+ Arrays.toString(intArray));

long[] longArray = new long[5];
// operand代表正在计算的元素索引
Arrays.parallelSetAll(longArray, operand -> operand * 5);
System.out.println("索引*5:"+Arrays.toString(longArray));
}
}

运行结果:

1
2
3
排序:[Java, HTML, Hello, World, JavaScript]
累积:[3, -12, -300, -4800, -144000, -2592000]
索引*5:[0, 5, 10, 15, 20]

6.8.4 Lambda表达式与匿名内部类的联系和区别

Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用, Lambda表达式与匿名内部类存在如下相同点。

  • Lambda表达式与匿名内部类一样,都可以直接访问"effectively final"的局部变量,以及外部类的成员变量(包括实例变量和类变量)
  • Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
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
@FunctionalInterface
interface Displayable {
// 定义一个抽象方法
void display();

// 定义一个默认方法
default int add(int a, int b) {
return a + b;
}

}

public class LambdaAndinner {
// 实例变量
private int age = 12;
// 类变量
private static String name = "Hello";

public void test() {
// 方法局部变量
String book = "World";
// final String book = "World";
// 创建函数式接口实例,通过Lambda表达式实现display方法
Displayable displayable = () -> {
// Lambda表达式中不可以修改局部变量的值
// book = "被局部内部类访问的局部变量 只能赋值一次";
System.out.println("当前方法 局部变量 book=" + book);
System.out.println("外部类 类变量 name=" + name);
System.out.println("外部类 实例变量 age=" + age);
// Lambda表达式代码体中不允许调用接口默认方法
// System.out.println(add(1,2));
};
// 调用实现的display方法
displayable.display();
// 调用从接口中继承得到的add方法
displayable.add(3, 5);
}

public static void main(String[] args) {
LambdaAndinner lambdaAndinner = new LambdaAndinner();
lambdaAndinner.test();
}
}
  • Lambda表达式的代码块与匿名内部类的方法体是相同的。
  • 与匿名内部类相似的是,由于Lambda表达式访问了book局部变量,因此该局部变量相当于有隐式的final修饰,因此同样不允许对book局部变量重新赋值。
  • 程序使用Lambda表达式创建的对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法。

Lambda表达式和匿名内部类的区别

Lambda表达式与匿名内部类主要存在如下区别。

  1. 匿名内部类可以为任意接口创建实例,不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但**Lambda表达式只能为函数式接口创建实例**
  2. 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
  3. 匿名内部类实现的抽象方法的方法体中允许调用接口中定义的默认方法;但Lambda表达式的代码块中不允许调用接口中定义的默认方法。