Les méthodes d'objet en Java incluent : 1. [getClass()] est une méthode publique ; 2. [hashCode()] est une méthode publique et peut être appelée directement via l'objet ; ] Utilisé pour comparer si l'objet actuel et l'objet cible sont égaux.
Recommandations d'apprentissage gratuites associées : Tutoriel de base Java
Les méthodes objet en Java sont :
1 Introduction
L'objet est la classe de base de toutes les classes Java et est la classe supérieure de la classe entière. structure d'héritage. C'est aussi la classe la plus abstraite. Tout le monde utilise toString(), equals(), hashCode(), wait(), notify(), getClass() et d'autres méthodes tous les jours. Peut-être qu'ils ne réalisent pas qu'il s'agit de méthodes d'Object et ne regardent pas non plus quoi. d'autres méthodes dont dispose Object. Et réfléchissez à la raison pour laquelle ces méthodes devraient être placées dans Object. Cet article traite des fonctions spécifiques de chaque méthode, des règles de réécriture et de certaines de mes propres connaissances.
2. Explication détaillée des méthodes Object
L'objet contient : registerNatives(), getClass(), hashCode(), equals(), clone(), toString(), notify(), notifyAll (), wait(long), wait(long,int), wait() et finalize(), un total de douze méthodes. Cet ordre est répertorié dans l'ordre dans lequel les méthodes sont définies dans la classe Object. Je les expliquerai également dans cet ordre ci-dessous.
1.1, registerNatives()
public class Object { private static native void registerNatives(); static { registerNatives(); } }
C'est quoi ce bordel ? Hahaha, je viens de voir cette méthode et j'étais confus. D'après le nom, nous comprenons que cette méthode consiste à enregistrer une méthode native (une méthode native, implémentée par la JVM, et la couche inférieure est implémentée en C/C++). Bien entendu, il s'agit de la JVM. Lorsqu'un programme appelle une méthode native, la JVM peut trouver ces méthodes sous-jacentes et les appeler.
méthode native dans Object et utilisez registerNatives() pour vous inscrire auprès de la JVM. (Cela appartient à la catégorie JNI. Jiulong ne le sait pas encore. Si vous êtes intéressé, vous pouvez le vérifier par vous-même.)
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}, };
Pourquoi devrions-nous utiliser des méthodes statiques et les mettre en statique des blocs ?
Nous savons que lorsqu'une classe est initialisée, les variables de classe de la classe parent vers cette classe et les variables et méthodes de classe dans le bloc d'initialisation de classe seront placées dans la méthode < définition, cela peut garantir que les variables de classe et les méthodes de la classe parent doivent être initialisées avant la sous-classe. Par conséquent, lorsqu'une sous-classe appelle la méthode native correspondante, comme le calcul du hashCode, elle est garantie de pouvoir appeler la méthode native de la JVM.
1.2. getClass()
public final native Class getClass() : Il s'agit d'une méthode publique que nous pouvons appeler directement via l'objet.
La première étape du chargement de classe est le processus de chargement du fichier .class en mémoire et de génération d'un objet java.lang.Class. La méthode getClass() consiste à obtenir cet objet, qui est une collection de toutes les informations sur la classe d'exécution de l'objet de la classe actuelle. Cette méthode est l'une des trois méthodes de réflexion.
1.2.1. Trois modes de réflexion :
getClass() de l'objet
class name.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
La réflexion est principalement utilisée ; Pour obtenir des informations d'exécution, vous pouvez rendre dynamique un langage statique comme Java. Vous pouvez attribuer un objet enfant à une référence de la classe parent lors de l'écriture du code. Toutes les informations de l'objet d'exécution peuvent être obtenues par réflexion au moment de l'exécution, c'est-à-dire polymorphes. refléter. Il existe encore beaucoup de connaissances sur la réflexion, je n’entrerai donc pas dans les détails ici.
1.3. hashCode()
public native int hashCode(); Il s'agit d'une méthode publique, donc les sous-classes peuvent la remplacer. Cette méthode renvoie la valeur hashCode de l'objet actuel, qui est un nombre compris dans la plage entière (-2^31 ~ 2^31 - 1).
Il existe les contraintes suivantes pour hashCode
Lors de l'exécution d'une application Java, lorsque la méthode hashCode est appelée plusieurs fois sur le même objet, le même entier doit être renvoyé de manière cohérente, à condition que l'objet est Les informations utilisées dans la comparaison égale n'ont pas été modifiées
Si la méthode x.equals(y) de deux objets renvoie vrai, le hashCode des deux objets x et y doit être égal.
Si la méthode x.equals(y) de deux objets renvoie false, le hashCode des deux objets x et y peut être égal ou différent. Cependant, générer des résultats entiers différents pour des objets inégaux peut améliorer les performances de la table de hachage.
Le hashCode par défaut est la valeur de hachage convertie à partir de l'adresse mémoire. Après réécriture, il s'agit d'une méthode de calcul personnalisée ; le hashCode d'origine peut également être renvoyé via System.identityHashCode(Object).
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; } }
Il est recommandé d'utiliser la méthode Objects.hash(Object…values). Je crois que lorsque vous regardez le code source, vous verrez que 31 est utilisé comme multiplicateur de base pour calculer hashCode. Pourquoi 31 est-il utilisé ? Je suis d'accord et je comprends le résultat * 31 = (result<<5) - résultat. La couche inférieure de la JVM peut optimiser automatiquement les opérations sur les bits, ce qui est très efficace ; et comme le hashCode calculé par 31 a moins de conflits, il est propice à la distribution des bits du bucket de hachage.
1.4, equals()
public boolean equals(Object obj); utilisé pour comparer si l'objet actuel et l'objet cible sont égaux. La valeur par défaut est de comparer si la référence pointe vers le. même objet. Il s'agit d'une méthode publique et peut être remplacée par des sous-classes.
public class Object{ public boolean equals(Object obj) { return (this == obj); } }
Pourquoi devez-vous remplacer la méthode égale ?
因为如果不重写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垃圾回收,但不一定回收,多运行几次,结果可能都不一致。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!