【Java基础之Object类(一)】Java中Object类中的所有方法(toString、equals、hashCode、clone、finalize、wait和notify等)详解(转载)

时间:2023-12-20 21:35:50

java中的hashcode、equals和toString方法都是基类Object的方法。

首先说说toString方法,简单的总结了下API说明就是:返回该对象的字符串表示,信息应该是简明但易于读懂的信息表达式,一般来说大部分的常见类都会重写这个方法。比如Collection会重写的该方法就会在集合前后加上[ ]中间的元素中间会加上。但是如果没有重写的话Object的默认的toString方法是返回:getClass().getName() + '@' + Integer.toHexString(hashCode()) 即这个类的实例的类名+“@”+hashcode的16进制表示。这里就引进了hashcode的概念了。

hashcode方法是返回哈希码的一个方法,这里哈希码可以认为是地址的概念,用处是在某些时候加快哈希表的性能。因此在涉及到哈希表的操作的时候就可能设计到hashcode的调用。

equals方法是比较两个类,默认的效果是和==一样的,即比较引用(地址)看是不是同一个元素,但是因为大部分情况下比较两个元素是不是同一个元素是没有意义的所以会重写这个方法。比如:String会重写为判断两个字符串是不是相同,而自定义的复杂类更要自己定义比较的规则。

而在集合的hashset的时候,因为set中不会存储相同的元素,而是否相同也是你判断的。为了方便,内部的规则是先判断hashcode,如果hashcode不同的话就直接存入。如果hashcode相同的再判断equals是不是true。

所以如果我们对equals方法进行了重写,建议一定要对hashCode方法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。

我们针对例子分析。

 1 package TestCollection;
2
3 import java.util.Collection;
4 import java.util.HashSet;
5 import java.util.LinkedList;
6 import java.util.TreeSet;
7
8 import pack.SOP;
9
10 public class TestCollection {
11
12 /**
13 * @param args
14 */
15 public static void main(String[] args) {
16 // TODO Auto-generated method stub
17 Collection c = new HashSet();
18 c.add(new Integer(100));
19 c.add("abcdefg");
20 c.add("abcdefg");
21 c.add(new Cat("米米",3));
22 c.add(new Cat("米米",3));
23 SOP.sop(c.size());
24 SOP.sop("==================");
25 SOP.sop(c);
26
27 }
28
29 }
30
31 class Cat {
32 String name;
33 int age;
34 Cat(){
35
36 }
37 Cat(String name,int age){
38 this.name = name;
39 this.age =age;
40 }
41 public boolean equals(Object obj){
42 if(obj instanceof Cat){
43 Cat c = (Cat)obj;
44 SOP.sop("==================");
45 return false;
46 // return (this.name.equals(c.name))&&(this.age==c.age) ;
47 }
48 return super.equals(obj);
49
50 }
51 public int hashCode() {
52 //return name.hashCode();
53 return 0x68888;
54 }
55 /*public String toString() {
56 //return "是这样吗";
57 return "name :"+name+"age :"+age;
58 }*/
59 }

向hashset中添加元素,hashset有个规则是不存储相同的元素,而相同与否是由各自的规则判断的,比如插入字符串的时候,String的equals规则是只要指向的内容相同就true。所以两个相同的“abcdefg”就只有一个。

接下来就是插入自定义的类了。

首先需要自定义toString方法让print的时候按照自己的需求显示。
重写equals方法和hashcode方法进行是不是相同元素的判断。

然后做如下的测试证明原理的正确性:

首先不写hashcode方法,则即使equals判断了内容规则了,但是set中会存在相同显示的两个元素,这是因为虚拟机会先判断hashCode方法,因为没有写hashCode方法的话会继承父类的hashCode方法,所以两个对象的hashcode值不一样,所以这里是不会判断equals方法就直接认定两个元素是不一样的,即使你“看上去是一样的”。

然后重写了hashcode方法,如果定义了固定了int值,则Object的默认的toString会把你定义的hashcode值显示出来。

然后如果有了相同的hashcode的话就接着判断equals方法,如果自己定义让返回的值始终是false的话,也会两个都显示出来,因为虚拟机会认为他们是两个元素。只有hashcode和equals都一样的话,才会认为是两个相同的元素。插入的时候,只插入一个。

最后得出结论:虚拟机判断两个元素是不是相同首先判断两个元素的hashcode,如果hashcode不同,则直接认为不同;如果hashcode相同,再判断equals方法是不是相同,如果equals不同,则两个元素不同,如果equals相同,则才会最终认为元素相同。因为很多系统的类实现了这些方法,因此我们在自定义的时候可以拿来调用系统类的这些方法。

第6个方法:

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

该方法为被public修饰,所有对象都可见。

该方法返回对象的字符串表示形式,开发人员能够清晰的看到一个对象的各种属性。在官方建议上,建议所有的方法都重写这个方法。Eclipse有代码生成器,可以方便的重写该方

法。

看方法体的内容,可以知道,最终返回getClass().getName() + ' @' + Integer.toHexString(hashCode())。下面用一个例子看一下:

举例:

  1. package edu.java.test;
  2. public class TestToString {
  3. public static void main(String[] args) {
  4. TestClone tc = new TestClone();
  5. tc.setName("my name is java");
  6. tc.setAge("70 yeas");
  7. System.out.println("未重写之前的toString方法:"+tc.getClass().getName()+"@"+Integer.toHexString(tc.hashCode()));
  8. System.out.println("重写之后的toString方法:"+tc.toString());
  9. }
  10. }

上面示例了两个打印语句,一个是原生态的toString方法,一个是重写后的toString方法。下面是结果:

  1. 未重写之前的toString方法:edu.java.test.TestClone@1e57e8f
  2. 重写之后的toString方法:TestClone [name=my name is java, age=70 yeas]

可以看出,原生态的toString方法,不能很好的显示出对象的属性,所以在toString源码里,java开发人员建议每个类都应该重写toString方法的原因。下面是重写后的toString方法。

  1. @Override
  2. public String toString() {
  3. return "TestClone [name=" + name + ", age=" + age + "]";
  4. }

第7个方法:wait()

  1. public final void wait() throws InterruptedException {
  2. wait(0);
  3. }

该方法会导致当前线程等待,直到另一个线程调用notify()或者notifyAll()方法或者是设定的等待时间过去。

当前线程必须拥有此对象监视器。如果线程等待,有以下四种方式可以解除等待。

第一种方式:其他线程调用notify()方法,然后该线程刚好被唤醒。

第二种方式:其他线程中断。

第三种方式:其他线程调用notifyAll()方法。

第四种方式:超时时间为0。

一旦发生上面四件事情,就会把该线程从等待设置中删除。

第8个方法:notify()

  1. public final native void notify();

notify本意在中文中是通知的意思。该方法由final和native修饰,表明该方法是依赖于本地,不被java实现,而且不能被子类重写。

这个方法主要是唤醒一个等待中的线程。而且,这个唤醒后的线程和其他正常活跃的线程一样。

在这里要提一下wait方法,wait方法是使当前线程等待,而notify则是唤醒,而且当前运行的线程必须具有该对象的对象监视器,监视器的获得有三种方式:

第一种方式:通过执行该对象的同步的方法。在此大致介绍一下同步,同步就相当于给一个对象上了一把锁,在一段时间内,这个对象只能被一个线程访问,其他任何线程都不能进入。比如说,我现在进入了我家,然后把门锁了,这个时候其他任何没有钥匙的人都进不来。屋里面的东西只能自己动。

synchronized(object)。

第二种方式:在同步代码块中执行。

synchronized void method(){}。

第三种方式:执行一个Class类型的同步静态方法。

而这三种方式都必须要求当前线程拥有对象的监视权。

第9个方法:notifyAll();

唤醒所有线程,可能是按照线程等待时间倒叙唤醒。

以上3个方法举例:

  1. package edu.java.test;
  2. import java.text.DateFormat;
  3. import java.text.SimpleDateFormat;
  4. import java.util.Calendar;
  5. public class TestNotify01 {
  6. private Object monitor = new Object();
  7. DateFormat format = new SimpleDateFormat("yyyy-MM-dd:hh:mm:ss");
  8. private String getTime(){
  9. return format.format(Calendar.getInstance().getTime());
  10. }
  11. /**
  12. * 0.首先获得对象的监视器
  13. * 1.打印等待前后线程的名称和等待时间
  14. * 2.线程开启
  15. * @param thread
  16. * @param ms
  17. */
  18. public void waitOnce(String thread, final Long ms) {
  19. Thread waitThread = new Thread() {
  20. public void run() {
  21. // 对对象上锁,获得对象的监视器,用的第二种方式,对同步代码块进行上锁
  22. synchronized (monitor) {
  23. try {
  24. System.out.println("Thread等待之前 " + Thread.currentThread().getName() + " Wait at" + getTime());
  25. monitor.wait(ms);
  26. System.out.println("Thread等待之后 " + Thread.currentThread().getName() + " Wait at" + getTime());
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }
  32. };
  33. waitThread.setName(thread);
  34. waitThread.start();
  35. }
  36. /**
  37. * 0.获得对象的监视器
  38. * 1.唤醒一个线程
  39. * 2.睡眠两次
  40. * 3.开启线程
  41. * @param thread
  42. * @param ms
  43. */
  44. public void awake(String thread, final Long ms) {
  45. Thread notifyThread = new Thread() {
  46. public void run() {
  47. // 对对象上锁,获得对象的监视器,用的第一种方式
  48. synchronized (monitor) {
  49. monitor.notify();
  50. System.out.println("Thread" + Thread.currentThread().getName() + " 唤醒 at " + getTime());
  51. try {
  52. Thread.sleep(ms);
  53. } catch (InterruptedException e) {
  54. e.printStackTrace();
  55. }
  56. }
  57. try {
  58. Thread.sleep(ms);
  59. } catch (InterruptedException e) {
  60. e.printStackTrace();
  61. }
  62. };
  63. };
  64. notifyThread.setName(thread);
  65. notifyThread.start();
  66. }
  67. /**
  68. * 唤醒全部线程
  69. * @param thread
  70. */
  71. public void awakeAll(String thread) {
  72. Thread notifyThread = new Thread() {
  73. public void run() {
  74. // 对对象上锁,获得对象的监视器,用的第一种方式
  75. synchronized (monitor) {
  76. monitor.notifyAll();
  77. System.out.println("Thread" + Thread.currentThread().getName() + " 唤醒 at " + getTime());
  78. }
  79. };
  80. };
  81. notifyThread.setName(thread);
  82. notifyThread.start();
  83. }
  84. public static void main(String[] args) {
  85. /**
  86. * 0.首先建立了一个对象
  87. * 1.然后开启三个等待中的线程
  88. * 2.睡眠两秒后唤醒一个线程,从线程的名称中可以看出,唤醒线程的顺序可能是按照线程开启时间来的.
  89. */
  90. TestNotify01 test = new TestNotify01();
  91. test.waitOnce("1",Long.MAX_VALUE);
  92. test.waitOnce("2",Long.MAX_VALUE);
  93. test.waitOnce("3",Long.MAX_VALUE);
  94. try {
  95. Thread.sleep(2000);
  96. } catch (InterruptedException e) {
  97. e.printStackTrace();
  98. }
  99. test.awake("100",2000L);
  100. //test.awakeAll("100");
  101. }
  102. }

这个测试主要是先等待,然后再唤醒,可以从结果直观的感受一下:

  1. Thread等待之前 1 Wait at2016-07-15:12:28:19
  2. Thread等待之前 2 Wait at2016-07-15:12:28:19
  3. Thread等待之前 3 Wait at2016-07-15:12:28:19
  4. Thread100 唤醒 at 2016-07-15:12:28:21
  5. Thread等待之后 1 Wait at2016-07-15:12:28:23

先等待3个线程,然后2S后唤醒,按照顺序唤醒。接下来我们看一下如果是唤醒全部,会是什么结果:

  1. Thread等待之前 1 Wait at2016-07-15:12:29:35
  2. Thread等待之前 2 Wait at2016-07-15:12:29:35
  3. Thread等待之前 3 Wait at2016-07-15:12:29:35
  4. Thread100 唤醒 at 2016-07-15:12:29:37
  5. Thread等待之后 3 Wait at2016-07-15:12:29:37
  6. Thread等待之后 2 Wait at2016-07-15:12:29:37
  7. Thread等待之后 1 Wait at2016-07-15:12:29:37

可以看出,唤醒是倒叙唤醒的。

第10个方法:

  1. public final native void wait(long timeout) throws InterruptedException;

这个方法就是设置时间唤醒,一段时间过后唤醒,上面的wait()方法,其实就是这个方法里的参数设置为0,也就是理解唤醒。

第11个方法:

  1. public final void wait(long timeout, int nanos) throws InterruptedException {
  2. if (timeout < 0) {
  3. throw new IllegalArgumentException("timeout value is negative");
  4. }
  5. if (nanos < 0 || nanos > 999999) {
  6. throw new IllegalArgumentException(
  7. "nanosecond timeout value out of range");
  8. }
  9. if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
  10. timeout++;
  11. }
  12. wait(timeout);
  13. }

这个方法和wait()方法很类似,不过它能够更精确的控制时间,并且抛出对应的异常。

如果超时时间<0,就会抛出超时时间为负的异常,因为时间必须>=0。

如果时间最小单位超过999999,就会抛出纳秒时间超出范围异常。

第12个方法:

  1. protected void finalize() throws Throwable { }

Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前被自动调用的。
垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize()的方法,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作(如关闭流等操作)。但JVM(Java虚拟机)不保证此方法总被调用。