finalize()
方法可以被子类对象所覆盖,然后作为一个终结者,当GC
被调用的时候完成最后的清理工作(例如释放系统资源之类)。这就是终止。默认的finalize()
方法什么也不做,当被调用时直接返回。
对于任何一个对象,它的finalize()
方法都不会被JVM
执行两次。如果你想让一个对象能够被再次调用的话(例如,分配它的引用给一个静态变量),注意当这个对象已经被GC回收的时候,finalize()
方法不会被调用第二次。
finalize()
是Object
的protected
方法,子类可以覆盖该方法以实现资源清理工作,GC
在回收对象之前调用该方法。
finalize()
与C++
中的析构函数
不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java
中的finalize
的调用具有不确定性,不建议用finalize
方法完成非内存资源
的清理工作,但建议用于:
可以遵循下面这个模式写finalize()方法:
@Override protected void finalize() throws Throwable{ try { // Finalize the subclass state. // ... } finally { super.finalize(); } }
子类终结器一般会通过调用父类的终结器来实现。当被调用时,先执行try模块,然后再在对应的finally
中调用super.finalize()
;这就保证了无论try
会不会抛出异常父类都会被销毁。
当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的行为)
大致描述一下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
:对象不可通过上面两种途径可达hashCode()
方法返回给调用者此对象的哈希码(其值由一个hash
函数计算得来)。这个方法通常用在基于hash
的集合类中,像java.util.HashMap,java.until.HashSet和java.util.Hashtable.
在覆盖equals()
的时候同时覆盖hashCode()
可以保证对象的功能兼容于hash集合
。这是一个好习惯,即使这些对象不会被存储在hash
集合中。
在同一个Java
程序中,对一个相同的对象,无论调用多少次hashCode()
,hashCode()
返回的整数必须相同,因此必须保证equals()
方法比较的内容不会更改。但不必在另一个相同的Java
程序中也保证返回值相同。
如果两个对象用equals()
方法比较的结果是相同的,那么这两个对象调用hashCode()
应该返回相同的整数值。
当两个对象使用equals()
方法比较的结果是不同的,hashCode()
返回的整数值可以不同。然而,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()
应该返回相同的整数值。
toString()
方法将根据调用它的对象返回其对象的字符串形式,通常用于debug
当toString()
没有被覆盖的时候,返回的字符串格式是 类名@哈希值
,哈希值是十六进制
。举例说,假设有一个 Employee
类,toString()
方法返回的结果可能是 Empoyee@1c7b0f4d
根据对象的引用,调用引用的 toString()
。例如,假设 emp
包含了一个 Employee
引用,调用 emp.toString()
就会得到这个对象的字符串形式。
System.out.println(o.toString()); 和 System.out.println(o)
两者的输出结果中都包含了对象的字符串形式。区别是,System.out.println(o.toString());
直接调用toString()
方法,而System.out.println(o)
则是隐式调用了 toString()