【年薪百万之IT界大神成长之路】重写hashcode和equals方法

 
愿你如阳光,明媚不忧伤。

目録

    • 1. Hash简介
    • 2. Hash的作用
    • 3. HashCode 和 equals 源码分析
    • 4. 重写hashCode()和equals()的场景
    • 5. 常用HASH函数
    • 6. 总结
  • 【每日一面】
          • 什么是哈希碰撞

 


1. Hash简介

Hash 散列(哈希)就是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值(哈希码),是一种压缩映射。Hash算法可以将一个数据转换为一个标志,这个标志和源数据的每一个字节都有十分紧密的关系。Hash算法还具有一个特点,就是很难找到逆向规律。Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。注意:equals相同,则hashCode相同;而hashCode相同,equals不一定相同(可能会发生散列碰撞)。

 


2. Hash的作用

  • 减少查找次数,提高程序效率
    假设,HashSet中已经有1000个元素。当插入第1001个元素时,需要怎么处理?
    因为HashSet是Set集合,它不允许有重复元素。将第1001个元素逐个的和前面1000个元素进行比较?显然,这个效率是相当低下的。散列表很好的解决了这个问题,它根据元素的散列码计算出元素在散列表中的位置,然后将元素插入该位置即可。对于相同的元素,自然是只保存了一个。
  • 安全加密
    日常用户密码加密通常使用的都是 md5、sha等哈希函数,因为不可逆,而且微小的区别加密之后的结果差距很大,所以安全性更好。例如:
    12345经过SHA1的哈希算法之后结果为:8cb2237d0679ca88db6464eac60da96345513964;
    123456经过SHA1的哈希算法之后结果为:7c4a8d09ca3762af61e59520943dc26494f8941b
  • 数据校验
    比如从网上下载的很多文件(尤其是P2P站点资源),都会包含一个 MD5 值,用于校验下载数据的完整性,避免数据在中途被劫持篡改。
  • 负载均衡
    对于同一个客户端上的请求,尤其是已登录用户的请求,需要将其会话请求都路由到同一台机器,以保证数据的一致性,这可以借助哈希算法来实现,通过用户 ID 尾号对总机器数取模(取多少位可以根据机器数定),将结果值作为机器编号。
  • 分布式缓存
    分布式缓存和其他机器或数据库的分布式不一样,因为每台机器存放的缓存数据不一致,每当缓存机器扩容时,需要对缓存存放机器进行重新索引(或者部分重新索引),这里应用到的也是哈希算法的思想。

 


3. HashCode 和 equals 源码分析

 

  • Object 源码
    默认的equals判断的是两个对象的引用指向的是不是同一个对象;而hashcode根据对象地址生成一个整数数值;另外我们可以看到Object的hashcode()方法的修饰符为native,表明该方法是操作系统实现的,java调用操作系统底层代码获取哈希值。
*****************************************************************
    @HotSpotIntrinsicCandidate
    public native int hashCode();
    public boolean equals(Object obj) {
     
        return (this == obj);
    }
................................................................
    public int hashCode() {
     
    int lockWord = shadow$_monitor_;
    final int lockWordStateMask = 0xC0000000; // Top 2 bits.
    final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
    final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits.
    if ((lockWord & lockWordStateMask) == lockWordStateHash) {
     
        return lockWord & lockWordHashMask;
    }
    //返回的是对象引用地址
    return System.identityHashCode(this);
}
*****************************************************************
  • String 源码
*****************************************************************
	/* 如果进行过hash计算,或者字符串的长度为0 ,不进行hash计算
	与16进制数0xff(二进制1111 1111 占一个字节)按位与运算,只有两个位同时为1,才能得到1
	经过计算后得到一个整数数值。
	*/
    public int hashCode() {
     
        int h = hash;
        if (h == 0 && value.length > 0) {
     
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }
................................................................
	// StringLatin1中的hashCode方法(单字节编码)
    public static int hashCode(byte[] value) {
     
        int h = 0;
        for (byte v : value) {
     
            h = 31 * h + (v & 0xff);
        }
        return h;
    }
	// StringUTF16中的hashCode方法(双字节编码,UTF8是变长编码)
    public static int hashCode(byte[] value) {
     
        int h = 0;
        int length = value.length >> 1;
        for (int i = 0; i < length; i++) {
     
            h = 31 * h + getChar(value, i);
        }
        return h;
    }
................................................................
・【模拟hash计算】
	String name = "God";
	
	value = {
     'G', 'o', 'd'};
	hash = 0;
	value.length = 3;
	
	//执行逻辑:
	val = value;
	val[0] = "G";
	val[1] = "o";
	val[2] = "d";
	
	h = 31 * 0 + G = G;
	
	h = 31 * (31 * 0 + G) + o = 31 * G + o;
	
	h = 31 * (31 * (31 * 0 + G) + o) + d = 31 * 31 * G + 31 * o + d;

	推导出数学公式:val[0]*31^(n-1) + val[1]*31^(n-2) + ... + val[n-1]  
	至于为什么是31?因为31的二进制是11111
*****************************************************************
	// 如果引用地址一样,则返回true;如果地址不一样,则挨个比较他们的字节码,也就是比较字符的内容。
    public boolean equals(Object anObject) {
     
        if (this == anObject) {
     
            return true;
        }
        if (anObject instanceof String) {
     
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
     
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                  : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }
................................................................
	// StringLatin1中的equals方法(单字节编码)
    @HotSpotIntrinsicCandidate
    public static boolean equals(byte[] value, byte[] other) {
     
        if (value.length == other.length) {
     
            for (int i = 0; i < value.length; i++) {
     
                if (value[i] != other[i]) {
     
                    return false;
                }
            }
            return true;
        }
        return false;
    }
	// StringUTF16中的equals方法(双字节编码,UTF8是变长编码)
    @HotSpotIntrinsicCandidate
    public static boolean equals(byte[] value, byte[] other) {
     
        if (value.length == other.length) {
     
            int len = value.length >> 1;
            for (int i = 0; i < len; i++) {
     
                if (getChar(value, i) != getChar(other, i)) {
     
                    return false;
                }
            }
            return true;
        }
        return false;
    }
*****************************************************************
  • Integer 源码
    hashcode方法直接返回数值本身;equals会先比较类型,然后比较数值是否一样。也就是说Integer的5 equals Byte的5,结果为false。
*****************************************************************
    @HotSpotIntrinsicCandidate
    public static int hashCode(int value) {
     
        return value;
    }
    public boolean equals(Object obj) {
     
        if (obj instanceof Integer) {
     
            return value == ((Integer)obj).intValue();
        }
        return false;
    }
*****************************************************************

 


4. 重写hashCode()和equals()的场景

  • 假设现在有很多苹果对象,要判断多个苹果是否相等,需要根据颜色,形状,重量进行判断,若全部相同,则判定他们是一样的;但现在重新制定了判定规则,如果形状和重量一样,即判定他们是一样的。
    例如:苹果A(颜色:红,形状:圆,重量:200.86)和苹果B(颜色:绿,形状:圆,重量:200.86)这时候如果不重写Object的equals方法,那么返回的一定是false不相等,这个时候就需要我们根据自己的需求重写equals()方法了。
*****************************************************************
package com.it.god.entity;

public class HashApple {
     
    private String color;
    private String shape;
    private Double gram;

    // 重写equals方法
    @Override
    public boolean equals(Object obj) {
     
        if (!(obj instanceof HashApple)) {
     
            return false;
        }
        HashApple happle = (HashApple) obj;
        if (this == happle) {
     
            return true;
        }
        if (happle.shape.equals(this.shape) && happle.gram.equals(this.gram)) {
     
            return true;
        } else {
     
            return false;
        }
    }

    public HashApple(String color, String shape, Double gram) {
     
        super();
        this.color = color;
        this.shape = shape;
        this.gram = gram;
    }

    public String getColor() {
     
        return color;
    }

    public void setColor(String color) {
     
        this.color = color;
    }

    public String getShape() {
     
        return shape;
    }

    public void setShape(String shape) {
     
        this.shape = shape;
    }

    public Double getGram() {
     
        return gram;
    }

    public void setGram(Double gram) {
     
        this.gram = gram;
    }

}

*****************************************************************
package com.it.god.controller;

import com.it.god.entity.HashApple;

public class HashAppleController {
     

    public static void main(String[] args) {
     
        HashApple ha1 = new HashApple("red", "circle", 200.86);
        HashApple ha2 = new HashApple("green", "circle", 200.86);
        if (ha1.equals(ha2)) {
     
            System.out.println("ha1与ha2两个苹果是一样的");
        } else {
     
            System.out.println("ha1与ha2两个苹果不一样");
        }
    }

}
................................................................
・【CONSOLE】结果
	ha1与ha2两个苹果是一样的
*****************************************************************
  • 到这里,你是不是会想,不是说要同时重写Object的equals方法和hashcode方法吗?那上面的例子怎么才只用到equals方法呢,hashcode方法没有体现出来,不要着急,我们往下看。
*****************************************************************
public class HashAppleController {
     

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
     
        HashApple ha1 = new HashApple("red", "circle", 200.86);
        HashApple ha2 = new HashApple("green", "circle", 200.86);
        if (ha1.equals(ha2)) {
     
            System.out.println("ha1与ha2两个苹果是一样的");
        } else {
     
            System.out.println("ha1与ha2两个苹果不一样");
        }

        @SuppressWarnings("rawtypes")
        Set set = new HashSet();
        set.add(ha1);
        set.add(ha2);
        System.out.println(set);
    }

}
................................................................
・【CONSOLE】结果
	ha1与ha2两个苹果是一样的
	[com.it.god.entity.HashApple@73a28541, com.it.god.entity.HashApple@5aaa6d82]
*****************************************************************
  • 看输出结果,equals判断是一样的,预期应该存入一个对象进入到set集合,然而,set却判定两个对象是不一样的,存进去了两个。这就涉及到Set的底层实现问题了,这里简单介绍下就是HashSet的底层是通过HashMap实现的,最终比较set容器内元素是否相等是通过比较对象的hashcode来判断的。重写hashcode方法之后,返回的set只有一个实例对象了,完美!
*****************************************************************
    // 重写hashcode方法
    @Override
    public int hashCode() {
     
        int result = shape.hashCode();
        result = 17 * result + gram.hashCode();
        return result;
    }
................................................................
・【CONSOLE】结果
	ha1与ha2两个苹果是一样的
	[com.it.god.entity.HashApple@fc89d439]
*****************************************************************

 


5. 常用HASH函数

  • 直接寻址法
    取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)
  • 数字分析法
    分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
  • 平方取中法
    取关键字平方后的中间几位作为散列地址。
  • 折叠法
    将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
  • 随机数法
    选择一随机函数,取关键字作为随机函数的种子生成随机值作为散列地址,通常用于关键字长度不同的场合。
  • 除留余数法
    取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生碰撞。

 


6. 总结

  1. hashCode主要用于提升查询效率,来确定在散列结构中对象的存储地址;
  2. 重写equals()必须重写hashCode(),二者参与计算的自身属性字段应该相同;
  3. hash类型的存储结构,添加元素重复性校验的标准就是先取hashCode值,后判断equals();
  4. equals()相等的两个对象,hashcode()一定相等;
  5. 反过来:hashcode()不等,一定能推出equals()也不等;
  6. hashcode()相等,equals()可能相等,也可能不等。

 


【每日一面】

什么是哈希碰撞

不同关键字通过相同哈希计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
例如一个hash算法为mod3对3取模运算,5mod3=2,8mod3=2,这就是哈希碰撞。

你可能感兴趣的:(IT界大神成长之路,hashcode,java,分布式)