Object 通用方法

2023/11/6 Java基础

Object类是所有类的超类。如果在类声明中没有使用extends关键字明确指定超类,那么默认的超类就是Object类。这就意味着所有的对象(包括数组)都实现了该类的方法。

# 1、概览

native表示这个方法的实现是由其他语言(比如C或C++)编写的,它并不在Java源代码中。通过使用本地方法,我们可以直接调用其他语言的功能和库,从而提高性能或实现一些特定的系统操作。

Object所有方法

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native Class<?> getClass()

protected void finalize() throws Throwable {}

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2、equals()

Object的equals()方法源码

public boolean equals(Object obj) {
    return (this == obj);
}
1
2
3

# 2.1、等价关系

两个对象具有等价关系,需要满足五个条件:自反性、对称性、传递性、一致性、非空性。

  • 自反性
x.equals(x); // true
1
  • 对称性
x.equals(y) == y.equals(x); // true
1
  • 传递性
if (x.equals(y) && y.equals(z))
    x.equals(z); // true;
1
2
  • 一致性

多次调用 equals() 方法结果不变

x.equals(y) == x.equals(y); // true
1
  • 非空性

对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false

x.equals(null); // false;
1

# 2.2、equals() 与 ==

理解

  • ==:如果比较的是基本数据类型,那么比较的是变量的值,如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)
  • equals:如果没重写 equals 方法比较的是两个对象的地址值,如果重写了 equals 方法后我们往往比较的是对象中的属性的内容,equals 方法是从 Object 类中继承的,默认的实现就是使用==

这里Integer重写了equals方法,比较值内容是否相等

Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y);      // false
1
2
3
4

对于String而言,String类对equals方法进行了重写,它比较的是字符串对象的内容而不是对象的引用。也就是比较String时,equals比较的是内容是否相同。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2.3、实现

按要求实现equals()

  • 检查是否为同一个对象的引用,如果是直接返回 true;
  • 检查是否是同一个类型,如果不是,直接返回 false;
  • 将 Object 对象进行转型;
  • 判断每个关键域是否相等。
public class EqualExample {
    private int x;
    private int y;
    private int z;

    public EqualExample(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

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

        EqualExample that = (EqualExample) o;

        if (x != that.x) return false;
        if (y != that.y) return false;
        return z == that.z;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3、hashCode()

# 3.1、hashCode介绍

hashCode()是一个由Object类发起并由Java系统原生支持的方法。它用于获取对象的哈希码,通常在散列算法中使用,如Java中的HashSet、HashMap等数据结构。

hashCode()返回的是对象在内存中的地址,通过转换得到的一个值(这个转换过程叫做散列运算,得到的值也可以叫散列值)。用于快速访问和定位,在一些涉及到大量对象并且需要快速访问的场合可以提高效率。并且这个内容相同时每次执行hashCode()返回的值都一样。但不同内容的hashCode()返回值不一定相同。

String s1 = "abc";
System.out.println(s1.hashCode());

// 输出:96354
1
2
3
4

例子:10个人排队,每个人都有身份证号码(唯一),分成3个组排队,将身份证号码除以3取余数,余数大于3就再除,不够四舍五入,直到值为1,2,3。就按照这个值排到指定组去。

# 3.2、equals()与hashCode()

  • hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。

  • 在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。

  • 下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2
1
2
3
4
5
6
7
  • 理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。

  • R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。并且一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}
1
2
3
4
5
6
7
8

# 3.3、重写equals()

保证在重写equals()方法后,hashCode()方法也返回相同的值。

public class Person {
    private String name;
    private int age;

    // 构造函数、getter和setter方法省略

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

用于比较的所有属性计算出一个唯一的哈希码,以保证不同的对象不会生成相同的哈希码。使用Java提供的Objects工具类的hash()方法来简化计算过程。

# 3.4、为什么重写equals()

  • 当我们想比较两个对象的内容是否相等时,我们需要重写equals()方法。默认的equals()方法(定义在Java的Object类中)只是比较两个对象的内存地址,也就是说它只会检查两个引用是否指向堆内存中的同一个实例。如果我们需要比较的是对象的内容(例如,两个字符串是否包含相同的字符),那么我们就必须重写equals()方法。

  • 如果你的类被用作HashMap的键或者用于List的contains()或remove()方法,那么正确的重写equals()方法就变得非常重要了,因为这些操作都依赖于equals()方法来确定对象是否相等。如果你没有正确地重写equals()方法,那么你可能会发现你的HashMap或List并不按照你预期的那样工作。

# 4、toString()

默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

public class ToStringExample {
    private int number;

    public ToStringExample(int number) {
        this.number = number;
    }
}
1
2
3
4
5
6
7
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
1
2
ToStringExample@4554617c
1

# 5、clone()

# 5.1、cloneable

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

public class CloneExample {
    private int a;
    private int b;
}
1
2
3
4
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
1
2

重写 clone() 得到以下实现:

public class CloneExample {
    private int a;
    private int b;

    @Override
    protected CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }
}
1
2
3
4
5
6
7
8
9
CloneExample e1 = new CloneExample();
try {
    CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
1
2
3
4
5
6
java.lang.CloneNotSupportedException: CloneExample
1

以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。

应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

public class CloneExample implements Cloneable {
    private int a;
    private int b;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
1
2
3
4
5
6
7
8
9

# 5.2、浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象。

public class ShallowCloneExample implements Cloneable {

    private int[] arr;

    public ShallowCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected ShallowCloneExample clone() throws CloneNotSupportedException {
        return (ShallowCloneExample) super.clone();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
1
2
3
4
5
6
7
8
9

# 5.3、深拷贝

拷贝对象和原始对象的引用类型引用不同对象。

public class DeepCloneExample implements Cloneable {

    private int[] arr;

    public DeepCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected DeepCloneExample clone() throws CloneNotSupportedException {
        DeepCloneExample result = (DeepCloneExample) super.clone();
        result.arr = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            result.arr[i] = arr[i];
        }
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
1
2
3
4
5
6
7
8
9

理解

拷贝对象,并将属性也引用新的对象

# 5.4、clone() 的替代方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

public class CloneConstructorExample {

    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
1
2
3
4