一、Object 类介绍
在Java中,Object
类是所有类的根类。每个Java类都直接或间接地继承自Object
类。这使得Object
类的实例方法可以被所有Java对象使用,因此它在Java编程中起着至关重要的作用。
- 定义:
Object
类是Java中所有类的超类。即使一个类没有显式地继承任何其他类,Java也会隐式地使其继承自Object
类。 - 作用:提供了通用的方法,用于处理对象的基本操作,如比较、克隆、转换为字符串等。
Object
类提供了几个关键的方法:
二、equals 方法
1、== 方法与 equals
- == 是一个运算符,可以判断基本类型也可以判断引用类型。判断基本类型时,就是判断两者的值是否相等;判断引用类型时,就是判断两者的地址是否相等,也就是说判断实际对象是不是同一个。
- equals 是 Object 中的方法,只能判断引用类型,也是通过判断两者地址是否相等。一般情况下,子类会重写这个 equals 方法,以添加判断内容是否相等的功能。
2、查看源代码
1)Object 中的 equals 源码:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到 Object 类中的 equals 方法就是通过判断两个引用类型的地址是否相等来确定它们是否相等的。
2)String 中的 equals 源码:
public boolean equals(Object anObject) {
// 首先判断传入的对象是不是与调用方法的对象是不是同一个,
// 直接判断地址是否相等,就可以判断是否是同一个对象,
// 如果是同一个则返回true
if (this == anObject) {
return true;
}
// 不是同一个对象,然后判断这个传入的参数是否是String类或其子类,
// 如果是本类或子类,则判断内容,如果不是,则返回false
if (anObject instanceof String) {
String anotherString = (String)anObject;// 是String类或其子类,则向下转型
int n = value.length;// 这里的value是当前调用方法的对象的字符数组,其中存储着这个字符串的内容,
// 这里获取字符串长度
if (n == anotherString.value.length) {// 然后获取传入的参数字符串的长度,然后比较,
// 如果长度相等,则接着判断内容,如果长度不相等,
// 则后面返回false
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {// 依次比较两个字符串的每一个字符
if (v1[i] != v2[i])
return false;// 一旦有一个字符不相等,则返回false
i++;
}
return true;// 内容相同,则返回true
}
}
return false;
}
可以看到这里重写了 Object 类的 equals 方法,增加了判断字符串内容是否相等的内容,如果字符串内容相等但不是同一个对象,也可以返回 true。
例子:
package com.pack1;
public class Test {
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
// 这里的 == 判断两者是否是同一个对象,显然不是同一个对象,所以这里是false
System.out.println(str1.equals(str2));
// 这里的 equals 会判断字符串内容是否相等,所以这里返回true
}
}
运行结果:
3)Integer 中的 equals 源码:
public boolean equals(Object obj) {
if (obj instanceof Integer) { // 判断参数是否是Integer类或2其子类,如果是,则进行下面的判断,如果不是,则返回false
return value == ((Integer)obj).intValue();// 判断当前对象与这个参数对象的整数值是否相等,如果值相等,则返回真, 反之则反
}
return false;
}
Integer 类也重写了 equals 方法,直接判断对象的类型是否是 Integer 类或其子类,如果是,则接着判断对象的整数值。
例子:
package com.pack1;
public class Test {
public static void main(String[] args) {
Integer num1 = new Integer(10000);
Integer num2 = new Integer(10000);
System.out.println(num1 == num2);
// 这里的 == 判断两者是否是同一个对象,显然不是同一个对象,所以这里是false
System.out.println(num1.equals(num2));
// 这里的 equals 会判断Integer的整型数值是否相等,所以这里返回true
}
}
运行结果:
3、Integer 补充
package com.pack1;
public class Test {
public static void main(String[] args) {
Integer num1 = 6;
Integer num2 = 6;
System.out.println(num1 == num2);
}
}
运行结果:
在使用上面的方法创建 Integer 对象时,如果我们使用 == 来判断这两个对象是否是同一个对象,就会出现一个特别的现象,结果是 true 也就是说这两个对象是同一个对象,这是什么原因呢?
这是因为 Java 中的 Integer缓存机制:
- 在Java中,
Integer
类会对在-128
到127
范围内的整数值进行缓存。Java的Integer
缓存机制仅适用于通过自动装箱或使用Integer.valueOf(int)
方法创建的Integer
对象。在这种情况下,Java会返回缓存的对象实例。
所以说,如果我们使用在 -128 到 127 范围内的值创建 Integer 对象时,这时引用变量指向的都是同一个对象,所以我们使用 == 会返回 true。
三、重写 equals 方法
1、给自定义类重写 equals 方法
我们现在有一个 Person 类,类中有名字和年龄属性,我们给 Person 类重写一个 equals 方法,要求如果名字和年龄两个属性相同就可以返回 true,否则返回 false。
package com.pack1;
public class Test {
public static void main(String[] args) {
Person bob1 = new Person("Bob", 18);
Person bob2 = new Person("Bob", 18);
System.out.println(bob1.equals(bob2));
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if(this == obj) { // 如果是同一个对象,则直接返回true
return true;
}
if(obj instanceof Person) { // 如果不是同一个对象,先判断是否是Person类,如果是Person类,接着判断年龄和名字是否相同
Person person = (Person)obj;
return (this.age == person.age && this.name.equals(person.name));
}
return false;
}
}
运行结果:
这里两个对象的名字和年龄相等,所以返回 true。
四、hashCode 方法
1、hashCode 方法介绍
在Java中,hashCode
方法是Object
类的一个重要方法,所有的Java对象都继承了这个方法。hashCode
的主要作用是返回对象的哈希码,即一个整数值,用于在哈希表等数据结构中快速定位对象。
- 提高具有哈希结构的容器的效率。
- 如果两个对象相等(即
equals
返回true
),那么它们的hashCode
值也必须相等。 - 在
Object
类中,hashCode
的默认实现通常会返回对象的内存地址的某种形式(不能完全将哈希值等价于地址),具体的实现依赖于JVM。
2、hashCode 方法示例
package com.pack1;
public class Test {
public static void main(String[] args) {
Person person1 = new Person();
Person person2 = new Person();
Person person3 = person2;
System.out.println("person1 的 hashcode: " + person1.hashCode());
System.out.println("person2 的 hashcode: " + person2.hashCode());
System.out.println("person3 的 hashcode: " + person3.hashCode());
}
}
class Person {
}
运行结果:
可以看到相同的对象的哈希码是相同的。
五、toString 方法
1、toString 方法介绍
在Java中,toString
方法是Object
类的一个重要方法,所有的Java对象都继承了这个方法。toString
的主要作用是返回对象的字符串表示形式。
在Object
类中,toString
的默认实现返回的是对象的全类名(包名加类名)和对象的哈希码(十六进制表示),格式通常是:
全类名@哈希码
源码如下:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
所以我们使用自定义类调用 toString 就会调用 Object 类中的 toString 方法。
package com.pack1;
public class Test {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.toString());
}
}
class Person {
}
运行结果:
可以发现运行结果的结构与我们上面提到的结构是一样的。
在自定义类中,通常建议重写toString
方法,以提供更具可读性的字符串表示。这使得在调试、日志记录或打印对象时,可以更直观地理解对象的内容和状态。
2、重写自定义类的 toString 方法
package com.pack1;
public class Test {
public static void main(String[] args) {
Person person = new Person("Bob", 18, "Teacher");
System.out.println(person.toString());
}
}
class Person {
private String name;
private int age;
private String job;
public Person(String name, int age, String job) {
this.name = name;
this.age = age;
this.job = job;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", job='" + job + '\'' +
'}';
}
}
运行结果:
3、toString 方法补充
当我们直接输出一个对象时,toString 方法会被默认调用。我们将上面的代码改动一下:
package com.pack1;
public class Test {
public static void main(String[] args) {
Person person = new Person("Bob", 18, "Teacher");
System.out.println(person);// 这里没有显式调用 toString 方法,但是会默认调用 toString 方法
}
}
class Person {
private String name;
private int age;
private String job;
public Person(String name, int age, String job) {
this.name = name;
this.age = age;
this.job = job;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", job='" + job + '\'' +
'}';
}
}
运行结果:
虽然我们没有显式的调用 toString 方法,但是实际上默认会调用 toString 方法。
六、finalize 方法
1、finalize 方法介绍
在Java中,finalize
方法是Object
类中的一个保护方法。它的主要作用是在对象被垃圾回收器回收之前,执行一些清理操作。也就是说当对象被垃圾回收器回收之前,系统就会调用该对象的 finalize 方法。
垃圾回收发生在某个对象没有任何引用时,也就是没有引用指向这个对象时,JVM就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象。
在Object
类中,finalize
方法的默认实现是空的。这意味着如果一个类没有重写finalize
方法,那么在对象被垃圾回收时不会执行任何特定的操作。
finalize
方法通常用于释放非Java资源,例如:
- 关闭文件句柄
- 释放网络连接
- 清理本地资源或外部系统资源
当你需要在对象被垃圾回收之前执行某些操作时,可以在自定义类中重写finalize
方法。
2、finalize 方法调用示例
package com.pack1;
public class Test {
public static void main(String[] args) {
Person bob = new Person("Bob");
bob = null;// 这里引用变量被置空,原来的对象没有引用了,变成了垃圾
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
System.out.println("释放一些资源...");
}
}
运行结果:
我们创建了一个 Person 类的对象,然后将引用变量置空了,这时对象没有了引用,就变成了垃圾,理论上应当被回收的,回收之前就应当调用 finalize 方法,我们在 Person 类中已经重写了 finalize 方法了,调用 finalize 方法就应该会输出“释放一些资源...”这句话啊,但是为什么没有呢?
实际上这是因为垃圾回收机制是有一些算法的,不会在一个对象变成垃圾的时就立即将其回收的,中间可能需要经过一段时间。这里我们可以使用 System.gc() 方法来建议JVM进行垃圾回收(但这并不是强制性的,而是一种“建议”,JVM可能会选择忽略这个请求。),就可以看到我们重写的 finalize 方法的效果了:
package com.pack1;
public class Test {
public static void main(String[] args) {
Person bob = new Person("Bob");
bob = null;// 这里引用变量被置空,原来的对象没有引用了,变成了垃圾
System.gc();
System.out.println("程序结束");
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
System.out.println("释放一些资源...");
}
}
运行结果:
这里我们发现我们重写的 finalize 方法被调用了,但是还是有一个奇怪的地方, 为什么是先执行的“程序结束”的输出语句,然后才调用的 finalize 方法的呢?
实际上这个 System.gc() 方法不是阻塞性的,在调用这个方法后程序还会接着运行,然后打印了“程序结束”,最后才进行了垃圾回收,在打印“程序结束”之后,垃圾回收之前,调用了 finalize 方法。
3、finalize 方法补充
在实际开发中,几乎不会使用 finalize 方法。
Java 7 引入了 try-with-resources
语句和 AutoCloseable
接口,这些提供了更可靠的资源管理方式。程序员可以通过这些工具确保在使用完资源后及时释放。
在 Java 9 中,finalize
方法被标记为已过时(deprecated),这进一步鼓励开发者使用其他更优雅的资源管理方法。