ホームページ >Java >&#&チュートリアル >Javaのオブジェクトメソッドとは何ですか
Java のオブジェクト メソッドには次のものがあります: 1. [getClass()] はパブリック メソッドです; 2. [hashCode()] はオブジェクトを通じて直接呼び出すことができるパブリック メソッドです; 3. [equals() ] 現在のオブジェクトとターゲット オブジェクトが等しいかどうかを比較するために使用されます。
関連する無料学習の推奨事項: Java 基本チュートリアル
Java のオブジェクト メソッドは次のとおりです:
1. はじめに
Object は Java のすべてのクラスの基本クラスであり、クラスの最上位にあります。クラス全体の継承構造であり、最も抽象的なクラスでもあります。誰もが毎日、toString()、equals()、hashCode()、wait()、notify()、getClass() などのメソッドを使用していますが、おそらくそれらが Object のメソッドであることを認識しておらず、見向きもしません。 Object には他にどのようなメソッドがあるのか、そしてなぜこれらのメソッドを Object に配置する必要があるのかを考えてみましょう。この記事では、各メソッドの具体的な機能、書き換えルール、および私自身の理解について説明します。
2. オブジェクト メソッドの詳細説明
オブジェクトには次が含まれます: registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、メソッドは、notifyAll ()、wait(long)、wait(long,int)、wait()、および Finalize() の合計 12 個です。この順序は、Object クラスにメソッドが定義されている順序であり、以下でもこの順序で説明します。
1.1, registerNatives()
public class Object { private static native void registerNatives(); static { registerNatives(); } }
一体どういうことですか?ははは、この方法を見たばかりで混乱してしまいました。名前からするとネイティブメソッド(ローカルメソッド、JVMで実装、最下層はC/Cで実装)を登録する方法だと思うのですが、誰に登録すればいいのでしょうか?もちろん、それは JVM に対するものであり、プログラムがネイティブ メソッドを呼び出すと、JVM はこれらの基礎となるメソッドを見つけて呼び出すことができます。
オブジェクトのネイティブ メソッド。registerNatives() を使用して JVM に登録します。 (これは JNI のカテゴリに属します。Jiulong はまだ認識していません。興味がある場合は、自分で調べてください。)
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}, };
なぜ静的メソッドを使用し、静的ブロックに配置する必要があるのか?
クラスが初期化されると、親クラスからこのクラスへのクラス変数、およびクラス初期化ブロック内のクラス変数とメソッドが、次の順序で b75c58c37951302f0f985c3101bcd2b3 メソッドに配置されることがわかっています。これにより、親クラスのクラス変数とメソッドがサブクラスの前に初期化される必要があることが保証されます。したがって、サブクラスが対応するネイティブ メソッドを呼び出すとき (hashCode の計算など)、JVM のネイティブ メソッドを呼び出せることが保証されます。
1.2. getClass()
public 最終ネイティブ クラス getClass(): これは、オブジェクトを通じて直接呼び出すことができるパブリック メソッドです。
クラス ロードの最初の段階: クラス ロードは、.class ファイルをメモリにロードし、java.lang.Class オブジェクトを生成するプロセスです。 getClass() メソッドは、現在のクラスのオブジェクトのランタイム クラスに関するすべての情報のコレクションであるこのオブジェクトを取得します。この方法は 3 つの反射方法のうちの 1 つです。
1.2.1. 3 つのリフレクション メソッド: オブジェクトの
getClass();
クラス名.class;
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 = aClass.getDeclaredMethod("privateTest", String.class); //取消java访问修饰符限制。 privateTest.setAccessible(true); privateTest.invoke(aClass.newInstance(), "private method test"); //getMethod只能获取public方法 Method say = aClass.getMethod("say", String.class); say.invoke(aClass.newInstance(), "Hello World"); } } //输出结果: //class test. //private method test //Hello World
Reflection は主に次の目的で使用されます。実行時情報を取得することで、Java のような静的言語を動的にすることができます。コードを記述するときに、子オブジェクトを親クラスの参照に割り当てることができます。実行時オブジェクトのすべての情報は、実行時のリフレクション (ポリモーフィック) を通じて取得できます。 。リフレクションについてはまだ多くの知識があるため、ここでは詳しく説明しません。
1.3. hashCode()
public Native int hashCode(); これはパブリック メソッドであるため、サブクラスでオーバーライドできます。このメソッドは、現在のオブジェクトの hashCode 値を返します。これは、整数範囲 (-2^31 ~ 2^31 - 1) の数値です。
hashCode には次の制約があります。
Java アプリケーションの実行中、同じオブジェクトに対して hashCode メソッドが複数回呼び出された場合、次の条件を満たす場合は、一貫して同じ整数を返す必要があります。オブジェクトは等しい比較で使用される情報は変更されていません;
2 つのオブジェクトの x.equals(y) メソッドが true を返す場合、2 つのオブジェクト x と y の hashCode は等しい必要があります。
2 つのオブジェクトの x.equals(y) メソッドが false を返す場合、2 つのオブジェクト x と y の hashCode は等しいか、異なる可能性があります。ただし、等しくないオブジェクトに対して異なる整数の結果を生成すると、ハッシュ テーブルのパフォーマンスを向上させることができます。
デフォルトの hashCode はメモリアドレスから変換されたハッシュ値ですが、書き換え後はカスタマイズされた計算方法となり、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が使用されるのでしょうか?私は結果 * 31 = (結果
1.4, equals()
public booleanquals(Object obj); は、現在のオブジェクトとターゲット オブジェクトが等しいかどうかを比較するために使用されます。デフォルトでは、参照がポイントしているかどうかを比較します。同じオブジェクトです。これはパブリック メソッドであり、サブクラスによってオーバーライドできます。
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方法的几条约定:
自反性:即x.equals(x)返回true,x不为null;
对称性:即x.equals(y)与y.equals(x)的结果相同,x与y不为null;
传递性:即x.equals(y)结果为true, y.equals(z)结果为true,则x.equals(z)结果也必须为true;
一致性:即x.equals(y)返回true或false,在未更改equals方法使用的参数条件下,多次调用返回的结果也必须一致。x与y不为null。
如果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
从以上输出结果验证了我们的重写规定是正确的。
注意: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 { private int age; private String name; //省略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.println(cloneTest.getAge()==clone.getAge()); System.out.println(cloneTest.getName()==clone.getName()); } } //输出结果 //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.println(cloneTest.getAge() == clone.getAge()); System.out.println(cloneTest.getName() == clone.getName()); Person clonePerson = clone.getPerson(); System.out.println(person == clonePerson); System.out.println(person.getName() == clonePerson.getName()); } } //输出结果 //false //true //true //false //false
可以看到,即使成员变量是引用类型,我们也实现了深拷贝。如果成员变量是引用类型,想实现深拷贝,则成员变量也要实现Cloneable接口,重写clone方法。
1.5、toString()
public String toString();这是一个public方法,子类可重写,建议所有子类都重写toString方法,默认的toString方法,只是将当前类的全限定性类名+@+十六进制的hashCode值。
public class Object{ public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } }
我们思考一下为什么需要toString方法?
我这么理解的,返回当前对象的字符串表示,可以将其打印方便查看对象的信息,方便记录日志信息提供调试。
我们可以选择需要表示的重要信息重写到toString方法中。为什么Object的toString方法只记录类名跟内存地址呢?因为Object没有其他信息了,哈哈哈。
1.6、wait()/ wait(long)/ waite(long,int)
这三个方法是用来线程间通信用的,作用是阻塞当前线程,等待其他线程调用notify()/notifyAll()方法将其唤醒。这些方法都是public final的,不可被重写。
注意:
此方法只能在当前线程获取到对象的锁监视器之后才能调用,否则会抛出IllegalMonitorStateException异常。
调用wait方法,线程会将锁监视器进行释放;而Thread.sleep,Thread.yield()并不会释放锁。
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的,不可被重写。
public final native void notify(); 随机唤醒之前在当前对象上调用wait方法的一个线程
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); balance -= drawAmount; //标识账户已没钱 flag = false; //唤醒其他线程 notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void deposit(double depositAmount) { try { if (flag) { //如果flag为true,表明账户已经存入钱,取钱方法阻塞 wait(); } else { //存钱操作 System.out.println(Thread.currentThread().getName() + " 存钱" + depositAmount); balance += depositAmount; //标识账户已存入钱 flag = true; //唤醒其他线程 notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } //取钱者 public class DrawThread extends Thread { private Account account; private double drawAmount; public DrawThread(String name, Account account, double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } @Override public void run() { //循环6次取钱 for (int i = 0; i < 6; i++) { account.draw(drawAmount); } } } //存钱者 public class DepositThread extends Thread { private Account account; private double depositAmount; public DepositThread(String name, Account account, double depositAmount) { super(name); this.account = account; this.depositAmount = depositAmount; } @Override public void run() { //循环6次存钱操作 for (int i = 0; i < 6; i++) { account.deposit(depositAmount); } } } //测试 public class DrawTest { public static void main(String[] args) { Account brady = new Account("9龙", 0); new DrawThread("女票", brady, 10).start(); new DepositThread("公司", brady, 10).start(); } } //输出结果 //公司 存钱10.0 //女票 取钱10.0 //公司 存钱10.0 //女票 取钱10.0 //公司 存钱10.0 //女票 取钱10.0
例子中我们通过一个boolean变量来判断账户是否有钱,当取钱线程来判断如果账户没钱,就会调用wait方法将此线程进行阻塞;这时候存钱线程判断到账户没钱, 就会将钱存入账户,并且调用notify()方法通知被阻塞的线程,并更改标志;取钱线程收到通知后,再次获取到cpu的调度就可以进行取钱。反复更改标志,通过调用wait与notify()进行线程间通信。实际中我们会时候生产者消费者队列会更简单。
注意:调用notify()后,阻塞线程被唤醒,可以参与锁的竞争,但可能调用notify()方法的线程还要继续做其他事,锁并未释放,所以我们看到的结果是,无论notify()是在方法一开始调用,还是最后调用,阻塞线程都要等待当前线程结束才能开始。
为什么wait()/notify()方法要放到Object中呢?
因为每个对象都可以成为锁监视器对象,所以放到Object中,可以直接使用。
1.8、finalize()
protected void finalize() throws Throwable ;
此方法是在垃圾回收之前,JVM会调用此方法来清理资源。此方法可能会将对象重新置为可达状态,导致JVM无法进行垃圾回收。
我们知道java相对于C++很大的优势是程序员不用手动管理内存,内存由jvm管理;如果我们的引用对象在堆中没有引用指向他们时,当内存不足时,JVM会自动将这些对象进行回收释放内存,这就是我们常说的垃圾回收。但垃圾回收没有讲述的这么简单。
finalize()方法具有如下4个特点:
永远不要主动调用某个对象的finalize()方法,该方法由垃圾回收机制自己调用;
finalize()何时被调用,是否被调用具有不确定性;
当JVM执行可恢复对象的finalize()可能会将此对象重新变为可达状态;
当JVM执行finalize()方法时出现异常,垃圾回收机制不会报告异常,程序继续执行。
public class FinalizeTest { private static FinalizeTest ft = null; public void info(){ System.out.println("测试资源清理得finalize方法"); } public static void main(String[] args) { //创建FinalizeTest对象立即进入可恢复状态 new FinalizeTest(); //通知系统进行垃圾回收 System.gc(); //强制回收机制调用可恢复对象的finalize()方法 // Runtime.getRuntime().runFinalization(); System.runFinalization(); ft.info(); } @Override public void finalize(){ //让ft引用到试图回收的可恢复对象,即可恢复对象重新变成可达 ft = this; throw new RuntimeException("出异常了,你管不管啊"); } } //输出结果 //测试资源清理得finalize方法
我们看到,finalize()方法将可恢复对象置为了可达对象,并且在finalize中抛出异常,都没有任何信息,被忽略了。
1.8.1、对象在内存中的状态
对象在内存中存在三种状态:
可达状态:有引用指向,这种对象为可达状态;
可恢复状态:失去引用,这种对象称为可恢复状态;垃圾回收机制开始回收时,回调用可恢复状态对象的finalize()方法(如果此方法让此对象重新获得引用,就会变为可达状态,否则,会变为不可大状态)。
不可达状态:彻底失去引用,这种状态称为不可达状态,如果垃圾回收机制这时开始回收,就会将这种状态的对象回收掉。
1.8.2、垃圾回收机制
垃圾回收机制只负责回收堆内存种的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源);
程序无法精确控制垃圾回收的运行,垃圾回收只会在合适的时候进行。当对象为不可达状态时,系统会在合适的时候回收它的内存。
在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能会将对象置为可达状态,导致垃圾回收机制取消回收。
1.8.3、强制垃圾回收
上面我们已经说了,当对象失去引用时,会变为可恢复状态,但垃圾回收机制什么时候运行,什么时候调用finalize方法无法知道。虽然垃圾回收机制无法精准控制,但java还是提供了方法可以建议JVM进行垃圾回收,至于是否回收,这取决于虚拟机。但似乎可以看到一些效果。
public class GcTest { public static void main(String[] args){ for(int i=0;i<4;i++){ //没有引用指向这些对象,所以为可恢复状态 new GcTest(); //强制JVM进行垃圾回收(这只是建议JVM) System.gc(); //Runtime.getRuntime().gc(); } } @Override public void finalize(){ System.out.println("系统正在清理GcTest资源。。。。"); } } //输出结果 //系统正在清理GcTest资源。。。。 //系统正在清理GcTest资源。。。。
System.gc(),Runtime.getRuntime().gc()两个方法作用一样的,都是建议JVM垃圾回收,但不一定回收,多运行几次,结果可能都不一致。
以上がJavaのオブジェクトメソッドとは何ですかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。