万类之父——Object
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.
package com.coderbuff.customequals; /**
* Studen类
* Created by Kevin on 2018/2/10.
*/
public class Student {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 性别
*/
private byte sex; public Student() {
} public Student(String name, int age, byte sex) {
this.name = name;
this.age = age;
this.sex = sex;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} public byte getSex() {
return sex;
} public void setSex(byte sex) {
this.sex = sex;
} @Override
public boolean equals(Object obj) {
if (!(obj instanceof Student)) {
return false;
}
Student other = (Student)obj;
if (other.getName().equals(this.name) && other.getAge() == this.age && other.getSex() == this.sex) {
return true;
}
return false;
}
}
测试代码:
package com.coderbuff.customequals; import org.junit.Before;
import org.junit.Test; import static org.junit.Assert.assertEquals; /**
* 测试Student类equals方法
* Created by Kevin on 2018/2/10.
*/
public class StudentTest {
private Student a, b, c; @Before
public void setUp() {
a = new Student("Kevin", 23, (byte)0);
b = new Student("Kevin", 23, (byte)0);
c = new Student("Kevin", 23, (byte)0);
} /**
* 自反性
*/
@Test
public void testReflexive() {
assertEquals(true, a.equals(a));
} /**
* 对称性
*/
@Test
public void testSymmetric() {
assertEquals(true, a.equals(b));
assertEquals(true, b.equals(a));
} /**
* 传递性
*/
@Test
public void testTransitive() {
assertEquals(true, a.equals(b));
assertEquals(true, b.equals(c));
assertEquals(true, a.equals(c));
} /**
* 一致性
*/
@Test
public void testConsistent() {
for (int i = 0; i < 100; i++) {
assertEquals(true, a.equals(b));
}
} /**
* 非空性
*/
@Test
public void testNonNullity() {
assertEquals(false, a.equals(null));
}
}
从上面重写的equals的测试结果来看是通过的,但是实际上是错误的,如果在你的程序中只使用这个类的equals方法,而不会使用到集合,那没问题,但是一旦使用Map集合,上面的错误立马暴露。例如如果运行以下测试方法,返回的结果将会是null。
e.g.
/**
* 测试equals方法
*/
@Test
public void testMap() {
Map<Student, String> map = new HashMap<>();
map.put(a, "this is map.");
assertEquals("this is map.", map.get(b));
}
但明明逻辑中a和b是相等的,b也应该能取出值来,这就是没有重写hashCode方法a和b对象的hashCode值不一致导致的问题,这不是bug,这是没有满足JDK的规定。上面的hashCode方法末尾提到了equals相等,hashCode值也相等;hashCode值相等,equals不一定相等。上面的代码3个对象的hashCode值是不相等的,所以导致b不能从Map中取出相应的值,相等的对象必须具有相等的hashCode值。
这就涉及到如何设计一个良好运作的散列函数。一个好的散列函数,更能较为平均地散列到散列通中,而不是造成大量的散列冲突,大量的散列冲突会使得散列表退化成链表形式,这会使得效率大大降低。设计上要设计一个好的散列函数并不是一件容易的事,下面为Student类设计的散列函数是根据《Effective Java》中的解决办法。
@Override
public int hashCode() {
int result = 17;
result = name.hashCode() + result;
result = 31 * result + age;
result = 31 * result + (int)sex;
return result;
}
测试方法:
/**
* 测试hashCode值是否相等
*/
@Test
public void testHashCode() {
assertEquals(true, a.hashCode() == b.hashCode());
}
protected native Object clone() throws CloneNotSupportedException
“克隆”,也称为“复制”。这个方法在访问权限不同于其他方法,它在Object类中是protected修饰的方法。protected意味着只能在它的子类调用Object类中的clone方法,而不能直接在外部调用,想要使用对象的clone方法,需要在方法中调用父类的clone方法,并且需要实现Cloneable接口。
这个方法如其名,复制一个相同的对象示例,而不是将引用拷贝给它,所以复制后的对象实例一个新的对象示例。
e.g.
//还是上面Student类,重写clone方法,并且实现Cloneable接口
@Override
protected Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
测试方法:
/**
* 测试clone方法
* @throws CloneNotSupportedException
*/
@Test
public void testClone() throws CloneNotSupportedException {
Student cloneA = (Student) a.clone();
assertEquals(false, cloneA == a);
assertEquals(true, cloneA.equals(a));
}
这个方法呢,坑比较多。它有一个比较重要的地方——“深复制”和“浅复制”。
一个复杂类的成员属性有“基本数据类型”和“引用数据类型”。
假设现在需要对对象A复制一个对象B。
对于浅复制来讲,对象B的基本数据类型和A的基本数据类型它们相等,且互相不会受到影响。但是如果修改了对象B中的引用数据类型,此时将会影响到A对应的引用数据类型。
但对于深复制来讲,对象B就是完完全全和A一样的对象实例,不管是基本的还是引用的数据类型都不会相互影响。
如果像上面的示例代码重写clone方法,它所实现的就是浅复制(当然在Student类中并没有引用类型),如果在Student类中有一个Course引用类型的话,想要它实现深复制需要完成以下2点:
1. Course本身也已经实现Cloneable接口,且重写了clone方法。
2. Student类中在调用了父类的clone方法后,还需要调用Course的clone方法。
如下所示:
@Override
protected Student clone() throws CloneNotSupportedException {
Student s = (Student) super.clone();
s.course = (Course) this.course.clone();
return s;
}
重写实现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.
package com.coderbuff.communication; import java.util.Queue; /**
* Producer
* Created by Kevin on 2018/2/13.
*/
public class Producer implements Runnable{
Queue<String> queue; public Producer(Queue<String> queue) {
this.queue = queue;
} @Override
public void run() {
synchronized (queue) {
try {
while (queue.size() == 10) {
System.out.println("生产线程" + Thread.currentThread().getId() + "执行,队列为满,生产者等待");
queue.wait();
}
queue.add(String.valueOf(System.currentTimeMillis()));
System.out.println("生产线程" + Thread.currentThread().getId() + "执行,队列不为满,生产者生产:" + String.valueOf(System.currentTimeMillis()) + ",容量" + queue.size());
queue.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.coderbuff.communication; import java.util.Queue; /**
* Consumer
* Created by Kevin on 2018/2/13.
*/
public class Consumer implements Runnable{
Queue<String> queue; public Consumer(Queue<String> queue) {
this.queue = queue;
} @Override
public void run() {
synchronized (queue) {
try {
while (queue.isEmpty()) {
System.out.println("消费线程" + Thread.currentThread().getId() + "执行,队列为空,消费者等待");
queue.wait();
}
System.out.println("消费线程" + Thread.currentThread().getId() + "执行,队列不为空,消费者消费:" + queue.remove() + ",容量" + queue.size());
queue.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试方法:
package com.coderbuff.communication; import org.junit.Before;
import org.junit.Test; import java.util.LinkedList;
import java.util.Queue; /**
* Test Producer & Consumer
* Created by Kevin on 2018/2/13.
*/
public class ProducerConsumerTest {
Queue<String> queue; @Before
public void setUp() {
queue = new LinkedList<>();
} @Test
public void test() {
new Thread(new Consumer(queue)).start();
new Thread(new Consumer(queue)).start();
new Thread(new Producer(queue)).start();
new Thread(new Producer(queue)).start();
}
}
这个生产者消费者模型很好的演示了线程间是如何通过Object中的wait和notify、notifyAll方法进行通信的。在程序中使用的是notifyAll方法而不是notify方法,实际当中也多用notify方法。它们俩的区别就是notify方法只会唤醒等待队列中的一个线程使之进入同步队列进而使之有了争夺CPU执行的权力,而notifyAll方法是会唤醒等待队列中的所有线程使之进入同步队列。注意,它们都是让等待线程从等待队列进入同步队列,它们仅仅是拥有了争夺CPU的权力,调用这两个方法不代表它们就会拥有CPU执行的权力。
至于wait方法另外个重载方法:
wait(long):等待N毫秒没用收到通知就超时返回;
wait(long, int):同样也是超时等待指定的时间没有收到通知超时返回,不同的是第二个参数可以达到更加细粒度的时间控制——纳秒。
protected void finalize() throws Throwable { }
这个方法在对象在被GC前会被调用,需要着重强调的是,千万不要依赖此方法在其中做一些资源的关闭,因为我们不能保证JVM何时进行GC,所以我们也就无法判断该方法何时会被执行,除非你不在意它执行的时间,否则千万不要重写它。它不能当做是C++中的析构函数。
这是一个能给程序员加buff的公众号
万类之父——Object的更多相关文章
- Java类的根Object
一.Object类介绍 Object全名java.lang.Object,java.lang包在使用的时候无需显示导入,编译时由编译器自动导入.Object类是类层次结构的根,Java中所有的类从根本 ...
- 13_Python的面向对象编程-类class,对象object,实例instance
1.面向对象概述 1.类是用来描述对象的工具,把拥有相同属性和行为的对象分为一组 2.对象是由类实例化出来的一个具体的对象 属性: 对象拥有的名词,用变量表示 ...
- 把一个类(或者Object)转换成字典
直接上代码:把一个类转换成object,然后在转换成字典 internal static IDictionary<string, string> GetDictionary(this ob ...
- python定义类()中写object和不写的区别
这里需要说明一下: python3中,类定义默认继承object,所以写不写没有区别 但在python2中,并不是这样 所以此内容是针对python2的,当然python3默认继承,不代表我们就傻乎乎 ...
- Python中新式类 经典类的区别(即类是否继承object)
首先什么是新式类 经典类呢: #新式类是指继承object的类 class A(obect): ........... #经典类是指没有继承object的类 class A: ........... ...
- Java类的设计----Object 类
Object类 Object类是所有Java类的根父类如果在类的声明中未使用extends关键字指明其父类,则默认父类为Object类 public class Person { ... } 等价于: ...
- Object类 任何类都是object类的子类 用object对象接收数组 object类的向上向下转型
任何类都是object类的子类 用object对象接收数组 object类的向上向下转型
- 最终父类【根类】:Object类&Objects类
一.java.lang.Object类 1.Object类介绍 Object类是所有类的父类.一个类都会直接或间接继承自该类: 该类中提供了一些非常常用的方法! 2.toString()方法 A: ...
- 深入理解ClassLoader(四)—类的父委托加载机制
上几次我们介绍到了JVM内部的几个类加载器,我们来重新画一下这个图,再来看一下他们之间的关系.
随机推荐
- js代码性能优化的几个方法
相信写代码对于大部分人都不难,但想写出高性能的代码就需要一定的技术积累啦,下面是一些优化JavaScript代码性能的常见方法. 一.注意作用域 1.避免全局查找 使用全局变量和函数肯定要比局部的开销 ...
- 利用JAVA API远程进行HDFS的相关操作
学习HDFS有一段时间了,现在把自己总结的HDFS的相关操作代码展示给大家. 主要有HDFS的增删改查,文件的追加,windows本地文件的上传,hdfs文件的下载,文件重命名,创建目录,文件是否存在 ...
- Lucene学习笔记1(V7.1)
Lucene是一个搜索类库,solr.nutch和elasticsearch都是基于Lucene.个人感觉学习高级搜索引擎应用程序之前 有必要了解Lucene. 开发环境:idea maven spr ...
- 【开发技术】web.xml vs struts.xml
web.xml用来配置servlet,监听器(Listener),过滤器(filter),还有404错误跳转页面,500,等还配置欢迎页面等,总之一句话,就是系统总配置方案写在web.xml中 str ...
- Linux - ubuntu中vi不能正常使用方向键与退格键的问题
一度怀疑是键盘坏了! 之前安装solaris也是这个问题! 重新安装vim就可以了! $sudo apt-get remove vim-common $sudo apt-get install vim
- php等比例压缩图片
<?php function resizeImage($im,$maxwidth,$maxheight,$name,$filetype) { $pic_width = imagesx($im); ...
- UWP: 实现 UWP 应用自启动
在上一篇文章中,我们实现了使用命令行来启动 UWP 应用,在这一篇文章中,我们会实现 UWP 应用自启用的实现,也即开机后或用户登陆后,应用自己启动.这些特性原来都是 Win32 程序所具备的,UWP ...
- python 程序退出方式
sys.exit() 执行该语句会直接退出程序,这也是经常使用的方法,也不需要考虑平台等因素的影响,一般是退出Python程序的首选方法. 该方法中包含一个参数status,默认为0,表示正常退出,也 ...
- mybatis一级缓存二级缓存
一级缓存 Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言.所以在参数和SQL完全一样的情况下,我们使用同一个SqlSess ...
- junit4X系列--Rule
原文出处:http://www.blogjava.net/DLevin/archive/2012/05/12/377955.html.感谢作者的无私分享. 初次用文字的方式记录读源码的过程,不知道怎么 ...