Java 问答:终极父类(2)

我之前发布了关于java.lang.Object类及其方法的一系列文章。在介绍了Object之后,我们又探究了clone()和euqals()方法。在这篇文章中,我们将继续讨论Object中的finalize()、getClass()和hashCode()方法。

终止

问: finalize()方法是用来做什么的?

答: finalize()方法可以被子类对象所覆盖,然后作为一个终结者,当GC被调用的时候完成最后的清理工作(例如释放系统资源之类)。这就是终止。默认的finalize()方法什么也不做,当被调用时直接返回。

对于任何一个对象,它的finalize()方法都不会被JVM执行两次。如果你想让一个对象能够被再次调用的话(例如,分配它的引用给一个静态变量),注意当这个对象已经被GC回收的时候,finalize()方法不会被调用第二次。

问: 有人说要避免使用finalize()方法,这是真的吗?

答: 通常来讲,你应该尽量避免使用finalize()。相对于其他JVM实现,终结器被调用的情况较少——可能是因为终结器线程的优先级别较低的原因。如果你依靠终结器来关闭文件或者其他系统资源,可能会将资源耗尽,当程序试图打开一个新的文件或者新的系统资源的时候可能会崩溃,就因为这个缓慢的终结器。

问: 应该使用什么来替代终结器?

答: 提供一个明确的用来销毁这个对象的方法(例如,java.io.FileInputStream的void close()方法),并且在代码中使用try - finally结构来调用这个方法,以确保无论有没有异常从try中抛出,都会销毁这个对象。参考下面释放锁的代码:

Lock l = ...; // ... is a placeholder for the actual lock-acquisition code
l.lock();
try {
   // access the resource protected by this lock
} 
finally {
   l.unlock();
}

这段代码保证了无论try是正常结束还是抛出异常都会释放锁。

问: 什么情况下适合使用终结器?

答: 终结器可以作为一个安全保障,以防止声明的终结方法(像是java.io.FileOutputStream对象的close()方法或者java.util.concurrent.Lock对象的Lock()方法)没有被调用。万一这种情况出现,终结器可以在最后被调用,释放临街资源。

问: 怎么写finalize()?

答: 可以遵循下面这个模式写finalize()方法:

@Override
protectedvoidfinalize()throwsThrowable {
   try {
      // Finalize the subclass state.
      // ...
   }
   finally {
      super.finalize();
   }
}

子类终结器一般会通过调用父类的终结器来实现。当被调用时,先执行try模块,然后再在对应的finally中调用super.finalize();这就保证了无论try会不会抛出异常父类都会被销毁。

问: 如果finalize()抛出异常会怎样?

答: 当finalize()抛出异常的时候会被忽略。而且,对象的终结将在此停止,导致对象处在一种不确定的状态。如果另一个进程试图使用这个对象的话,将产生不确定的结果。通常抛出异常将会导致线程终止并产生一个提示信息,但是从finalize()中抛出异常就不会。

问: 我想实践一下finalize()方法,能提供一个范例吗?

答: 参考代码清单1.

class LargeObject {
   byte[] memory = new byte[1024*1024*4];

   @Override
   protected void finalize() throws Exception {
      System.out.println("finalized");
   }
}

public class FinalizeDemo {
   public static void main(String[] args) {
      while (true)
         new LargeObject();
   }
}

代码清单1:实践finalize()

代码清单1中的代码写了一个FinalizeDemo程序,重复地对largeObject类实例化。每一个Largeobject对象将产生4M的数组。在这种情况下,由于没有指向该对象的引用,所以LargeObject对象将被GC回收。

GC会调用对象的finalize()方法来回收对象。LargeObject重载的finalize()方法被调用的时候会想标准输出流打印一条信息。它没有调用父类的finalize()方法,因为它的父类是Object,即父类的finalize()方法什么也不做。

编译(javac FinalizeDemo.java)并运行(java FinalizeDemo)代码清单1.当我在我的环境下(64位win7平台)使用JDK7u6来编译运行的时候,我看到一列finalized的信息。但是在JDK8的环境下时,在几行finalized之后抛出了java.lang.OutOfMemoryError。

因为finalize()方法对于虚拟机来说不是轻量级的程序,所以不能保证你一定会在你的环境下观察到输出信息。

得到对象的类

问: getClass()方法是用来做什么的?

答: 通过getClass()方法可以得到一个和这个类有关的java.lang.Class对象。返回的Class对象是一个被static synchronized方法封装的代表这个类的对象;例如,static sychronized void foo(){}。这也是指向反射API。因为调用getClass()的对象的类是在内存中的,保证了类型安全。

问: 还有其他方法得到Class对象吗?

答: 获取Class对象的方法有两种。可以使用类字面常量,它的名字和类型相同,后缀为.class;例如,Account.class。另外一种就是调用Class的forName()方法。类字面常量更加简洁,并且编译器强制类型安全;如果找不到指定的类编译就不会通过。通过forname()可以动态地通过指定包名载入任意类型地引用。但是,不能保证类型安全,可能会导致Runtime异常。

问: 实现equals()方法的时候,getClass()和instanceof哪一个更好?

答: 使用getClass()还是instanceof的话题一直都是Java社区争论的热点,Angelika Langer的Secrets of equals – Part 1这片文章可以帮助你做出选择。关于正确覆盖equals()方法(例如保证对称性)的讨论,Lang的这篇文章可以作为一个很好的参考手册。

哈希码

问: hashCode()方法是用来做什么的?

答: hashCode()方法返回给调用者此对象的哈希码(其值由一个hash函数计算得来)。这个方法通常用在基于hash的集合类中,像java.util.HashMap,java.until.HashSetjava.util.Hashtable.

问: 在类中覆盖equals()的时候,为什么要同时覆盖hashCode()

答: 在覆盖equals()的时候同时覆盖hashCode()可以保证对象的功能兼容于hash集合。这是一个好习惯,即使这些对象不会被存储在hash集合中。

问: hashCode()有什么一般规则?

答: hashCode()的一般规则如下:

  • 在同一个Java程序中,对一个相同的对象,无论调用多少次hashCode()hashCode()返回的整数必须相同,因此必须保证equals()方法比较的内容不会更改。但不必在另一个相同的Java程序中也保证返回值相同。
  • 如果两个对象用equals()方法比较的结果是相同的,那么这两个对象调用hashCode()应该返回相同的整数值。
  • 当两个对象使用equals()方法比较的结果是不同的,hashCode()返回的整数值可以不同。然而,hashCode()的返回值不同可以提高哈希表的性能。

问: 如果覆盖了equals()却不覆盖hashCode()会有什么后果?

答: 当覆盖equals()却不覆盖hashCode()的时候,在hash集合中存储对象时就会出现问题。例如,参考代码清单2.

代码清单2:当hash集合只覆盖equals()时的问题

import java.util.HashMap;
import java.util.Map;

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)));
   }
}

代码清单2声明了一个Employee类,覆盖了equals()方法但是没有覆盖hashCode()。同时声明了一个一个HashDemo类,来演示将Employee作为键存储时时产生的问题。

main()函数首先在实例化Employee之后创建了一个hashmap,将Employee对象作为键,将一个字符串作为值来存储。然后它将这个对象作为键来检索这个集合并输出结果。同样地,再通过新建一个具有相同内容的Employee对象作为键来检索集合,输出信息。

编译(javac HashDemo.java)并运行(java HashDemo)代码清单2,你将看到如下输出结果:

first employee
null

如果hashCode()方法被正确的覆盖,你将在第二行看到first employee而不是null,因为这两个对象根据equals()方法比较的结果是相同的,根据上文中提到的规则2:如果两个对象用equals()方法比较的结果是相同的,那么这两个对象调用hashCode()应该返回相同的整数值。

问: 如何正确的覆盖hashCode()

答: Joshua Bloch的《Effective Java》第八版中给出了一个四步法来正确的覆盖hashCode()。下面的步骤和Bloch的方法类似。

  1. 声明一个int型的变量,命名为result(或者其他你喜欢的名字),然后初始化为一个不为零的常量(比如31)。使用一个不为零的常量会影响到所有的初始的哈希值(步骤2.1的结果)为零的值。【A nonzero value is used so that it will be affected by any initial fields whose hash value (computed in Step 2.1) is zero. 】如果初始的result为0的话,最后的哈希值不会被它影响到,所以冲突的几率会增加。这个非零result值是任意的。
  2. 对每一个对象中有意义的具体值(在equals()中所涉及的值),f,进行以下步骤的处理:
    1. 按照以下步骤计算f的基于int型的哈希值hc:
      a. 对于一个boolean型变量,hc=f?0:1;。 b. 对于一个byte,char,short,或者int型变量,hc = (int)f;. c. 对于一个long型变量,hc = (int) (f ^ (f >>> 32));.这个表达式是将long型变量作为32位(long型最多有32位)来计算的; d. 对于一个float型变量,hc = Float.floatToIntBits(f);. e. 对于一个double型变量,long l = Double.doubleToLongBits(f); hc = (int) (l ^ (l >>> 32));. f. 对于引用类型的变量,如果类中的equals()方法递归的调用equals()类比较成员变量,那么就递归调用hashCode();如果需要更复杂的比较,就计算这个值的“标准表示”来脚酸标准的哈希值;如果引用类型的值为null,f = 0. g. 对于一个数组类型的引用,将每一个元素视为单独的变量,对于每一个有意义的值,调用对应的方法计算其哈希值,最后如步骤2.2的描述那样将所有的哈希值合并。
    2. 计算result=37*result+hc,将所有的hc合并到哈希值中。乘法使哈希值取决于它的值的规则,当一个类中存在多种相似的值时,就增加了哈希表的离散性。
  3. 返回result。
  4. 完成hashCode()之后,要确保相同的对象调用hashCode()得到相同的哈希值。

举例说明上面这个方法,代码清单3是代码清单2的第二个版本,它的Employee类重写了hashCode()

代码清单3:正确地覆盖hashCode()

import java.util.HashMap;
import java.util.Map;

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;
   }

   @Override
   public int hashCode() {
      int result = 31;
      result = 37*result+name.hashCode();
      result = 37*result+age;
      return result;
   }
}

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)));
   }
}

代码清单3的Employee类中声明了两个在hashCode()都涉及到的值。覆盖的hashCode()方法首先初始化result为31,然后将String类型的name变量和int型的age变量的哈希值合并到result中,随后返回result。

编译(javac HashDemo.java)并运行(java HashDemo)代码清单3,你将看到如下输出结果:

first employee
first employee

The ultimate superclass, Part 2 Java问答:终极父类(2)—上篇 Java问答:终极父类(2)—下篇

  • qq_43638135
    妲己再美究为妃: 博主没有想过自己接一些私活干吗?我现在还没毕业,但是我也确实听说外挂市场自动化游戏脚本市场挺火热的,并且报酬也很丰厚,但是具体的我也不是很清楚,求解答。 (1个月前 #47楼) 查看回复(2) 举报 回复
    22