关于toString、hashCode、equals方法详解

1.1 自动实现重写方式

当我们在类上添加lombok的@Data注解后,除了会为类中的各个字段添加get和set方法外,还会自动重写类中的toString、hashCode、equals方法,具体如下所示

@Data
public class Student{
    
    private String name;

    private Integer age;
}

编译后的类文件内容如下所示

public class Student {
    private String name;
    private Integer age;

    // 空参构造器(默认)
    public Student() {
    }

    // getter 和 setter
    public String getName() {
        return this.name;
    }

    public Integer getAge() {
        return this.age;
    }

    public void setName(final String name) {
        this.name = name;
    }

    public void setAge(final Integer age) {
        this.age = age;
    }

    // 重新equals方法
    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Student)) {
            return false;
        } else {
            Student other = (Student)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$age = this.getAge();
                Object other$age = other.getAge();
                if (this$age == null) {
                    if (other$age != null) {
                        return false;
                    }
                } else if (!this$age.equals(other$age)) {
                    return false;
                }

                Object this$name = this.getName();
                Object other$name = other.getName();
                if (this$name == null) {
                    if (other$name != null) {
                        return false;
                    }
                } else if (!this$name.equals(other$name)) {
                    return false;
                }

                return true;
            }
        }
    }
	
    // 检查传入的对象是否是当前对象的相同类型
    // 通常与equals(Object obj)方法一起使用,用于确保在进行对象比较时只比较相同类型的对象
    protected boolean canEqual(final Object other) {
        return other instanceof Student;
    }
    
	// 重新hashCode方法
    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $age = this.getAge();
        int result = result * 59 + ($age == null ? 43 : $age.hashCode());
        Object $name = this.getName();
        result = result * 59 + ($name == null ? 43 : $name.hashCode());
        return result;
    }
    
	// 重新toString方法
    public String toString() {
        return "Student(name=" + this.getName() + ", age=" + this.getAge() + ")";
    }
}

1.2、父类中实现

Java中所有对象的顶级父类都是Object类,如果在类的声明中没有使用extends关键字指明父类,则默认的父类为java.lang.Object类。

  • 在Object中的toString方法实现

通过该方法可以获取对象的一种字符串形式,默认的字符串是全类名@十六进制内存地址(示例:com.example.DemoApplicationTests$Student@239b98cb)

通过getClass().getName() 获取全类名

通过hashCode() 获取当前类的哈希值

通过调用Integer.toHexString(hashCode())获取十六进制值

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

关于Integer.toHexString(hashCode())的深挖见1.3部分

  • 在Object中的hashCode方法实现

其是一个native本地方法(该方法的具体实现是由本地代码(通常是C或C++)编写,Java虚拟机(JVM)会在运行时加载本地方法的实现,以执行实际的操作),用于返回对象的哈希码,它是一个32位的整数值。

哈希码值在集合类(如HashMap、HashSet等)中广泛使用,用于确定对象在集合中的存储位置。根据Java规范,如果两个对象使用equals方法判断为相等,那么它们的hashCode值也必须相等。反之,如果两个对象的hashCode值相等,不一定代表它们相等,只是有可能相等。(即通过hashCode方法,判断两个对象是否在某个散列存储结构的同一个位置中,通过equals方法,判断两个在同一个散列存储结构位置的对象是否相同)

public native int hashCode();
  • 在Object中的equals方法实现

对于引用变量来说,==比较的是两个对象的地址值是否相同。

对于基本类型,==比较的其值大小。

一般我们会重写了equals方法,比如String的equals被重写后,比较的是字符值,另外重写了equlas后,也必须重写hashcode()方法,因为在Java中,equals和hashCode方法是配对使用的。

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

1.3、Integer.toHexString(hashCode())源码深挖

参考文章:https://blog.csdn.net/fyzzlz/article/details/79716825

1.源码说明

Integer.toHexString(hashCode())方法是在Object父类中toString方法中调用的,用于返回当前对象的类全限定名和其十六进制内存地址,具体内容如下所示:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

2.详细说明:

(1)getClass

Object中的方法,不能被子类重写覆盖(final),是一个本地方法(native),返回一个调用该方法对象所属类的Class对象。

public final native Class<?> getClass();

(2)getClass().getName()

是Java中Class类的一个方法,它用于获取表示类的名称的字符串,返回的是包含类的全限定名的字符串。

(3)Integer.toHexString(hashCode())

获取当前的哈希值作为参数,调用Integer的toHexString方法,以16进制无符号整数形式返回此哈希码的字符串表示形式。

toHexString方法的源码如下所示,其中会调用toUnsignedString0方法:

public static String toHexString(int i) {
    return toUnsignedString0(i, 4);
}

toUnsignedString0方法源码如下所示

private static String toUnsignedString0(int val, int shift) {
    // assert shift > 0 && shift <=5 : "Illegal shift value";
    // 计算val参数除去前导零后的有效位数
    //  Integer.size == 32  int类型的最大位数 (int在jvm中占4个字节,共32位)
    //  Integer.numberOfLeadingZeros 方法用来计算前导零的数量,详情见a
    // mag为有效二进制位数(即除去前导零后,i需要占用的位数)
    int mag = Integer.SIZE - Integer.numberOfLeadingZeros(val);
    // 计算i有效二进制位数(除去前导零数量)所需要占的字符数(即对应的16进制的有效位数,每4个二进制位代表一个16进制数)
    /*
    	可以分为三类:
    	1. 有效位数mag == 0 那么只需要(1)个字符(4位)
    	2. 有效位数mag % 4 == 0 那么需要(mg / 4)个字符
    	3. 有效位数mag % 4 != 0 呢么需要(mag / 4 + 1)个字符
    	表达式Math.max(((mag + (shift - 1)) / shift), 1)可以覆盖上述三种情况
    */	
    int chars = Math.max(((mag + (shift - 1)) / shift), 1);
    // 按照有效二进制位数,创建一个字符数组
    char[] buf = new char[chars];
	
    // 将val值以16进制形式存入buf数组中 详情见b
    formatUnsignedInt(val, shift, buf, 0, chars);

    // Use special constructor which takes over "buf".
    return new String(buf, true);
}

详情a:Integer.numberOfLeadingZeros

该方法的作用是使用二分法,计算输入Interge类型i的前导零数量(即左边第一个非零值的位置)。

关于有符号右移运算符和无符号右移运算符说明(左移运算符同理):

  • 有符号右移运算符(>>)

    • 将操作数的二进制表示向右移动指定的位数。

    • 对于正数,向右移动时用 0 填充左侧空出的位。

    • 对于负数,向右移动时用 1 填充左侧空出的位,以保持负数的负号不变。

  • 无符号右移运算符(>>>)

    • 将操作数的二进制表示向右移动指定的位数。

    • 无论操作数是正数还是负数,都用 0 填充左侧空出的位,不考虑符号位。

  • 在移位运算符的后面加上等号表示移位并赋值

    • 如>>=表示有符号的右移并赋值操作运算符

具体的源码如下所示:

其二分的方法是每一次通过二分法将位数分为前一部分后后一部分(如此时是32位,经过二分后,分成高16位和低16位),对前一部分是否全是前导零进行判断(即经过无符号右移操作后判断是否为0),如果前导零在后一部分(即经过无符号右移操作后不为0),就将后一部分的内容放到前一部分(通过左移运算赋值符进行操作),再次通过二分获取前一部分的高位,并进行判断,如此往复,一直二分到两位的时候。最后对最高位进行判断,将i无符号右移31位,结果为0,说明此时的最高位为0,此时 n 就是i的前导零位数,结果为1,则说明最高位不为0,返回 n-1,即前导零个数减1(注意初始的前导零个数为1)。因为integer的位数是固定的,并且每次二分的对象都是前一部分,因此其二分的步骤也是固定的,可以分为五个步骤,如下所示。

public static int numberOfLeadingZeros(int i) {
    // 如果i是0,则表示前导零的个数就是Integer最大的位数,即32
    if (i == 0)return 32;
    // 变量n记录前导0个数,初始的前导0的个数为1
    int n = 1;
    // 二分计算前导零个数
    // 32位划分高16位和低16位,对高16位是否全是零进行判断
    if (i >>> 16 == 0) { 
        // 如果高16位全是0,那么前端0的个数至少有16个
        n += 16; 
        // 将低位部分放到高位,进行进行二分判断
        i <<= 16; 
    }
    // 16位划分高8位和低8位,对高8位是否全是零进行判断
    if (i >>> 24 == 0) { n +=  8; i <<=  8; }
    // 8位划分高4位和低4位,对高4位是否全是零进行判断
    if (i >>> 28 == 0) { n +=  4; i <<=  4; }
    // 8位划分高2位和低2位,对高2位是否全是零进行判断
    if (i >>> 30 == 0) { n +=  2; i <<=  2; }
    // 判断最高位是否为零
    n -= i >>> 31;
    return n;
}

详情b:formatUnsignedInt(val, shift, buf, 0, chars);

源码详解:

static int formatUnsignedInt(int val, int shift, char[] buf, int offset, int len) {
    // val的有效二进制位数
    int charPos = len;
    // shift == 4
    // 1 << 4 == 10000B
    int radix = 1 << shift;
    // mask == 1111B
    int mask = radix - 1;
    do {
        // offset + --charPos 每次从字符数组的最高位开始赋值
        // val & mask val低4位对应的十进制值 获取Integer.digits对应的字符
        buf[offset + --charPos] = Integer.digits[val & mask];
        // val 低4位右移
        val >>>= shift;
    } while (val != 0 && charPos > 0);

    // 返回16进制无符号字符串
    return charPos;
}

final static char[] digits = {
    '0' , '1' , '2' , '3' , '4' , '5' ,
    '6' , '7' , '8' , '9' , 'a' , 'b' ,
    'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
    'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
    'o' , 'p' , 'q' , 'r' , 's' , 't' ,
    'u' , 'v' , 'w' , 'x' , 'y' , 'z'
};
  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值