JAVA中Object类⽅法详解
⼀、引⾔
Object是java所有类的基类,是整个类继承结构的顶端,也是最抽象的⼀个类。⼤家天天都在使⽤toString()、equals()、hashCode()、waite()、notify()、getClass()等⽅法,或许都没有意识到是Object的⽅法,也没有去看Object还有哪些⽅法以及思考为什么这些⽅法要放到Object中。本篇就每个⽅法具体功能、重写规则以及⾃⼰的⼀些理解。
⼆、Object⽅法详解
Object中含有: registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait(long)、
wait(long,int)、wait()、finalize()共⼗⼆个⽅法。这个顺序是按照Object类中定义⽅法的顺序列举的,下⾯我也会按照这个顺序依次进⾏讲解。
1.1、registerNatives()
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
}
什么⿁?哈哈哈,我刚看到这⽅法,⼀脸懵逼。从名字上理解,这个⽅法是注册native⽅法(本地⽅法,由JVM实现,底层是C/C++实现的)向谁注册呢?当然是向JVM ,当有程序调⽤到native⽅法时,JVM才好去到这些底层的⽅法进⾏调⽤。
Object中的native⽅法,并使⽤registerNatives()向JVM进⾏注册。(这属于JNI的范畴,9龙暂不了解,有兴趣的可⾃⾏查阅。)
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
为什么要使⽤静态⽅法,还要放到静态块中呢?
我们知道了在类初始化的时候,会依次从⽗类到本类的类变量及类初始化块中的类变量及⽅法按照定义顺序放到< clinit>⽅法中,这样可以保证⽗类的类变量及⽅法的初始化⼀定先于⼦类。所以当⼦类调⽤相应native⽅法,⽐如计算hashCode时,⼀定可以保证能够调⽤到JVM 的native⽅法。
1.2、getClass()
public final native ClassgetClass():这是⼀个public的⽅法,我们可以直接通过对象调⽤。
类加载的第⼀阶段类的加载就是将.class⽂件加载到内存,并⽣成⼀个java.lang.Class对象的过程。getC
lass()⽅法就是获取这个对象,这是当前类的对象在运⾏时类的所有信息的集合。这个⽅法是反射三种⽅式之⼀。
1.2.1、反射三种⽅式:
1. 对象的getClass();
2. 类名.class;
3. Class.forName();
class extends ObjectTest {
private void privateTest(String str) {
System.out.println(str);
}
public void say(String str) {
System.out.println(str);
}
}
public class ObjectTest {
public static void main(String[] args) throws Exception {
ObjectTest = new ();
//获取对象运⾏的Class对象
Class<? extends ObjectTest> aClass = .getClass();
System.out.println(aClass);
//getDeclaredMethod这个⽅法可以获取所有的⽅法,包括私有⽅法
Method privateTest = DeclaredMethod("privateTest", String.class);
//取消java访问修饰符限制。
privateTest.setAccessible(true);
privateTest.wInstance(), "private method test");
//getMethod只能获取public⽅法
Method say = Method("say", String.class);
say.wInstance(), "Hello World");
}
}
//输出结果:
//class test.
//private method test
//Hello World
反射主要⽤来获取运⾏时的信息,可以将java这种静态语⾔动态化,可以在编写代码时将⼀个⼦对象赋值给⽗类的⼀个引⽤,在运⾏时通过反射可以或许运⾏时对象的所有信息,即多态的体现。对于反射知识还是很多的,这⾥就不展开讲了。
1.3、hashCode()
public native int hashCode();这是⼀个public的⽅法,所以⼦类可以重写它。这个⽅法返回当前对象的hashCode值,这个值是⼀个整数范围内的(-2^31 ~ 2^31 - 1)数字。
对于hashCode有以下⼏点约束
在 Java 应⽤程序执⾏期间,在对同⼀对象多次调⽤ hashCode ⽅法时,必须⼀致地返回相同的整数,前提是将对象进⾏ equals ⽐较时所⽤的信息没有被修改;
如果两个对象 x.equals(y) ⽅法返回true,则x、y这两个对象的hashCode必须相等。
如果两个对象x.equals(y) ⽅法返回false,则x、y这两个对象的hashCode可以相等也可以不等。但是,为不相等的对象⽣成不同整数结果可以提⾼哈希表的性能。
默认的hashCode是将内存地址转换为的hash值,重写过后就是⾃定义的计算⽅式;也可以通过System.identityHashCode(Object)来返回原本的hashCode。
public class HashCodeTest {
private int age;
private String name;
@Override
public int hashCode() {
Object[] a = Stream.of(age, name).toArray();
int result = 1;
for (Object element : a) {
result = 31 * result + (element == null ? 0 : element.hashCode());
}
return result;
}
}
推荐使⽤Objects.hash(Object… values)⽅法。相信看源码的时候,都看到计算hashCode都使⽤了31作为基础乘数,为什么使⽤31呢?我⽐较赞同与理解result * 31 = (result<<5) - result。JVM底层可以⾃动做优化为位运算,效率很⾼;还有因为31计算的hashCode冲突较少,利于hash桶位的分布。
1.4、equals()
public boolean equals(Object obj);⽤于⽐较当前对象与⽬标对象是否相等,默认是⽐较引⽤是否指向同⼀对象。为public⽅法,⼦类可重写。
public class Object{
public boolean equals(Object obj) {
return (this == obj);
}
}
为什么需要重写equals⽅法?
因为如果不重写equals⽅法,当将⾃定义对象放到map或者set中时;如果这时两个对象的hashCode相同,就会调⽤equals⽅法进⾏⽐较,这个时候会调⽤Object中默认的equals⽅法,⽽默认的equals⽅法只是⽐较了两个对象的引⽤是否指向了同⼀个对象,显然⼤多数时候都不会指向,这样就会将重复对象存⼊map或者set中。这就破坏了map与set不能存储重复对象的特性,会造成内存溢出。
重写equals⽅法的⼏条约定:
1. ⾃反性:即x.equals(x)返回true,x不为null;
2. 对称性:即x.equals(y)与y.equals(x)的结果相同,x与y不为null;
3. 传递性:即x.equals(y)结果为true, y.equals(z)结果为true,则x.equals(z)结果也必须为true;
4. ⼀致性:即x.equals(y)返回true或false,在未更改equals⽅法使⽤的参数条件下,多次调⽤返回的结果也必须⼀致。x与y不为null。
5. 如果x不为null, x.equals(null)返回false。
我们根据上述规则来重写equals⽅法。
public class EqualsTest{
private int age;
private String name;
//省略get、set、构造函数等
@Override
public boolean equals(Object o) {
//先判断是否为同⼀对象
if (this == o) {
return true;
}
//再判断⽬标对象是否是当前类及⼦类的实例对象
//注意:instanceof包括了判断为null的情况,如果o为null,则返回false
if (!(o instanceof )) {
return false;
}
that = () o;
return age == that.age &&
Objects.equals(name, that.name);
}
public static void main(String[] args) throws Exception {
EqualsTest1 equalsTest1 = new EqualsTest1(23, "9龙");
EqualsTest1 equalsTest12 = new EqualsTest1(23, "9龙");
EqualsTest1 equalsTest13 = new EqualsTest1(23, "9龙");
System.out.println("-----------⾃反性----------");
System.out.println(equalsTest1.equals(equalsTest1));
System.out.println("-----------对称性----------");
System.out.println(equalsTest12.equals(equalsTest1));
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println("-----------传递性----------");
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println(equalsTest12.equals(equalsTest13));
System.out.println(equalsTest1.equals(equalsTest13));
System.out.println("-----------⼀致性----------");
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println(equalsTest1.equals(equalsTest12));
System.out.println("-----⽬标对象为null情况----");
System.out.println(equalsTest1.equals(null));
}
}
//输出结果
//-----------⾃反性----------
//true
/
/-----------对称性----------
//true
//true
//-----------传递性----------
//true
//true
//true
//-----------⼀致性----------
//true
//true
//-----⽬标对象为null情况----
/
/false
View Code
从以上输出结果验证了我们的重写规定是正确的。
注意:instanceof 关键字已经帮我们做了⽬标对象为null返回false,我们就不⽤再去显⽰判断了。
建议equals及hashCode两个⽅法,需要重写时,两个都要重写,⼀般都是将⾃定义对象放⾄Set中,或者Map中的key时,需要重写这两个⽅法。
1.4、clone()
protected native Object clone() throws CloneNotSupportedException;
此⽅法返回当前对象的⼀个副本。
这是⼀个protected⽅法,提供给⼦类重写。但需要实现Cloneable接⼝,这是⼀个标记接⼝,如果没有实现,当调⽤object.clone()⽅法,会抛出CloneNotSupportedException。
public class CloneTest implements Cloneable {
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
return (CloneTest) super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.Age()==Age());
System.out.Name()==Name());
}
}
//输出结果
//false
//true
//true
从输出我们看见,clone的对象是⼀个新的对象;但原对象与clone对象的String类型的name却是同⼀个引⽤,这表明,super.clone⽅法对成员变量如果是引⽤类型,进⾏是浅拷贝。
那什么是浅拷贝?对应的深拷贝?
浅拷贝:拷贝的是引⽤。
深拷贝:新开辟内存空间,进⾏值拷贝。
那如果我们要进⾏深拷贝怎么办呢?看下⾯的例⼦。
class Person implements Cloneable{
private int age;
private String name;
//省略get、set、构造函数等
@Override
protected Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
//name通过new开辟内存空间
person.name = new String(name);
return person;
}
}
public class CloneTest implements Cloneable {
private int age;
private String name;
//增加了person成员变量
private Person person;
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
CloneTest clone = (CloneTest) super.clone();
clone.person = person.clone();
return clone;
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
Person person = new Person(22, "路飞");
cloneTest.setPerson(person);
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.Age() == Age());
equals()方法System.out.Name() == Name());
Person clonePerson = Person();
System.out.println(person == clonePerson);
System.out.Name() == Name());
}
}
//输出结果
//false
//true
//true
//false
//false
可以看到,即使成员变量是引⽤类型,我们也实现了深拷贝。如果成员变量是引⽤类型,想实现深拷贝,则成员变量也要实现Cloneable接⼝,重写clone⽅法。
1.5、toString()
public String toString();这是⼀个public⽅法,⼦类可重写,建议所有⼦类都重写toString⽅法,默认的toString⽅法,只是将当前类的全限定性类名+@+⼗六进制的hashCode值。
return getClass().getName() + "@" + HexString(hashCode());
}
}
我们思考⼀下为什么需要toString⽅法?
我这么理解的,返回当前对象的字符串表⽰,可以将其打印⽅便查看对象的信息,⽅便记录⽇志信息提供调试。
我们可以选择需要表⽰的重要信息重写到toString⽅法中。为什么Object的toString⽅法只记录类名跟内存地址呢?因为Object没有其他信息了,哈哈哈。
1.6、wait()/ wait(long)/ waite(long,int)
这三个⽅法是⽤来线程间通信⽤的,作⽤是阻塞当前线程,等待其他线程调⽤notify()/notifyAll()⽅法将其唤醒。这些⽅法都是public final 的,不可被重写。
注意:
1. 此⽅法只能在当前线程获取到对象的锁监视器之后才能调⽤,否则会抛出IllegalMonitorStateException异常。
2. 调⽤wait⽅法,线程会将锁监视器进⾏释放;⽽Thread.sleep,Thread.yield()并不会释放锁。
3. wait⽅法会⼀直阻塞,直到其他线程调⽤当前对象的notify()/notifyAll()⽅法将其唤醒;⽽wait(long)是等待给定超时时间内(单位毫
秒),如果还没有调⽤notify()/nofiyAll()会⾃动唤醒;waite(long,int)如果第⼆个参数⼤于0并且⼩于999999,则第⼀个参数+1作为超时时间;
public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
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);
}
1.7、notify()/notifyAll()
前⾯说了,如果当前线程获得了当前对象锁,调⽤wait⽅法,将锁释放并阻塞;这时另⼀个线程获取到了此对象锁,并调⽤此对象的notify()/notifyAll()⽅法将之前的线程唤醒。这些⽅法都是public final的,不可被重写。
1. public final native void notify(); 随机唤醒之前在当前对象上调⽤wait⽅法的⼀个线程
2. public final native void notifyAll(); 唤醒所有之前在当前对象上调⽤wait⽅法的线程
下⾯我们使⽤wait()、notify()展⽰线程间通信。假设9龙有⼀个账户,只要9龙⼀发⼯资,就被⼥朋友给取⾛了。
/
/账户
public class Account {
private String accountNo;
private double balance;
private boolean flag = false;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
/
**
* 取钱⽅法
*
* @param drawAmount 取款⾦额
*/
public synchronized void draw(double drawAmount) {
try {
if (!flag) {
//如果flag为false,表明账户还没有存⼊钱,取钱⽅法阻塞
wait();
} else {
/
/执⾏取钱操作
System.out.println(Thread.currentThread().getName() + " 取钱" + drawAmount);
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论