java基础(四):面向对象(下)
6.1、 包装类
Byte、Short、Integer、Long、Character、Float、Double、Boolean
说明:为了让基本类型也具有对象的特征,就出现了包装类型。如使用集合类型Collection时,就一定要使用包装类型,因为容器都是装object的。
JDK 1.5提供了 自动装箱(Autoboxing)和 自动拆箱(AutoUnboxing)功能:
自动装箱(基本类型 --> 包装类):Integer i = 10;,底层调用:Integer i = Integer.valueOf(10);
自动拆箱(包装类 --> 基本类型):int n = i;,底层调用:int n = i.intValue();
包装类还可实现 基本类型变量 和 字符串 之间的转换。把字符串类型的值转换为基本类型的值有两种方式:
1、利用包装类提供的 parseXxx(String s) 静态方法(除Character之外的所有包装类都提供了该方法)。
2、 利用包装类提供的 valueOf(String s) 静态方法。
String 类也提供了多个重载 valueOf() 方法,用于将基本类型变量转换成字符串。
Java 7增强了包装类的功能,Java 7为所有的包装类都提供了一个静态的 compare(xxx val1,xxx val2)方法,来比较两个基本类型值的大小,
包括比较两个boolean类型值,两个boolean类型值进行比较时,true>false。
6.2、 处理对象
Object类提供的toString()方法总是返回该对象实现类的“类名+@+hashCode”值,这个返回值并不能真正实现“自我描述”的功能,一般重写Object类的toString()方法来实现“自我描述”。
== 和 equals 的区别是什么?
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用(是否相等);
equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
public static void main(String[] args) { String x = "str"; String y = "str"; String z = new String("str"); System.out.println(x==y); //true x和y指向同一个引用 System.out.println(x==z); //false new String()方法重新开辟了一块内存空间,x和z引用不同 System.out.println(x.equals(y)); //true equals本质上就是==,但String Integer重写了它,所以比较的是值 System.out.println(x.equals(z)); //true equals本质上就是==,但String Integer重写了它,所以比较的是值 }
6.3、 类成员
在Java类里只能包含:成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员。
/** * @ClassName:Singleton * * @Description: 单例类:始终只能创建一个实例的类 * 1、提供一个 static 属性缓存对象; * 2、用 private 隐藏构造器 * 3、提供一个 public static 方法创建对象,并保证只能创建一个对象; * * @author: * @date:2021年3月28日 上午10:56:43 * @version */ class Singleton { private static Singleton instance; //使用一个静态变量来缓存已创建的实例 //用private修饰构造器,隐藏它 private Singleton(){} //提供一个静态方法返回Singleton的实例,保证只创建一个对象 public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); //创建一个Singleton对象,并缓存 } return instance; } public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1==s2); //true } }
6.4、 final修饰符
final 、finally 和finalize的区别是什么?
final 可以修饰 类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally 一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize 是属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
对于 final修饰的成员变量 而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,
那么这些成员变量的值将一直是系统默认分配的0、'\u0000'、false或null,这些成员变量也就完全失去了存在的意义。因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值。
归纳起来,final 修饰的类变量、实例变量能指定初始值的地方如下:
1、类变量:必须在 静态初始化块中 指定初始值 或 声明该类变量时 指定初始值,而且只能在两个地方的其中之一指定。
2、实例变量:必须在 非静态初始化块、声明该实例变量 或 构造器中 指定初始值,而且只能在三个地方的其中之一指定。
系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
如果final 修饰的局部变量在定义时 没有 指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;
如果final 修饰的局部变量在定义时 已经 指定默认值,则后面代码中不能再对该变量赋值。
final 修饰的 形参 因为形参在调用该方法时,由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能被赋值。
当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,
final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
对一个final变量来说,不管它是 类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
1、 使用final修饰符修饰。
2、在定义该final变量时指定了 初始值。
3、该初始值可以在 编译时 就被确定下来。
注意:1、final修饰符的一个重要用途就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,
那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
2、final修饰的方法不可被重写。
3、final修饰的类不可以有子类,例如java.lang.Math类就是一个final类,它不可以有子类。
不可变(immutable)类 的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java提供的8个包装类和java.lang.String类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变。
如果需要创建自定义的不可变类,可遵守如下规则:
1、使用private和final修饰符来修饰该类的成员变量。
2、提供带参数的构造器(或返回该实例的类方法),用于根据传入参数来初始化类里的成员变量。
3、仅为该类成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
4、如果有必要,重写 Object类的 hashCode() 和 equals() 方法。equals() 方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。
6.5、 抽象类
抽象方法 和 抽象类 必须使用 abstract 修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
抽象方法和抽象类的规则如下:
1、抽象类必须使用 abstract 修饰符来修饰,抽象方法也必须使用 abstract 修饰符来修饰,抽象方法不能有方法体。
2、抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
3、抽象类可以包含 成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
4、含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
定义抽象方法:只需在普通方法上增加 abstract 修饰符,并把普通方法的 方法体(也就是方法后花括号括起来的部分)全部去掉,并在方法后增加分号即可。
定义抽象类:只需在普通类上增加 abstract 修饰符即可。甚至一个普通类(没有包含抽象方法的类)增加 abstract 修饰符后也将变成抽象类。
当使用 abstract 修饰类 时,表明这个 类只能被继承;
当使用 abstract 修饰方法 时,表明这个 方法必须由子类提供实现(即重写)。
而 final 修饰的类不能被继承,final 修饰的方法不能被重写。因此 final 和 abstract 永远不能同时使用。
注意:abstract 不能用于修饰 成员变量,不能用于修饰局部变量,即没有抽象变量、没有抽象成员变量等说法;
abstract 也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通构造器。
6.6、 Java 9改进的接口
抽象类 是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”—接口(interface)。
Java 8 对接口进行了改进,允许在接口中定义默认方法和类方法,默认方法和类方法都可以提供方法实现,Java 9为接口增加了一种私有方法,私有方法也可提供方法实现。
接口定义使用 interface 关键字,接口定义的基本语法如下:
1、修饰符可以是 public 或者省略,如果省略了public 访问控制符,则默认采用包权限访问控制符。
2、接口名应与类名采用相同的命名规则;
3、一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
由于接口定义的是一种规范,因此接口里 不能包含 构造器 和 初始化块定义。
接口里可以包含 成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。
一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号(,)隔开。
接口主要有如下用途:
➢ 定义变量,也可用于进行强制类型转换。
➢ 调用接口中定义的常量。
➢ 被其他类实现。
一个类可以实现一个或多个接口,继承使用 extends 关键字,实现则使用 implements 关键字。
因为一个类可以实现多个接口,这也是Java为单继承灵活性不足所做的补充。类实现接口的语法格式如下:
接口与抽象类异同点:
接口和抽象类很像,它们都具有如下特征(同):
➢ 接口和抽象类 都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
➢ 接口和抽象类 都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
接口 和 抽象类 在用法上也存在如下差别:
➢ 接口里只能包含 抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法。
➢ 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
➢ 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
➢ 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
➢ 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口。
➢ 接口是一种规范,抽象类是模板模式。
6.7、 内部类
Java从JDK 1.1开始引入内部类,内部类主要有如下作用:
➢ 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
➢ 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
➢ 匿名内部类适合用于创建那些仅需要一次使用的类。
从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除需要定义在其他类里面之外,还存在如下两点区别:
➢ 内部类比外部类可以多使用三个修饰符:private、protected、static —外部类不可以使用这三个修饰符。
➢ 非静态内部类不能拥有静态成员。
6.7.1、 非静态内部类
内部类 都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类 和 匿名内部类 则不是类成员。
成员内部类分为两种:静态内部类 和 非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。
成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$InnerClass.class。
当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;
如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;
如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;
如果依然不存在,系统将出现编译错误:提示找不到该变量。因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分。
非静态内部类的成员可以访问外部类的实例成员,但反过来就不成立了。如果外部类需要访问非静态内部类的实例成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。
Java不允许在非静态内部类里定义静态成员,非静态内部类里不能有静态方法、静态成员变量、静态初始化块。
6.7.2、 静态内部类
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。
即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。
6.7.3、 使用内部类
1.在外部类内部使用内部类
从前面程序中可以看出,在外部类内部使用内部类时,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。
唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
2.在外部类以外使用非静态内部类
如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。
对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用:
➢ 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问。
➢ 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
➢ 使用public修饰的内部类,可以在任何地方被访问。
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
由于非静态内部类的对象必须寄生在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法如下:
3.在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:
6.7.4、 局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。
6.7.5 匿名内部类
匿名内部类适合创建那种只需要一次使用的类,定义匿名内部类的格式如下:
关于匿名内部类还有如下两条规则:
➢ 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
➢ 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
6.8 Java 11增强的Lambda表达式
当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,Lambda表达式就相当一个匿名方法。
从上面语法格式可以看出,Lambda表达式的主要作用就是代替匿名内部类的烦琐语法,它由三部分组成:
➢ 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
➢ 箭头(->)。必须通过英文中画线和大于符号组成。
➢ 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。
Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。
Lambda表达式的类型,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。
函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
前面已经介绍过,如果Lambda表达式的代码块只有一条代码,程序就可以省略Lambda表达式中代码块的花括号。不仅如此,如果Lambda表达式的代码块只有一条代码,
还可以在代码块中使用方法引用和构造器引用。方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。
Lambda表达式支持如表6.2所示的几种引用方式:
Lambda表达式与匿名内部类的联系和区别:
Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用。
Lambda表达式与匿名内部类存在如下相同点:
➢ Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
➢ Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
Lambda表达式与匿名内部类主要存在如下区别:
➢ 匿名内部类可以为任意接口创建实例—不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例。
➢ 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
➢ 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中定义的默认方法。
6.9 枚举类
Java 5新增了一个enum关键字(它与class、interface关键字的地位相同),用以定义枚举类。正如前面看到的,枚举类是一种特殊的类,
它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。
但枚举类终究不是普通类,它与普通类有如下简单区别:
➢ 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显式继承其他父类。
其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
➢ 使用enum定义、非抽象的枚举类默认会使用final修饰。
➢ 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
由于枚举类的所有构造器都是private的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类。
➢ 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。
枚举类默认提供了一个values()方法,该方法可以很方便地遍历所有的枚举值。
6.10 对象与垃圾回收
6.10.1 对象在内存中的状态
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
➢ 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
➢ 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,
系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。
➢ 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。
只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
6.10.2 强制垃圾回收
当一个对象失去引用后,系统何时调用它的finalize()方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。
程序只能控制一个对象何时不再被任何引用变量引用,绝不能控制它何时被回收。程序无法精确控制Java垃圾回收的时机,但依然可以强制系统进行垃圾回收—这种强制只是通知系统进行垃圾回收,
但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。
强制系统垃圾回收有如下两种方式:
➢ 调用System类的gc()静态方法:System.gc()。
➢ 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()。
这种强制只是建议系统立即进行垃圾回收,系统完全有可能并不立即进行垃圾回收,垃圾回收机制也不会对程序的建议完全置之不理:垃圾回收机制会在收到通知后,尽快进行垃圾回收。
6.10.3 finalize方法
finalize()方法具有如下4个特点:
➢ 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用。
➢ finalize()方法何时被调用,是否被调用具有不确定性,不要把finalize()方法当成一定会被执行的方法。
➢ 当JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态。
➢ 当JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。
6.10.4 对象的软、弱和虚引用
Java语言对对象的引用有如下4种方式:
1、强引用(StrongReference)这是Java程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象,
前面介绍的对象和数组都采用了这种强引用的方式。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
2、软引用(SoftReference)软引用需要通过SoftReference类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,
它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
3、弱引用(WeakReference)弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,
不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收—正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
4、虚引用(PhantomReference)虚引用通过PhantomReference类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。
如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。