欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

万类之父——Object

程序员文章站 2022-08-13 11:29:28
jdk1.8.0_144 Object类作为Java中的*类,位于java.lang包中。所有的类直接或者间接都继承自它。所以Object类中的方法在所有类中都可以直接调用。在深入介绍它的API时,先插一句它和泛型之间的关系。 在JDK1.5之前是没有泛型的,集合能够装下任意的类型,这就导致了一个 ......

jdk1.8.0_144

  Object类作为Java中的*类,位于java.lang包中。所有的类直接或者间接都继承自它。所以Object类中的方法在所有类中都可以直接调用。在深入介绍它的API时,先插一句它和泛型之间的关系。

  在JDK1.5之前是没有泛型的,集合能够装下任意的类型,这就导致了一个潜在的问题——不能在编译时做类型检查,也就可能导致程序bug出现的概率。JDK1.5出现了泛型,在定义一个集合时指定一个泛型,这就能在编译时能做类型检查,避免了一些低级bug的出现。时至今日,实际上在JDK源码中遗留了部分不是特别优美的代码,从今天的角度来看是能够将其泛型化的(例如Map的get方法),但在当时需要考虑向后兼容不得不放弃对某些方法和类的泛型化,才导致了一丝瑕疵。

  接下来将详细的剖析Object类中的一些方法,其中某些方法会延伸到其他方面(例如:wait和notify等)。

 

public final native Class<?> getClass()

  返回Class对象实例。Class类有点“特殊”,因为它在我们的日常代码逻辑中不常出现,它所出现的地方往往是一些基础框架或者基础工具。  

  Class类所处的包同样是java.lang,毫无疑问它的父类还是Object。在学习面向对象编程时,我们知道类是对一个事物抽象的定义,对象实例是表示的是一个具体的事物。那么Class这个名字有点含糊的类抽象的是什么呢?它的实例有代表的是什么呢?

  在程序中定义一个People类,我们将男人、女人抽象为了人类——People,它的实例表示的是男人或女人。程序中类似People这样的类千千万万,Class类就是千千万万类和接口的抽象,Class类的对象实例就这千千万万中具体的某个类或接口。再继续,男人和女人能被抽象为People类,这是因为男人和女人都有很多相同的特征,那千千万万类和接口都有名字、方法等也就意味着它们也能被抽象,故Class类就千千万万类和接口的抽象,Class类的对象实例就这千千万万中具体的某个类或接口。

  Class类作为类和接口的抽象,它存在的意义在哪里呢?它是类和接口的抽象,它的实例是某个具体的类,那为何不直接通过People p = new People()来实例化一个People对象呢?而是麻烦的需要先获取Class类,再获取它的实例,再通过它的实例创造一个类的对象。

  通常情况下使用Class类来获取某个类在实际编码中确实不常见,但这是JVM的执行机制。每个类被创建编译过后都对应一个.class文件,这个.class文件包含了它对应的Class对象,这个类的Class对象会被载入内存,它就会被JVM来创建这个类的所有实例对象。当然在实际运用中,Java的反射机制是离不开Class类的。 所以,回到Object类的getClass方法,提供的是该类的Class对象,每个类都可以通过这个方法获取它对应的Class对象。

 

public native int hashCode()

  这个方法是一个native本地方法,它的具体实现是有C++实现的。在Java程序中,每个对象实例(注意是对象实例)都有一个唯一的hashCode值(哈希码值),可以通过对比两个对象实例是否相同来判断是否指向同一个对象实例。

  它有这么一个性质,例如判断两个String字符串是否相等,使用“==”表示的两个对象的引用是否相等,而使用equals则表示两个对象的值是否相等。equals相等,则hashCode值一定相等;hashCode值相等,而equals不一定相等。并且它被应用在我们熟悉的Map集合中。

 

public boolean equals(Object obj)

  该方法用于比较两个对象是否“相等”。之所以相等有引号,是这个相等在代码逻辑中分为两种情况:对象引用相等;对象值相等。

  Object中equals方法有一个默认实现,它直接使用“==”进行比较,也就是说在Object中equals和“==”是等价的。但是在String和Integer等中, equals方法是被重写的,它们的equals方法代表的是值相等,而不是引用相等。

  注意在重写equals方法时,需要遵守以下几个原则:

  1. 自反性。也就是说自己调用equals方法和自己比较时,必须返回true。(自己都不和自己相等,那谁才相等)

  2. 对称性。我和你比较返回ture,你和我比较也要返回true,a.equals(b)返回true,b.equals(a)返回true。

  3. 传递性。这个根据名字就很好理解。a.equals(b)返回true,b.equals(c)返回true,a.equals(c)也需要返回true。

  4. 一致性。也就是在没有修改两个对象的情况下,多次调用返回的结果应该是一样的。

  5. 非空性。非空对象与null值比较必须返回false。

e.g.

万类之父——Object
 1 package com.coderbuff.customequals;
 2 
 3 /**
 4  * Studen类
 5  * Created by Kevin on 2018/2/10.
 6  */
 7 public class Student {
 8     /**
 9      * 姓名
10      */
11     private String name;
12     /**
13      * 年龄
14      */
15     private int age;
16     /**
17      * 性别
18      */
19     private byte sex;
20 
21     public Student() {
22     }
23 
24     public Student(String name, int age, byte sex) {
25         this.name = name;
26         this.age = age;
27         this.sex = sex;
28     }
29 
30     public String getName() {
31         return name;
32     }
33 
34     public void setName(String name) {
35         this.name = name;
36     }
37 
38     public int getAge() {
39         return age;
40     }
41 
42     public void setAge(int age) {
43         this.age = age;
44     }
45 
46     public byte getSex() {
47         return sex;
48     }
49 
50     public void setSex(byte sex) {
51         this.sex = sex;
52     }
53 
54     @Override
55     public boolean equals(Object obj) {
56         if (!(obj instanceof Student)) {
57             return false;
58         }
59         Student other = (Student)obj;
60         if (other.getName().equals(this.name) && other.getAge() == this.age && other.getSex() == this.sex) {
61             return true;
62         }
63         return false;
64     }
65 }
View Code

测试代码:

万类之父——Object
 1 package com.coderbuff.customequals;
 2 
 3 import org.junit.Before;
 4 import org.junit.Test;
 5 
 6 import static org.junit.Assert.assertEquals;
 7 
 8 /**
 9  * 测试Student类equals方法
10  * Created by Kevin on 2018/2/10.
11  */
12 public class StudentTest {
13     private Student a, b, c;
14 
15     @Before
16     public void setUp() {
17         a = new Student("Kevin", 23, (byte)0);
18         b = new Student("Kevin", 23, (byte)0);
19         c = new Student("Kevin", 23, (byte)0);
20     }
21 
22     /**
23      * 自反性
24      */
25     @Test
26     public void testReflexive() {
27         assertEquals(true, a.equals(a));
28     }
29 
30     /**
31      * 对称性
32      */
33     @Test
34     public void testSymmetric() {
35         assertEquals(true, a.equals(b));
36         assertEquals(true, b.equals(a));
37     }
38 
39     /**
40      * 传递性
41      */
42     @Test
43     public void testTransitive() {
44         assertEquals(true, a.equals(b));
45         assertEquals(true, b.equals(c));
46         assertEquals(true, a.equals(c));
47     }
48 
49     /**
50      * 一致性
51      */
52     @Test
53     public void testConsistent() {
54         for (int i = 0; i < 100; i++) {
55             assertEquals(true, a.equals(b));
56         }
57     }
58 
59     /**
60      * 非空性
61      */
62     @Test
63     public void testNonNullity() {
64         assertEquals(false, a.equals(null));
65     }
66 }
View Code

  从上面重写的equals的测试结果来看是通过的,但是实际上是错误的,如果在你的程序中只使用这个类的equals方法,而不会使用到集合,那没问题,但是一旦使用Map集合,上面的错误立马暴露。例如如果运行以下测试方法,返回的结果将会是null。

e.g.

万类之父——Object
1 /**
2  * 测试equals方法
3  */
4 @Test
5 public void testMap() {
6     Map<Student, String> map = new HashMap<>();
7     map.put(a, "this is map.");
8     assertEquals("this is map.", map.get(b));
9 }
View Code

  但明明逻辑中a和b是相等的,b也应该能取出值来,这就是没有重写hashCode方法a和b对象的hashCode值不一致导致的问题,这不是bug,这是没有满足JDK的规定。上面的hashCode方法末尾提到了equals相等,hashCode值也相等;hashCode值相等,equals不一定相等。上面的代码3个对象的hashCode值是不相等的,所以导致b不能从Map中取出相应的值,相等的对象必须具有相等的hashCode值。

  这就涉及到如何设计一个良好运作的散列函数。一个好的散列函数,更能较为平均地散列到散列通中,而不是造成大量的散列冲突,大量的散列冲突会使得散列表退化成链表形式,这会使得效率大大降低。设计上要设计一个好的散列函数并不是一件容易的事,下面为Student类设计的散列函数是根据《Effective Java》中的解决办法。

1 @Override
2 public int hashCode() {
3     int result = 17;    
4     result = name.hashCode() + result;
5     result = 31 * result + age;
6     result = 31 * result + (int)sex;
7     return result;
8 }

测试方法:

万类之父——Object
1 /**
2  * 测试hashCode值是否相等
3  */
4 @Test
5 public void testHashCode() {
6     assertEquals(true, a.hashCode() == b.hashCode());
7 }
View Code

 

protected native Object clone() throws CloneNotSupportedException

  “克隆”,也称为“复制”。这个方法在访问权限不同于其他方法,它在Object类中是protected修饰的方法。protected意味着只能在它的子类调用Object类中的clone方法,而不能直接在外部调用,想要使用对象的clone方法,需要在方法中调用父类的clone方法,并且需要实现Cloneable接口。

  这个方法如其名,复制一个相同的对象示例,而不是将引用拷贝给它,所以复制后的对象实例一个新的对象示例。

e.g.

万类之父——Object
1 //还是上面Student类,重写clone方法,并且实现Cloneable接口
2 @Override
3 protected Student clone() throws CloneNotSupportedException {
4     return (Student) super.clone();
5 }
View Code

测试方法:

万类之父——Object
 1 /**
 2  * 测试clone方法
 3  * @throws CloneNotSupportedException
 4  */
 5 @Test
 6 public void testClone() throws CloneNotSupportedException {
 7     Student cloneA = (Student) a.clone();
 8     assertEquals(false, cloneA == a);
 9     assertEquals(true, cloneA.equals(a));
10 }
View Code

  这个方法呢,坑比较多。它有一个比较重要的地方——“深复制”和“浅复制”。

  一个复杂类的成员属性有“基本数据类型”和“引用数据类型”。

  假设现在需要对对象A复制一个对象B。

  对于浅复制来讲,对象B的基本数据类型和A的基本数据类型它们相等,且互相不会受到影响。但是如果修改了对象B中的引用数据类型,此时将会影响到A对应的引用数据类型。

  但对于深复制来讲,对象B就是完完全全和A一样的对象实例,不管是基本的还是引用的数据类型都不会相互影响。

  如果像上面的示例代码重写clone方法,它所实现的就是浅复制(当然在Student类中并没有引用类型),如果在Student类中有一个Course引用类型的话,想要它实现深复制需要完成以下2点:

  1. Course本身也已经实现Cloneable接口,且重写了clone方法。

  2. Student类中在调用了父类的clone方法后,还需要调用Course的clone方法。

  如下所示:

1 @Override
2 protected Student clone() throws CloneNotSupportedException {
3     Student s = (Student) super.clone();
4     s.course = (Course) this.course.clone();
5     return s;
6 }

  重写实现clone方法时一定要仔细,切记需要调用父类的clone方法。

 

public String toString()

  返回类的一些描述信息。“最佳的编程实践”是最好对每个类都重写toString方法。在Object类中这个方法的实现是调用getClass返回类信息+@符号+16进制的hashCode值。

 

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

  这几个方法拿到一块来说是因为它们用于多线程并发编程当中。

  上面的5个方法实际上只有前3个核心方法,后两个只是wait方法的重载而已。我们先了解前3个,后两个也会迎刃而解。

  开头提到这用于多线程并发编程中,众所周知Java应用程序号称“一次编译,到处运行”的奥秘就在于Java应用程序是运行在Java虚拟机(JVM)之上的,而JVM的设计实际上是类同于一个操作系统的。在操作系统中谈的更多是进程与进程之间的关系,例如进程间的同步与通信,进程和进程的并发等等。Java应用程序在操作系统中只是1个进程,在JVM中就蕴含了N个线程,就类似于操作系统的N个进程,所以在Java中提及更多的是线程间的同步与通信,线程和线程的并发等。

  Java中的线程用于自己的运行空间,称之为虚拟机栈,这块空间是线程所独占的。如果Java应用程序中的N个线程相互孤立互不干扰的运行,可以说这个应用程序并没有多大的价值,最大的价值是N个线程之间相互配合完成工作。那自然就会涉及到多个线程间的通信问题。在本文只着重讲解线程间的通信,而对于线程安全这个议题不做过多深究。

  在操作系统中,对于进程间同步有这么一个定义:为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。将定义中的进程换为线程,即可当做在Java中对同步的定义。

  例如:线程T1运行到某处时,需要线程T2完成另一项任务才能继续运行下去,此时T1对CPU的占用就需要让位给T2,而T1此时只能等待T2完成。当T2完成任务后通知T1重新获取对CPU的占用继续完成未完成的任务。这个例子就是简单的同步示例,其中涉及到的等待、通知即表示线程间的通信,wait和notify、notifyAll所代表的就是线程间的通信。

  所以,Object类中的wait和notify、notifyAll方法是用于线程间的通信,且它们的调用需要在获取对象锁的情况下才可以(也就是说需要在线程安全的条件下调用),在具体一点是只有在synchronized关键字所修饰的同步方法或者同步代码块才可以使用,并不是任何地方都可以调用。这里有一个有关线程间通信的经典示例——生产者消费者模型。通过仔细咀嚼这个模型我们能好的理解线程间的通信。

e.g.

万类之父——Object
 1 package com.coderbuff.communication;
 2 
 3 import java.util.Queue;
 4 
 5 /**
 6  * Producer
 7  * Created by Kevin on 2018/2/13.
 8  */
 9 public class Producer implements Runnable{
10     Queue<String> queue;
11 
12     public Producer(Queue<String> queue) {
13         this.queue = queue;
14     }
15 
16     @Override
17     public void run() {
18         synchronized (queue) {
19             try {
20                 while (queue.size() == 10) {
21                     System.out.println("生产线程" + Thread.currentThread().getId() + "执行,队列为满,生产者等待");
22                     queue.wait();
23                 }
24                 queue.add(String.valueOf(System.currentTimeMillis()));
25                 System.out.println("生产线程" + Thread.currentThread().getId() + "执行,队列不为满,生产者生产:" + String.valueOf(System.currentTimeMillis()) + ",容量" + queue.size());
26                 queue.notifyAll();
27             } catch (InterruptedException e) {
28                 e.printStackTrace();
29             }
30         }
31     }
32 }
View Code
万类之父——Object
 1 package com.coderbuff.communication;
 2 
 3 import java.util.Queue;
 4 
 5 /**
 6  * Consumer
 7  * Created by Kevin on 2018/2/13.
 8  */
 9 public class Consumer implements Runnable{
10     Queue<String> queue;
11 
12     public Consumer(Queue<String> queue) {
13         this.queue = queue;
14     }
15 
16     @Override
17     public void run() {
18         synchronized (queue) {
19             try {
20                 while (queue.isEmpty()) {
21                     System.out.println("消费线程" + Thread.currentThread().getId() + "执行,队列为空,消费者等待");
22                     queue.wait();
23                 }
24                 System.out.println("消费线程" + Thread.currentThread().getId() + "执行,队列不为空,消费者消费:" + queue.remove() + ",容量" + queue.size());
25                 queue.notifyAll();
26             } catch (InterruptedException e) {
27                 e.printStackTrace();
28             }
29         }
30     }
31 }
View Code

测试方法:

万类之父——Object
 1 package com.coderbuff.communication;
 2 
 3 import org.junit.Before;
 4 import org.junit.Test;
 5 
 6 import java.util.LinkedList;
 7 import java.util.Queue;
 8 
 9 /**
10  * Test Producer & Consumer
11  * Created by Kevin on 2018/2/13.
12  */
13 public class ProducerConsumerTest {
14     Queue<String> queue;
15 
16     @Before
17     public void setUp() {
18         queue = new LinkedList<>();
19     }
20 
21     @Test
22     public void test() {
23         new Thread(new Consumer(queue)).start();
24         new Thread(new Consumer(queue)).start();
25         new Thread(new Producer(queue)).start();
26         new Thread(new Producer(queue)).start();
27     }
28 }
View Code

  这个生产者消费者模型很好的演示了线程间是如何通过Object中的wait和notify、notifyAll方法进行通信的。在程序中使用的是notifyAll方法而不是notify方法,实际当中也多用notify方法。它们俩的区别就是notify方法只会唤醒等待队列中的一个线程使之进入同步队列进而使之有了争夺CPU执行的权力,而notify方法是会唤醒等待队列中的所有线程使之进入同步队列。注意,它们都是让等待线程从等待队列进入同步队列,它们仅仅是拥有了争夺CPU的权力,调用这两个方法不代表它们就会拥有CPU执行的权力。

  至于wait方法另外个重载方法:

  wait(long):等待N毫秒没用收到通知就超时返回;

  wait(long, int):同样也是超时等待指定的时间没有收到通知超时返回,不同的是第二个参数可以达到更加细粒度的时间控制——纳秒。

 

protected void finalize() throws Throwable { }

  这个方法在对象在被GC前会被调用,需要着重强调的是,千万不要依赖此方法在其中做一些资源的关闭,因为我们不能保证JVM何时进行GC,所以我们也就无法判断该方法何时会被执行,除非你不在意它执行的时间,否则千万不要重写它。它不能当做是C++中的析构函数。

 

 

这是一个能给程序员加buff的公众号 

万类之父——Object