跳至主要內容

Object 类

AruNi_LuJavaJava 基础约 2542 字大约 8 分钟

本文内容

1. Object 介绍

Object 类是所有类的父类,任何一个类都 间接或直接继承 至 Object 类,所有对象(包括数组)都实现了此类的方法。

如果一个类没有直接明确继承哪一个类,那么它就是 Object 的直接子类:

// 隐式继承
class User {
}

// 显示继承
class User extends Object {
}

因此,Object 类可以接受所有 引用数据类型,通过向下转为需要的类型使用。

Object o = new User(1, "张三");
User user = (User) o;

Object 主要提供了以下 11 个方法:

/**
 * 返回当前运行时对象的 Class 对象
 */
public final native Class<?> getClass()
    
/**
 * 返回对象的哈希码,主要使用在哈希表中,比如 HashMap、HashSet 等
 */
public native int hashCode()
    
/**
 * 比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等
 */
public boolean equals(Object obj)
    
/**
 * 创建并返回当前对象的一份拷贝
 */
protected native Object clone() throws CloneNotSupportedException
    
/**
 * 返回对象的 字符串 表示形式,建议 Object 所有的子类都重写这个方法。
 */
public String toString()
    
/**
 * 唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念),如果有多个线程在等待只会任意唤醒一个。
 */
public final native void notify()
    
/**
 * 跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
 */
public final native void notifyAll()
    
/**
 * 暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
 */
public final native void wait(long timeout) throws InterruptedException
    
/**
 * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位),所以超时的时间还需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
    
/**
 * 跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这个概念
 */
public final void wait() throws InterruptedException
    
/**
 * 实例被垃圾回收器回收的时候触发的操作,JDK 9 已弃用
 */
protected void finalize() throws Throwable { }

2. equals() 和 == 的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于 基本数据类型== 比较的是值;
  • 对于 引用数据类型== 比较的是对象的 内存地址

因为 Java 只有 值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其 本质比较的都是值,只不过 引用类型 变量存的值是 对象的地址

equals() 用于判断两个对象是否相等。equals() 方法存在于 Object 类中,而 Object 类是所有类的直接或间接父类,因此 所有的类都有 equals() 方法

equals() 方法存在两种使用情况:

  • 类没有重写 equals() 方法:使用的是默认的 Object 类的 equals() 方法,直接使用 “==” 比较;
  • 类重写了 equals() 方法:我们一般都重写 equals() 方法来比较两个对象中的 属性 是否相等;若它们的属性相等,则返回 true;

示例:

class Test {
    public static void main(String[] args) {
        User user1 = new User(1, "张三");
        User user2 = new User(1, "张三");
        System.out.println(user1.equals(user2));	// false
    }
}

class User {
    int id;
    String name;
    
    // 构造函数略
}

可以发现,当没有重写 equals 方法时,默认使用 Object 的 equals 方法,直接使用 “==” 比较,两个对象的地址值肯定不一样,返回 false。

接下来自己重写 equals 方法,要求对象的值相同即可返回 true:

class Test {
    public static void main(String[] args) {
        User user1 = new User(1, "张三");
        User user2 = new User(1, "张三");
        System.out.println(user1.equals(user2));	// true
    }
}

class User {
    int id;
    String name;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        
        User user = (User) o;
        return this.id == user.id && this.name.equals(user.name);
    }
    
    // 构造函数略
}

3. hashCode() 有什么用?

3.1 hashCode() 的作用

hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。

hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。

另外需要注意的是: ObjecthashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。

散列表存储的是键值对(key-value),它的特点是:能根据 “键” 快速的检索出对应的 “值”。这其中就利用到了哈希码!(可以快速找到所需要的对象)

3.2 为什么需要 hashCode?

以 “HashSet 如何检查重复” 为例子来说明为什么要有 hashCode

  • 当把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较;
  • 如果没有相同的 hashCodeHashSet 会假设对象没有重复出现;
  • 但是如果发现 有相同 hashCode 值的对象,这时会 调用 equals() 方法 来检查 hashCode 相等的对象是否真的相同:
    • 如果两者相同,HashSet 就不会让其加入操作成功。
    • 如果 不同 的话,说明发生了哈希冲突,就会 重新散列到其他位置

可以发现,HashSet 会先使用 hashCode 来判断是否有重复元素,只有当 HashCode 相等时(发生了哈希冲突),才会调用 equals 方法进一步判断对象是否真的相等。这样我们就 大大减少了 equals 的调用次数,提高了执行速度

因此,hashCode()equals() 其实都是用于比较两个对象是否相等,只不过 hashCode() 的说服力没有那么强,毕竟 hashCode 存在哈希冲突。

因为存在哈希冲突的可能,因此 两个对象的 hashCode 值相等并不代表两个对象就相等

现在问题又来了,既然 hashCode() 没有 equals() 说服力强,那为什么还需要 hashCode() 呢?直接使用 equals() 不就好了吗??

这是因为在一些容器(比如 HashMapHashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进 HashSet 的过程)。

总结下来就是:

  • 如果两个对象的 hashCode 值相等,那这两个对象不一定相等(哈希碰撞);
  • 如果两个对象的 hashCode 值相等,且 equals() 也返回 true,才认为这两个对象相等;
  • 如果两个对象的 hashCode 值不相等,那么直接肯定这两个对象不相等;

3.3 重写 equals() 时必须重写 hashCode() ?

每个类的用途不一样,因此下面分为 该类是否被散列表使用 来讨论。

类不会被散列表使用

当我们不在 HashSet,HashTable,HashMap 等等这些 本质是散列表 的数据结构中用到这个类时,equals() 用来比较该类的两个对象是否相等,而 hashCode() 则根本使用不到

因为不是散列表的结构中,不会调用什么 hashCode() 方法,所以 重不重写都没关系

类会被散列表使用

当该类会被散列表使用时,该类的 hashCode()equals 是有关系的:

  • 如果两个对象相等,那么它们的 hashCode() 值一定相同。这里的 “对象相等” 是指通过 equals() 比较两个对象时返回 true;
  • 如果两个对象的 hashCode 相等它们的 equals() 不一定相等。因为存在 哈希冲突

因为散列表的数据结构 HashSet、HashMap 等在方法中都会调用 hashCode() 方法,所以为了安全,必须重写 hashCode()

若没有重写 hashCode(),会有什么问题呢

在添加对象时:如果 只重写了 equals,那么只要 值相等,我们就认为对象相等,但若这两个对象的 哈希值不相等,在 HashSet 中根据哈希值 来判断两个对象是否相等时就会 认为这两个对象不相等,因此将它们 都装入集合中,这显然是有问题的。

例如,下面我们只重写了 equals(),可以发现,我们认为两个对象的值相等就是相同了,但由于它们的 hashCode 不同,HashSet 将它们都添加进集合了:

class Test {
    public static void main(String[] args) {
        User user1 = new User(1, "aaa"), user2 = new User(1, "aaa");
        HashSet<User> set = new HashSet<>();
        set.add(user1); set.add(user2);
        for (User user : set) System.out.println(user);
    }
}
/* 输出:
User{id=1, name='aaa'}
User{id=1, name='aaa'}
*/

class User {
    int id;
    String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;
        return this.id == user.id && this.name.equals(user.name);
    }
    
    // 构造方法、toString() 略
}

现在我们重写 hashCode(),根据 id 和 name 属性来生成 hashCode,保证对象的属性值相同,hashCode 也相同。此时 HashSet 中只有一个对象:

class Test {
    public static void main(String[] args) {
        User user1 = new User(1, "aaa"), user2 = new User(1, "aaa");
        HashSet<User> set = new HashSet<>();
        set.add(user1); set.add(user2);
        for (User user : set) System.out.println(user);
    }
}
/* 输出:
User{id=1, name='aaa'}
*/

class User {
    int id;
    String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;
        return this.id == user.id && this.name.equals(user.name);
    }
    
    @Override
    public int hashCode() {
        // 根据 id 和 name 属性来生成 hashCode
        return Objects.hash(id, name);
    }
    
    // 构造方法、toString() 略
}

如果你翻开过 String 类的源码,会发现 String 类不仅重写了 equals(),还重写了 hashCode() 方法,因为 String 类是我们编程时使用得最多的类,也常常将它用作 HashMap 的 key,而 HashMap 的 key 是不可以重复的。正因为 String 类已经帮我们重写好了相关方法,所以我们使用 String 类时才如此方便安全。

4. 参考文章

上次编辑于: