一. Object类介绍
java.lang.Object类是所有类的父类,每个类都使用 Object 作为超类。所有对象(包括数组)都实现这个类的方法。在不明确给出超类的情况下,Java会自动把Object作为要定义类的超类。可以使用类型为Object的变量指向任意类型的对象。
Object类提供了多个方法,具体如下:
- public Object()
- public final native Class<?> getClass()
- public native int hashCode()
- public boolean equals(Object obj)
- public String toString()
- protected native Object clone()
- public final native void notify()
- public final native void notifyAll()
- public final native void wait(long timeout)
- public final void wait(long timeout, int nanos)
- public final void wait()
- protected void finalize()
下面笔者将详细介绍这些重要的方法。
二. 方法详细介绍
1. public Object()
这个方法是Object类的一个默认的构造方法,在构造子类时,都会先调用这个默认构造方法。
2. public final native Class<?> getClass()
getClass()方法是一个final方法,这就意味着不允许子类重写,同时这也是一个native方法。
getClass()返回该对象的运行时类的 java.lang.Class 对象,该对象保存着原对象的类信息,比如原对象的类名叫什么,类里有什么方法,字段等,这些信息可以由Class对象的getName()、getMethods()等方法获取。该方法返回值为Class对象,后面的“?”是泛型(“?”则属于类型通配符的一种),代表着正在运行的类。
这里笔者只是简单介绍Object类getClass()的作用,而Class对象更多的是与反射联系在一起,关于反射的内容请查看其它相关的资料。
3. public native int hashCode()
hashCode()方法同样是一个native方法,该方法返回对象的哈希码值。hashCode是用来在散列存储结构中确定对象的存储地址的,在HashMap、Hashtable等底层实现都有用到,其主要是用于查找的快捷性,因为在集合查找时,hashcode能大大降低对象比较次数,提高查找效率。
关于hashCode有几点常规的约定:
① 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
② 如果根据 equals(Object) 方法判定两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
③ 如果根据equals方法得到两个对象不相等,那么这2个对象的hashCode值不需要必须不相同,即可以相同。但是,不相等的对象的hashCode值不同的话可以提高哈希表的性能。
④当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定。
实际上,由于hashCode一般是通过将该对象的内部地址转换成一个整数来实现的,所以通常情况下,不同的对象产生的哈希码是不同的。
public class User {private String name;private int sex;private String address;public User(String name, int sex, String address) {super();this.name = name;this.sex = sex;this.address = address;}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((address == null) ? 0 : address.hashCode());result = prime * result + ((name == null) ? 0 : name.hashCode());result = prime * result + sex;return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;User other = (User) obj;if (address == null) {if (other.address != null)return false;} else if (!address.equals(other.address))return false;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;if (sex != other.sex)return false;return true;}
}
这个例子给出User类,有三个属性:name、sex和address。使用Eclipse自动生成hashCode()和equals() 方法如上。我们先跳过equals()方法直接看 hashCode()方法实现。从上面的代码可以看到hashCode()方法首先声明一个值为31的整型变量,然后遍历所有的类属性,累加计算result=31*result+A,若A是整型数直接相加,若是String类型调用String类的hashCode()方法(其他类型具体实现可自己自动生成hashCode()方法查看,这里笔者只简单讲下String类型),然后String类的hashCode()实现如下:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h;
}
上面的代码其实做的就是计算s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]的值, s[i]是string的第i个字符,n是String的长度。那么问题来了,为什么上面的User类还有这里的String类都使用31做乘数,而不是用其他数呢?主要有两个原因:①之所以选择31,是因为它是个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的好处并不是很明显,但是习惯上都使用素数来计算散列结果。②31有个很好的特性,就是用移位和减法来代替乘法,可以得到更好的性能:31*i==(i<<5)-i。
因此,hashCode()方法的实现通常使用31来作乘数,同时方法体里遍历所有值来累加得到hashCode的实现能够导致相同的value就一定有相同的hashCode,不同的value也较大可能得到不同的hashCode,但这不是一定的,不同的value还是有可能有相同的hashCode的。至于为什么要使用累加得到hashCode这么复杂的实现,就需要数学家来解释了。
4. public boolean equals(Object obj)
equals()方法用来比较两个对象是否相等,默认实现是使用“==”直接比较,即比较两个对象的内存地址是否相等:
public boolean equals(Object obj) { return (this == obj);
}
equals方法在非空对象引用上有以下特性:
①reflexive 自反性。任何非空引用值x,对于x.equals(x)必须返回true。
②symmetric 对称性。任何非空引用值x和y,如果x.equals(y)为true,那么y.equals(x)也必须为true。
③transitive 传递性。任何非空引用值x、y和z,如果x.equals(y)为true并且y.equals(z)为true,那么x.equals(z)也必定为true。
④consistent 一致性。任何非空引用值x和y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
对于任何非空引用值 x,x.equals(null) 都应返回 false。
equals()比较容易理解,所以就不多说了。不过需要注意的是,如果重写了equals()方法,通常有必要重写hashCode()方法,这一点已经在上面已经讲过了。
5. public String toString()
toString()方法是用来放回一个简明易懂的“以文本方式表示”的字符串,建议Object所有的子类都重写这个方法。默认实现如下:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
可以看到Object 类的 toString() 方法返回一个字符串,该字符串由类名(对象是该类的一个实例)、at 标记符“@”和此对象哈希码的无符号十六进制表示组成。
6. protected native Object clone()
clone()方法创建并返回此对象的一个拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 也为true。
由于Object 类本身不实现接口 Cloneable,所以在类为 Object 的对象上调用 clone() 方法将会导致在运行时抛出异常CloneNotSupportedException。同时,如果Object的子类没有实现接口 Cloneable,调用clone()方法同样会抛出CloneNotSupportedException,以指示无法克隆某个实例。
来到这里可能有人会问,为什么需要克隆对象,直接new一个对象不行吗?答案是因为克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone()方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行吗?可以是可以,但是一方面这比较麻烦,另一方面。我们可以看到clone()方法是native方法,毫无疑问这速度快,因为实在底层实现的。
要理解好clone()方法,先要知道深度克隆以及浅度克隆。我们知道,如果要实现克隆,需要实现Cloneable接口然后重写clone()方法。我们调用clone()方法,想的是先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。如果我们重写clone()方法是只是单单调用super.clone(),对于基本数据类型,这样的操作是没有问题的,但对于非基本类型变量,比如依赖其他的类,这样实现其实clone的对象保存的只是原对象的引用,这导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。这就是浅度克隆。
public class User implements Cloneable {private String name;private int sex;private String address;public User(String name, int sex, String address) {super();this.name = name;this.sex = sex;this.address = address;}@Overrideprotected Object clone() throws CloneNotSupportedException {// TODO Auto-generated method stubreturn super.clone();}public static void main(String[] args) {User u1 = new User("Jack", 1, "USA");try {User u2 = (User) u1.clone();System.out.println(u1.name == u2.name);} catch (CloneNotSupportedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
上面的程序输出是true,证明默认调用super.clone()浅度克隆得到的是原对象的引用。
其实我们的初衷是对于要克隆的对象中的非基本数据类型的属性对应的类,也要实现克隆,这样对于非基本数据类型的属性,复制的不是一份引用,即新产生的对象和原始对象中的非基本数据类型的属性指向的不是同一个对象,这就是深度克隆。
要实现深度克隆,对于有非基本数据类型的属性的类,在clone时除了实现Cloneable接口调用super.clone()方法,对于非基本数据类型,就需要再在该属性上调用一次clone()方法:
class Address implements Cloneable {private String address;public Address(String address) {super();this.address = address;}@Overrideprotected Object clone() throws CloneNotSupportedException {// TODO Auto-generated method stubreturn super.clone();}
}public class User implements Cloneable {private String name;private int sex;private Address address;public User(String name, int sex, Address address) {super();this.name = name;this.sex = sex;this.address = address;}@Overrideprotected Object clone() throws CloneNotSupportedException {User u = (User) super.clone();u.address = (Address) this.address.clone();return u;}public static void main(String[] args) {Address add = new Address("China");User u1 = new User("Jack", 1, add);try {User u2 = (User) u1.clone();System.out.println(u1.address == u2.address);} catch (CloneNotSupportedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}
}
上面的例子中,要实现User类的深度克隆,可以看到User类的clone()方法除了需要调用super.clone(),还需要在非基本数据类型Address上调用clone()方法,当然了,Address也得实现Cloneable接口。这样才能真正实现深度克隆,上面代码执行的结果才会是false。
clone()方法还有一个需要注意的地方,就是clone()方法是一个protected属性的方法。为什么Object类要将clone()方法定义为protected,而不是public呢?其实,使用protected来修饰clone()方法,是为了安全考虑。从上面我们已经了解了深度拷贝和浅度拷贝的定义以及区别,当当前的类依赖于其他类,即有非基本数据类型的属性后,Object类无法帮我们实现深度拷贝,将修饰符定义为protected,这样想要在其他任何地方调用这个类的clone()方法时,这个类就必须去重写clone()方法并且把修饰符改为public,这样在任何地方都可以调用这个类的clone()方法了。比如下面的例子:
package com;
public class A { }
package org;
import com.A;
public class B { public static void main(String[] args) { A a = new A(); a.clone(); }
}
如上面的例子,类A是要被克隆的类,类B相当于要使用A的地方,如果类A不重写clone方法,在B类中是调不到clone()方法的,因为A和B既不是子父类关系,也不在同一个包下,所以clone()方法对B是不可见的。当类A实现Cloneable接口并且重写clone()方法后,clone()方法在B类中就可见了,也就是说我们在任何地方都可以克隆A了。
总而言之,用protected修饰clone()方法,主要是为了让子类去重写它,实现深拷贝,以防在其他任何地方随意调用后修改了对象的属性对原来的对象造成影响。如果使用public修饰clone()方法,子类就不用必须重写clone()方法,这样就可能会出现问题。
7. public final native void notify()
首先看到notify()方法是一个native方法,并且也是final的,不允许子类重写。
notify()唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait ()方法,在对象的监视器上等待。
直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
notify()方法只能被作为此对象监视器的所有者的线程来调用。一个线程要想成为对象监视器的所有者,可以使用以下3种方法:①执行对象的同步实例方法;②使用synchronized内置锁;③对于Class类型的对象,执行同步静态方法。
需要注意的是一次只能有一个线程拥有对象的监视器。如果当前线程不是此对象监视器的所有者的话会抛出IllegalMonitorStateException异常。
8. public final native void notifyAll()
notifyAll()方法跟上面的notify()一样,不同的是notifyAll()是唤醒在此对象监视器上等待的所有线程。
同样的,如果当前线程不是对象监视器的所有者,那么调用notifyAll同样会发生IllegalMonitorStateException异常。
9. public final native void wait(long timeout)
该方法导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。当前的线程必须拥有此对象监视器,否则还是会发生IllegalMonitorStateException异常。
该线程发布对此监视器的所有权并等待,直到其他线程通过调用 notify ()方法或 notifyAll() 方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。
wait()方法会让当前线程(比如线程T)将其自身放置在对象的等待集中,并且放弃该对象上的所有同步要求。出于线程调度目的,线程T是不可用并处于休眠状态,直到发生以下四件事中的任意一件:
①其他某个线程调用此对象的notify()方法,并且线程T碰巧被任选为被唤醒的线程;
②其他某个线程调用此对象的notifyAll()方法;
③其他某个线程调用Thread.interrupt()方法中断线程T;
④时间到了参数设置的超时时间。如果timeout参数为0,则不会超时,会一直进行等待。
可以理解wait()方法相当于放弃了当前线程对对象监视器的所有者(也就是说释放了对象的锁)之后,线程T会被等待集中被移除,并且重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利。一旦获得对该对象的控制权,该对象上的所有其同步声明都将被恢复到以前的状态,这就是调用wait方法时的情况。然后,线程T从wait方法的调用中返回。所以,从wait方法返回时,该对象和线程T的同步状态与调用wait方法时的情况完全相同。
在没有被通知、中断或超时的情况下,线程还可以唤醒一个所谓的虚假唤醒 (spurious wakeup)。虽然这种情况在实践中很少发生,但是应用程序必须通过以下方式防止其发生,即对应该导致该线程被提醒的条件进行测试,如果不满足该条件,则继续等待。换句话说,等待应总是发生在循环中,如下面的示例:
synchronized (obj) { while (<condition does not hold>) obj.wait(timeout); ... // Perform action appropriate to condition
}
如果当前线程在等待之前或在等待时被任何线程中断,则会抛出InterruptedException异常。在按上述形式恢复此对象的锁定状态时才会抛出此异常。
10. public final void wait(long timeout, int nanos)
跟wait(long timeout)方法类似,多了一个nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
需要注意的是 wait(0, 0)和wait(0)效果是一样的,即一直等待。
11. public final void wait()
wait()方法导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。
一般情况下,wait()方法和notify()方法会一起使用的,wait()方法阻塞当前线程,notify()方法唤醒当前线程。一个使用wait()和notify()方法的生产者消费者例子代码如下:
public class WaitNotifyTest {public static void main(String[] args) {Factory factory = new Factory();new Thread(new Producer(factory, 5)).start();new Thread(new Producer(factory, 5)).start();new Thread(new Producer(factory, 20)).start();new Thread(new Producer(factory, 30)).start();new Thread(new Consumer(factory, 10)).start();new Thread(new Consumer(factory, 20)).start();new Thread(new Consumer(factory, 5)).start();new Thread(new Consumer(factory, 5)).start();new Thread(new Consumer(factory, 20)).start();}}class Factory {public static final Integer MAX_NUM = 50;private int currentNum = 0;public void consume(int num) throws InterruptedException {synchronized (this) {while(currentNum - num < 0) {this.wait();}currentNum -= num;System.out.println("consume " + num + ", left: " + currentNum);this.notifyAll();}}public void produce(int num) throws InterruptedException {synchronized (this) {while(currentNum + num > MAX_NUM) {this.wait();}currentNum += num;System.out.println("produce " + num + ", left: " + currentNum);this.notifyAll();}}}class Producer implements Runnable {private Factory factory;private int num;public Producer(Factory factory, int num) {this.factory = factory;this.num = num;}@Overridepublic void run() {try {factory.produce(num);} catch (InterruptedException e) {e.printStackTrace();}}
}class Consumer implements Runnable {private Factory factory;private int num;public Consumer(Factory factory, int num) {this.factory = factory;this.num = num;}@Overridepublic void run() {try {factory.consume(num);} catch (InterruptedException e) {e.printStackTrace();}}
}
12. protected void finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
finalize()方法是一个protected方法,Object类的默认实现是不进行任何操作。子类需要重写 finalize ()方法,以配置系统资源或执行其他清除。
finalize 的常规协定是:当 Java虚拟机已确定尚未终止的任何线程无法再通过任何方法访问此对象时,将调用此方法,除非由于准备终止的其他某个对象或类的终结操作执行了某个操作。finalize() 方法可以采取任何操作,其中包括再次使此对象对其他线程可用;不过,finalize()的主要目的是在不可撤消地丢弃对象之前执行清除操作。
finalize ()方法执行非特殊性操作,它仅执行一些常规返回。Object 的子类可以重写此定义。Java 不保证哪个线程将调用某个给定对象的 finalize 方法,但可以保证在调用 finalize()时,调用 finalize()的线程将不会持有任何用户可见的同步锁定。如果 finalize()方法抛出未捕获的异常,那么该异常将被忽略,并且该对象的终结操作将终止。
在启用某个对象的 finalize() 方法后,将不会执行进一步操作,直到 Java 虚拟机再次确定尚未终止的任何线程无法再通过任何方法访问此对象,其中包括由准备终止的其他对象或类执行的可能操作,在执行该操作时,对象可能被丢弃。
对于任何给定对象,Java 虚拟机最多只调用一次 finalize() 方法。