目录
Object类简介
Object类位于java.lang包下,是所有类的父类,java中任何类的基类都是Object,包括我们新建的类,就算继承的其他自定义的类,但它依旧是Object的子孙类。
public class Demo1 extends Demo{
public static void main(String[] args) {
System.out.println(Demo1.class instanceof Object);
System.out.println(String.class instanceof Object);
}
}
Object类中的方法
-
void registerNatives()
private static native void registerNatives();
这是一个本地方法,从方法名翻译过来的含义是注册本地,jdk中很多的超类都使用到了这个方法,首先我们解释一个什么是native方法,native方法不是由java语言编写,而是由其他语言,比如C语言编写实现,并不存在于我们的jdk源码内,而是其他语言通过操作系统去实现,jdk代码里只需要去调用。java代码里如果要使用native方法需要有两个步骤,第一个步骤:通过System.loadLibrary()方法调用,第二个步骤:虚拟机在加载的动态文件中定位并链接到具体的本地方法,然后才可以执行,具体更深层次的资料读者可以自行了解。而Object类里面的registerNatives方法的作用就是取代第二个步骤,让程序主动将本地方法链接到调用方,不需要虚拟机再去定位并链接,提高执行效率。
这个方法是放在静态代码块里的,代表这个类初始化的时候就会执行这个方法。
static {
registerNatives();
}
-
Class getClass()
public final native Class<?> getClass();
获取当前类的Class对象,Class对象是指当前类的Class,大家应该知道java里面的对象都是通过关键字new实例化的一个对象,而每个对象都会对应一个Class,也就是Class对象,每一个Class类可以声明无数个对象,而这些对象的Class对象都是同一个。Class对象包含了这类的所有信息和属性,当我们new一个新对象或者引用静态成员变量时,JVM的类加载器子系统会将其对应的Class对象加载到JVM当中,然后JVM就可以根据Class对象提供的信息和属性创建对应的实例对象或者提供静态变量的引用值。
public static void main(String[] args) {
System.out.println(new Demo().getClass());
System.out.println(new Demo().getClass());
System.out.println(new Demo().getClass());
}
不管new多少次,同一个类都是同一个Class对象。
-
int hashCode()
public native int hashCode();
获取当前对象的hashCode编码,hash码是根据hash算法(一些系列可自定义的算法)得到的编码值,所有得到的编码值储存在一张表中,就是hash表,所以每个对象的hashCode编码在这张hash表中都有一个位置,这个位置储存的可能是对象的物理地址或者其他信息(这里作者没有深入了解,有兴趣的可以自行研究),然后hashCode的作用主要用于集合,可以用来判断重复元素,不同对象的hashCode可能相同,但是如果hashCode不同,那对象一定不同,所以通过比较hashCode可以快速判断集合中是否存在相同元素。
public static void main(String[] args) throws CloneNotSupportedException {
ObjectA tom = new ObjectA("tom", 30);
ObjectA clone = (ObjectA) tom.clone();
System.out.println(tom.hashCode());
System.out.println(clone.hashCode());
}
就算对象的属性值一样,但是默认的hashCode算法计算出的hash值依旧会不同,所以我们在使用中常常会重写hashCode方法,便于对象属性值的比较(equals方法调用)。
-
boolean equals(Object)
public boolean equals(Object obj) {
return (this == obj);
}
用于比较两个对象是否相等,方法实现就是直接用等于符号进行判断,其实就是简单的比较两个对象的内存地址是否一致。
public static void main(String[] args) throws CloneNotSupportedException {
ObjectA tom = new ObjectA("tom", 30);
ObjectA jerry = new ObjectA("tom", 30);
System.out.println(tom);
System.out.println(jerry);
System.out.println(tom.equals(jerry));
}
输出@后面的6ed3ef1和2437c6dc就是对象储存的地址,不同对象储存的物理地址当然不一样,所以就算对象设置的属性值一样,调用equals方法得到的结果是false。
不过我们在实际项目中,比较两个对象是否相等的逻辑常常就是判断两个对象的属性值是否相等,所以这里我们会重写equals方法,在重写equals方法之前我们看见方法上的注释有这么一句话。
解释就是说,因为规定约定相等的对象必须具有相等的hashCode,所以如果业务逻辑中,需要根据属性值判断两个对象是否相等的话,也需要根据属性值去重写hashCode方法。
只重写equals方法,不重写hashCode
public class ObjectA {
private String name;
private int age;
public ObjectA(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ObjectA objectA = (ObjectA) o;
return age == objectA.age &&
Objects.equals(name, objectA.name);
}
}
public static void main(String[] args) {
ObjectA tom1 = new ObjectA("tom", 30);
ObjectA tom2 = new ObjectA("tom", 30);
System.out.println(tom1.equals(tom2));
}
输出结果是true,只重写equals方法并设置相同属性值之后,比较两个对象可以得到相等的结果,这看似没有什么问题,接下来我们把这两个对象再放在集合中进行重复判断。
public static void main(String[] args) {
ObjectA tom1 = new ObjectA("tom", 30);
ObjectA tom2 = new ObjectA("tom", 30);
Set<ObjectA> set = new HashSet<>();
set.add(tom1);
set.add(tom2);
System.out.println(set.size());
List<ObjectA> list = Arrays.asList(tom1, tom2);
System.out.println(list.stream().distinct().count());
}
无论是添加到set集合,还是通过stream流的方式去重,得到了结果证明了tom1和tom2是两个不同的对象,这和我们之前调用equals方法进行比较的结果不符,因为java中集合判断重复对象元素时会先比较对象的hashCode,如果hashCode相等再调用对象的equals方法,如果结果是true,就认定为相同元素,所以如果hashCode不相等,那么就算对象的属性值是相等的,对于集合来说依旧是两个不同的元素。
拿不重复set集合举例子,set集合的内部其实维护了一个map,而我们添加到set集合的元素实际上就是map的键,而当我们调用set的add方法时,实际上就是调用map的put方法。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在map调用真正的put方法之前,会先对key值进行hash计算,得到hash值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后就是真正的put逻辑,代码里面的tab是一个node节点数组,除了存放key和value之外,还存放了key对应的hash值,第一个红框的判断就是把当前存入key的hash值通过位运算放在tab里面去看能不能找到对应的数据,如果没有得到对应的数据,那么直接加入新节点,如果找到了数据,再与新加入的数据比较hash值,然后再调用对象的equals方法比较是否相等。
综上我们可以得出,对于类似set这种不可重复的集合来说,或者作者举例写的list集合通过stream流这样类似对集合去重的操作,判断重复元素必须元素对象的hashCode相等,并且equals结果为true,所以日常开发中,时常需要重写equals和hashCode方法,并且都是一起重写。
-
Object clone()
protected native Object clone() throws CloneNotSupportedException;
Object的clone方法,名义克隆,native标明了是一个本地方法,非java实现,java只负责调用即可,clone方法用的修饰符是protected,只能被内部或者子类访问,所以如果我们自定义的类需要使用clone方法必须对该方法进行重写,实现逻辑直接调用父类的clone方法即可。
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
这里的clone方法是一个浅克隆,当克隆对象里只有基本属性时没什么影响,但是当对象属性里有负责对象时,就会出现克隆问题,下面分别举例说明。
例一,当克隆的对象里只有基本属性
public class ObjectA implements Cloneable{
private String name;
private int age;
public ObjectA(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "ObjectA{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
private static void test1() {
ObjectA objectA = new ObjectA("bingo", 24);
Object clone = null;
try {
clone = objectA.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println("objectA-->" + objectA);
System.out.println("clone-->" + clone);
objectA.setName("bingo_query");
objectA.setAge(25);
System.out.println("------------修改ObjectA的属性值------------");
System.out.println("objectA-->" + objectA);
System.out.println("clone-->" + clone);
}
可以看到,当克隆完成之后,克隆对象获得了和objectA一样的属性,并且当objectA的属性值被修改之后,克隆对象的属性值不会受影响。
接下来,第二个例子,我们在属性中加入一个对象属性
public class ObjectProperties {
private int high;
private String sex;
public ObjectProperties(int high, String sex) {
this.high = high;
this.sex = sex;
}
public void setHigh(int high) {
this.high = high;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "ObjectProperties{" +
"high=" + high +
", sex='" + sex + '\'' +
'}';
}
}
public class ObjectA implements Cloneable {
private String name;
private int age;
private ObjectProperties objectProperties;
public ObjectA(String name, int age, ObjectProperties objectProperties) {
this.name = name;
this.age = age;
this.objectProperties = objectProperties;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setObjectProperties(ObjectProperties objectProperties) {
this.objectProperties = objectProperties;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "ObjectA{" +
"name='" + name + '\'' +
", age=" + age +
", objectProperties=" + objectProperties +
'}';
}
}
private static void test1() {
ObjectProperties objectProperties = new ObjectProperties(175, "man");
ObjectA objectA = new ObjectA("bingo", 24, objectProperties);
Object clone = null;
try {
clone = objectA.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println("objectA-->" + objectA);
System.out.println("clone-->" + clone);
objectA.setName("bingo_query");
objectA.setAge(25);
objectProperties.setHigh(165);
objectProperties.setSex("woman");
System.out.println("------------修改ObjectA的属性值------------");
System.out.println("objectA-->" + objectA);
System.out.println("clone-->" + clone);
}
从结果可以看到,我们的clone对象的objectProperties属性在克隆的时候high的值是175,sex的值是man,然后我们更改了原对象objectA的objectProperties属性,按常理来说,我们的克隆对象在克隆完成之后应该是单独的,不会受原对象属性的影响,但是实际情况是克隆对象的objectProperties属性也发生了变化,high变成了165,sex变成了woman,这当然不是我们所想要的,要解决这一问题,我们还需要在对象属性的类(ObjectProperties)中重写clone方法,并且在ObjectA类的clone方法中手动调用ObjectProperties的克隆方法。
public class ObjectProperties implements Cloneable{
private int high;
private String sex;
public ObjectProperties(int high, String sex) {
this.high = high;
this.sex = sex;
}
public void setHigh(int high) {
this.high = high;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "ObjectProperties{" +
"high=" + high +
", sex='" + sex + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class ObjectA implements Cloneable {
private String name;
private int age;
private ObjectProperties objectProperties;
public ObjectA(String name, int age, ObjectProperties objectProperties) {
this.name = name;
this.age = age;
this.objectProperties = objectProperties;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setObjectProperties(ObjectProperties objectProperties) {
this.objectProperties = objectProperties;
}
@Override
protected Object clone() throws CloneNotSupportedException {
ObjectA objectA = null;
try {
objectA = (ObjectA) super.clone();
} catch (Exception e) {
e.printStackTrace();
}
objectA.objectProperties = (ObjectProperties) objectProperties.clone();
return objectA;
}
@Override
public String toString() {
return "ObjectA{" +
"name='" + name + '\'' +
", age=" + age +
", objectProperties=" + objectProperties +
'}';
}
}
再来看下结果,clone对象的objectProperties属性也不再受原对象属性的影响了。
-
String toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
先来翻译一下方法上的注释
原有的方法实现是返回对象的类名+@+16进制的对象hashCode编码,不过我们日常开发中并不会使用到这些信息,时常的操作是重写toString方法,转换为返回对象的属性信息和属性值,并且在调用System.out.println输出对象的时候,会调用对象的toString方法,所以可以直接打印我们对象里的属性。
@Override
public String toString() {
return "ObjectProperties{" +
"high=" + high +
", sex='" + sex + '\'' +
'}';
}
public static void main(String[] args) {
ObjectProperties objectProperties = new ObjectProperties(175, "man");
System.out.println(objectProperties);
}
System.out.println的println方法调用了String.valueOf,把对象转为字符串。
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
String.valueOf调用了对象的toString方法
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
-
void notify()
public final native void notify();
notify方法和下面的nofityAll方法以及wait所有的重载方法属于对象的等待唤醒消息机制,因为是Object类的方法,所以说所有对象都配备了这一机制,这是一个本地方法,我们不用关心实现原理。第一步,先来翻译一下jdk的注释。
大概可以明白,此方法的作用是唤醒在此对象的监视器上等待的单个线程,那么就需要和对象的等待方法(wait)一起使用,并且是先需要对象处于wait状态,然后调用nofity才有意义,并且在方法的最后抛出了一个IllegalMonitorStateException不合法监控状态异常,当前线程状态不是次对象监视器的所有者,意思是调用方法的线程必须单独占有此对象,那么解决办法就是使用线程独占锁(synchronized),java里面还有一个Lock锁,但是如果用Lock锁依然会报错,原因简单来说因为Lock锁只能锁住代码块,而不是锁住整个对象,不符合注释里面写的要求(如果作者这里说法有误,希望读者多多指教)。下面写一个例子简单演示一下notify方法的使用。
演示说明:服务员到厨房等待厨师做菜,厨师做菜完成之后通知服务员上菜
首先需要选择一个类作为监视器的对象调用wait和nofity方法,我选择建一个餐盘类来作为监视器监视的对象
public class Dish {
//不需要任何代码,只需要使用Object的notify方法和wait方法
}
然后新建服务员类和厨师类,并且实现各自需要负责的方法
public class Waiter {
public void doServer(Dish dish) {
//使用synchronized锁住传入的dish对象,表示该线程独占此对象
synchronized (dish) {
try {
System.out.println("服务员:到厨房等待厨师做菜");
//等待厨师做菜
dish.wait();
System.out.println("服务员:收到厨师做菜完成通知,拿到餐盘,上菜完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Cook {
public void doCook(Dish dish) {
//使用synchronized锁住传入的dish对象,表示该线程独占此对象
synchronized (dish) {
try {
//上菜
System.out.println("厨师:开始做菜...");
TimeUnit.SECONDS.sleep(1);
//上菜完毕,通知服务员端菜
dish.notify();
System.out.println("厨师:做菜完成...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
写个demo进行测试
public static void main(String[] args) {
Dish dish = new Dish();
Waiter waiter = new Waiter();
Cook cook = new Cook();
new Thread(() -> {
waiter.doServer(dish);
}).start();
new Thread(() -> {
cook.doCook(dish);
}).start();
}
从结果看出,服务员先到厨房,然后调用dish的wait方法等待厨师做菜,厨师做菜完成之后又调用dish的nofity方法通知服务员上菜,按预期实现了Object类的等待唤醒机制。
-
void nofityAll
public final native void notifyAll();
对比上面nofity方法只能唤醒一个等待的线程,nofityAll会唤醒所有等待的线程。
我们先来演示一下和nofity方法的区别,在上面代码的基础上复制三个服务员去等待一个厨师做菜,但是做菜的厨师只调用nofity,唤醒某一个服务员
先改造一个服务员类,让他输出一下当前线程的名称
public class Waiter {
public void doServer(Dish dish) {
//使用synchronized锁住传入的dish对象,表示该线程独占此对象
synchronized (dish) {
try {
System.out.println( Thread.currentThread().getName() + ":到厨房等待厨师做菜");
//等待厨师做菜
dish.wait();
System.out.println(Thread.currentThread().getName() + ":收到厨师做菜完成通知,拿到餐盘,上菜完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
然后在demo中新建三个服务员一起去等待厨师做菜,不需要修改厨师类的代码。
public static void main(String[] args) {
Dish dish = new Dish();
Waiter waiterA = new Waiter();
Waiter waiterB = new Waiter();
Waiter waiterC = new Waiter();
Cook cook = new Cook();
new Thread(() -> {
waiterA.doServer(dish);
},"服务员A").start();
new Thread(() -> {
waiterB.doServer(dish);
},"服务员B").start();
new Thread(() -> {
waiterC.doServer(dish);
},"服务员C").start();
new Thread(() -> {
cook.doCook(dish);
}).start();
}
可以看到,打印结束了,但是程序并没有结束,只有服务员A完成了上菜,因为厨师的类调用的是nofity,只唤醒了服务员A这个线程,而服务员B和服务员C这两个线程任然一直在等待唤醒。接着我们再来改造一个厨师类,让厨师调用nofityAll方法。
public class Cook {
public void doCook(Dish dish) {
//使用synchronized锁住传入的dish对象,表示该线程独占此对象
synchronized (dish) {
try {
//上菜
System.out.println("厨师:开始做菜...");
TimeUnit.SECONDS.sleep(1);
//上菜完毕,通知所有服务员端菜
dish.notifyAll();
System.out.println("厨师:做菜完成...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
从结果可以看出,厨师通过调用dish的notifyAll方法,唤醒了所有等待的服务员,程序执行结束,所以由此我们可以看出notify和nofityAll方法的区别,前者只会随机唤醒一个等待的线程,而后者可以唤醒所有。
-
void wait()
public final void wait() throws InterruptedException {
wait(0);
}
jdk的注释已经解释清楚了,调用wait方法让当前线程等待,直到其他线程调用nofity方法或者nofityAll方法,然后继续执行后面代码,注意这里肯定必须要是同一个对象的wait和nofity才有用, 简单例子请看上面的nofity和nofityAll示例。
-
void wait(long)
public final native void wait(long timeout) throws InterruptedException;
wait的重载方法,jdk的注释有点长,简单来说就是在wait的基础上增加了等待时间的参数,且是以毫秒为单位,所以相比wait方法,除了调用notify和nofityAll方法之外,如果超过等待时间,也会结束等待,尝试去获取锁,执行后面的代码。注意是尝试去获取锁,而不是直接执行代码,因为wait方法是在同步代码块执行的(参照上面的例子,synchronized锁住了dish对象),调用wait方法让出了dish对象的占用权,所以这里超时之后需要重新去竞争。
-
void wait(long,int)
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
第二个wait的重载方法,在上一个等待超时参数上增加了一个纳米单位,意义就是更准确的去控制超时时间。
待完善
-
finalize()
结尾
作者是本着巩固学习和交流的目的编写,如果有问题或者不足之处请多多指导,感谢!
参考文章: