Object类大概是每一个JAVA程序员认识的第一个类,由于它是全部其余类的祖先类。在JAVA单根继承的体系下,这个类中的每一个方法都显得尤其重要,由于每一个类都可以调用或者重写这些方法。当你JAVA学到必定阶段,尤为是学到了反射机制、多线程和JVM以后,再回过头看一眼这些方法,可能会有新的体会。java
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()的实现:程序员
源码以下:面试
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() 与 ==的区别:算法
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() 方法经过哈希算法为每一个对象生成一个整数值,称为散列值。ide
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()方法用于返回对象的字符串表示,默认实现以下:
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() 是 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方法利用反射机制获取当前对象的Class对象。getClass方法是一个final方法,不容许子类重写,而且也是一个native方法。
这几个方法用于java多线程之间的协做。
下面举个例子说明:
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()
方法只能在同步代码块中才能使用。
从上述例子的输出结果中能够得出以下结论:
一、wait()
方法调用后当前线程将当即阻塞,且适当其所持有的同步代码块中的锁,直到被唤醒或超时或打断后且从新获取到锁后才能继续执行;
二、notify() / notifyAll()
方法调用后,其所在线程不会当即释放所持有的锁,直到其所在同步代码块中的代码执行完毕,此时释放锁,所以,若是其同步代码块后还有代码,其执行则依赖于JVM的线程调度。
finalize方法主要与Java垃圾回收机制有关,JVM准备对此对对象所占用的内存空间进行垃圾回收前,将会调用该对象的finalize方法。
finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是肯定的(对象离开做用域或delete掉),但Java中的finalize的调用具备不肯定性。
关于finalize方法的Best Practice就是在大多数时候不须要手动去调用该方法,让GC为你工做吧!