Object类:又回到最初的起点

Object类大概是每个JAVA程序员认识的第一个类,因为它是所有其他类的祖先类。在JAVA单根继承的体系下,这个类中的每个方法都显得尤为重要,因为每个类都能够调用或者重写这些方法。当你JAVA学到一定阶段,尤其是学到了反射机制、多线程和JVM之后,再回过头看一眼这些方法,可能会有新的体会。

Object根类方法

public final native Class<?> getClass()

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

protected void finalize() throws Throwable {}

equals()

equals()的实现:

  • 检查是否为同一个对象的引用,如果是直接返回 true;
  • 检查是否是同一个类型,如果不是,直接返回 false;
  • 将 Object 对象进行转型;
  • 判断每个关键域是否相等。

源码如下:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                  : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }

equals() 与 ==的区别:

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。

hashCode()

hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。这句话一定要想清楚,如果知道散列冲突的话,这句话也不难理解。在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。

比如下面这个例子,由于没有覆盖hashCode()方法,set会认为是两个不同的对象,去重失败。

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2

简单来说,hashCode() 方法通过哈希算法为每个对象生成一个整数值,称为散列值。

hashCode()方法的算法约定为:

  • 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 两个相等的对象(通过equals方法判断)必须返回相同哈希值。
  • 两个不相等的对象(通过equals方法判断),调用hashCode()方法返回值不是必须不相等。

下面以一个例子演示hashCode方法的覆写:

public class User {

    private long id;
    private String name;
    private String email;
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id != user.id 
          && (!name.equals(user.name) 
          && !email.equals(user.email));
    }
}

上面的代码中重写了equals方法,hashCode方法的不同重现版本如下:

//实现1  
@Override
    public int hashCode() {
        return 1;
}

//实现2
@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

//实现3(标准实现)
@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}
  • 实现1:

    哈希表中所有对象保存在同一个位置,哈希表退化成了链表。

  • 实现2:

    这个比较好,这样不同对象的哈希码发生的碰撞的概率就比较小了

  • 实现3(标准实现):

    用到了素数31,至于为什么要用31,Effective Java中做了比较清楚的解答,这里直接粘过来了:

    之所以选择31,是因为它是个素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的好处并不明显。但是习惯上都使用素数来计算散列结果。31有很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31 * i == (i << 5) - i。现在 虚拟机可以自动完成这种优化。

    这段话的最后讲到了重点,即使用31还是主要出于效率上的考虑。

toString()

toString()方法用于返回对象的字符串表示,默认实现如下:

public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

该字符串由类名 + ”@“ + 此对象散列值的无符号十六进制表示组成。输出对象时会自动调用toString方法把对象转化为字符串,比如System.out.println(obj);

Effective JAVA第12条指出:始终要覆盖toString方法。因为这种默认的输出形式不太可能是我们想要的,每个类应该实现自己的toString方法,比如下面这个例子:

public class User implements Serializable {
    private long id;
    private long phone;
    private String name;
    private String password;
    @Override
    public String toString() {
        return "User{" + "id=" + id + ", phone=" + phone + ", name=" + name + ", password=" + password + "}";
    }
}

clone()

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

一个类要重写clone()方法必须要实现Cloneable接口,即:

public class CloneDemo implements Cloneable {
    private int a;
    private int b;
  
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

但是clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。实际上这个接口没有包含任何的方法,Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。这种规定看上去的确非常奇怪,难怪Effective JAVA作者会说“Cloneable是接口的一种极端非典型的用法,也不值得效仿。通常情况下,实现接口是为了表明类可以为它的客户做些什么。”

clone又分为浅拷贝和深拷贝,这两者的区别也在面试中经常被问到。

简单说,两者的区别就是,在浅拷贝中拷贝对象和原始对象的引用类型引用同一个对象,在深拷贝中拷贝对象和原始对象的引用类型引用不同对象。

下面以一个例子说明,这个例子来自Effective JAVA。假如我们要为HashTable类实现Clone方法,它的内部维护了一个节点数组,部分代码是这样的:

class HashTable implements Cloneable {
      private Entry[] buckets;

      private static class Entry {
            final Object key;
            Object value;
            Entry next;

            public Entry(Object key, Object value, Entry next) {
                  super();
                  this.key = key;
                  this.value = value;
                  this.next = next;
	      }
      }
  // clone()...
}

浅拷贝如下,虽然拷贝对象也有自己的散列桶数组,但这个数组引用的链表与原始数组是一样的,这样就会引发诸多不确定行为。

 @Override
protected Object clone() throws CloneNotSupportedException {
      HashTable result = (HashTable) super.clone();
      result.buckets = buckets.clone();
      return result;
      }

深拷贝如下,可以看到深拷贝用递归的方式重新创建了一个新的散列桶数组,和原对象的不同。

@Override
protected Object clone() throws CloneNotSupportedException {
      HashTable result = (HashTable) super.clone();
      result.buckets = new Entry[buckets.length];
      for (int i = 0; i < buckets.length; i++)
            if (buckets[i] != null)
                  result.buckets[i] = buckets[i].deepCopy();
      return result;
      }
      private Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
      }

Effective JAVA提到对象拷贝的更好的办法是使用拷贝工厂而不是重写clone()方法,除非拷贝的是数组。

getClass()

getClass方法利用反射机制获取当前对象的Class对象。getClass方法是一个final方法,不允许子类重写,并且也是一个native方法。

wait() / notify() / notifyAll()

这几个方法用于java多线程之间的协作。

  • wait():调用此方法所在的当前线程等待,直到在其他线程上调用notify() / notifyAll()方法唤醒该线程。
  • wait(long timeout):调用此方法所在的当前线程等待,直到在其他线程上调用notify() / notifyAll()方法或者等待时间超过传入参数表示的时间,该线程被唤醒。
  • notify() / notifyAll():唤醒在此对象监视器上等待的单个线程/所有线程。

下面举个例子说明:

public class ThreadTest {
 
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
        synchronized (r) {
            try {
                System.out.println("main thread 等待t线程执行完");
                r.wait();
                System.out.println("被notity唤醒,得以继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("main thread 本想等待,但被意外打断了");
            }
            System.out.println("线程t执行相加结果" + r.getTotal());
        }
    }
}
 
class MyRunnable implements Runnable {
    private int total;
 
    @Override
    public void run() {
        synchronized (this) {
            System.out.println("Thread name is:" + Thread.currentThread().getName());
            for (int i = 0; i < 10; i++) {
                total += i;
            }
            notify();
            System.out.println("执行notify后同步代码块中依然可以继续执行直至完毕");
        }
        System.out.println("执行notify后且同步代码块外的代码执行时机取决于线程调度");
    }
 
    public int getTotal() {
        return total;
    }
}

输出结果:

main thread 等待t线程执行完
Thread name is:Thread-0
执行notif后同步代码块中依然可以继续执行直至完毕
执行notif后且同步代码块外的代码执行时机取决于线程调度
被notity唤醒,得以继续执行
线程t执行相加结果45

既然是作用于多线程中,为什么却是Object这个基类所具有的方法?原因在于理论上任何对象都可以视为线程同步中的监听器,且wait() / notify() / notifyAll()方法只能在同步代码块中才能使用。

从上述例子的输出结果中可以得出如下结论:

1、wait()方法调用后当前线程将立即阻塞,且适当其所持有的同步代码块中的锁,直到被唤醒或超时或打断后且重新获取到锁后才能继续执行;

2、notify() / notifyAll()方法调用后,其所在线程不会立即释放所持有的锁,直到其所在同步代码块中的代码执行完毕,此时释放锁,因此,如果其同步代码块后还有代码,其执行则依赖于JVM的线程调度。

finalize()

finalize方法主要与Java垃圾回收机制有关,JVM准备对此对对象所占用的内存空间进行垃圾回收前,将会调用该对象的finalize方法。

finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性。

关于finalize方法的Best Practice就是在大多数时候不需要手动去调用该方法,让GC为你工作吧!