1 Finalize
1.1 finalize方法作用
finalize()
方法可以被子类对象所覆盖,然后作为一个终结者,当GC
被调用的时候完成最后的清理工作(例如释放系统资源之类)。这就是终止。默认的finalize()
方法什么也不做,当被调用时直接返回。
对于任何一个对象,它的finalize()
方法都不会被JVM
执行两次。如果你想让一个对象能够被再次调用的话(例如,分配它的引用给一个静态变量),注意当这个对象已经被GC回收的时候,finalize()
方法不会被调用第二次。
finalize()
是Object
的protected
方法,子类可以覆盖该方法以实现资源清理工作,GC
在回收对象之前调用该方法。finalize()
与C++
中的析构函数
不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java
中的finalize
的调用具有不确定性,不建议用finalize
方法完成非内存资源
的清理工作,但建议用于:
- 清理本地对象(通过JNI创建的对象);
- 作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法
1.2 怎么使用finalize
可以遵循下面这个模式写finalize()方法:
@Override
protected void finalize() throws Throwable{
try {
// Finalize the subclass state.
// ...
} finally {
super.finalize();
}
}
子类终结器一般会通过调用父类的终结器来实现。当被调用时,先执行try模块,然后再在对应的finally
中调用super.finalize()
;这就保证了无论try
会不会抛出异常父类都会被销毁。
1.3 finalize异常和问题
当finalize()
抛出异常的时候会被忽略。而且,对象的终结将在此停止,导致对象处在一种不确定的状态。如果另一个进程试图使用这个对象的话,将产生不确定的结果。通常抛出异常将会导致线程终止并产生一个提示信息,但是从finalize()
中抛出异常就不会
一些与finalize
相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()
方法、Runtime.runFinalizersOnExit()
方法System.gc()与System.runFinalization()
方法增加了finalize
方法执行的机会,但不可盲目依赖它们Java
语言规范并不保证finalize
方法会被及时地执行、而且根本不会保证它们会被执行finalize
方法可能会带来性能问题。因为JVM
通常在单独的低优先级线程中完成finalize
的执行
对象再生问题:finalize
方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的finalize
方法至多由GC
执行一次(用户当然可以手动调用对象的finalize
方法,但并不影响GC对finalize的行为)
1.4 finalize执行过程(生命周期)
大致描述一下finalize流程:
当对象变成(GC Roots
)不可达时,GC
会判断该对象是否覆盖了finalize
方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize
方法,将其放入F-Queue
队列,由一低优先级线程执行该队列中对象的finalize
方法。执行finalize
方法完毕后,GC
会再次判断该对象是否可达,若不可达,则进行回收,否则,对象复活
。(GC Roots相关知识点学习)
具体的finalize
流程:
对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};
二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}
。
各状态含义如下:
-
unfinalized
: 新建对象会先进入此状态,GC
并未准备执行其finalize
方法,因为该对象是可达的 -
finalizable
: 表示GC
可对该对象执行finalize
方法,GC
已检测到该对象不可达。正如前面所述,GC
通过F-Queue
队列和一专用线程完成finalize
的执行 -
finalized
: 表示GC
已经对该对象执行过finalize
方法 -
reachable
: 表示GC Roots
引用可达 -
finalizer-reachable(f-reachable)
:表示不是reachable
,但可通过某个finalizable
对象可达 -
unreachable
:对象不可通过上面两种途径可达
2 hashCode
2.1 hashcode做什么用
hashCode()
方法返回给调用者此对象的哈希码(其值由一个hash
函数计算得来)。这个方法通常用在基于hash
的集合类中,像java.util.HashMap,java.until.HashSet和java.util.Hashtable.
2.2 在类中覆盖equals的时候,为什么要同时覆盖hashCode
在覆盖equals()
的时候同时覆盖hashCode()
可以保证对象的功能兼容于hash集合
。这是一个好习惯,即使这些对象不会被存储在hash
集合中。
2.3 hashCode一般规则
在同一个Java
程序中,对一个相同的对象,无论调用多少次hashCode()
,hashCode()
返回的整数必须相同,因此必须保证equals()
方法比较的内容不会更改。但不必在另一个相同的Java
程序中也保证返回值相同。
如果两个对象用equals()
方法比较的结果是相同的,那么这两个对象调用hashCode()
应该返回相同的整数值。
当两个对象使用equals()
方法比较的结果是不同的,hashCode()
返回的整数值可以不同。然而,hashCode()
的返回值不同可以提高哈希表的性能。
2.4 如果覆盖了equals却不覆盖hashCode的后果
当覆盖equals()
却不覆盖hashCode()
的时候,在hash
集合中存储对象时就会出现问题。
当hash集合只覆盖equals()时的问题
final class Employee
{
private String name;
private int age;
Employee(String name, int age)
{
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Employee))
return false;
Employee e = (Employee) o;
return e.getName().equals(name) && e.getAge() == age;
}
String getName()
{
return name;
}
int getAge()
{
return age;
}
}
public class HashDemo
{
public static void main(String[] args)
{
Map<Employee, String> map = new HashMap<>();
Employee emp = new Employee("John Doe", 29);
map.put(emp, "first employee");
System.out.println(map.get(emp));
System.out.println(map.get(new Employee("John Doe", 29)));
}
}
代码中声明了一个Employee
类,覆盖了equals()
方法但是没有覆盖hashCode()
。同时声明了一个HashDemo
类,来演示将Employee
作为键存储时产生的问题。main()
函数首先在实例化Employee
之后创建了一个hashmap
,将Employee
对象作为键,将一个字符串作为值来存储。然后它将这个对象作为键来检索这个集合并输出结果。同样地,再通过新建一个具有相同内容的Employee
对象作为键来检索集合,输出信息。
将看到如下输出结果:
first employee
null
如果hashCode()
方法被正确的覆盖,将在第二行看到first employee
而不是null
,因为这两个对象根据equals()
方法比较的结果是相同的,根据上文中提到的:如果两个对象用equals()
方法比较的结果是相同的,那么这两个对象调用hashCode()
应该返回相同的整数值。
3 toString
3.1 toString方法实现什么功能
toString()
方法将根据调用它的对象返回其对象的字符串形式,通常用于debug
3.2 当toString方法没有被覆盖的时候,返回的字符串通常是什么样子
当toString()
没有被覆盖的时候,返回的字符串格式是 类名@哈希值
,哈希值是十六进制
。举例说,假设有一个 Employee
类,toString()
方法返回的结果可能是 Empoyee@1c7b0f4d
3.3 如何得到字符串的表达形式
根据对象的引用,调用引用的 toString()
。例如,假设 emp
包含了一个 Employee
引用,调用 emp.toString()
就会得到这个对象的字符串形式。
3.4 System.out.println(o.toString()); 和 System.out.println(o) 的区别
System.out.println(o.toString()); 和 System.out.println(o)
两者的输出结果中都包含了对象的字符串形式。区别是,System.out.println(o.toString());
直接调用toString()
方法,而System.out.println(o)
则是隐式调用了 toString()