Person类
public class Person {
private Integer bao;
private Integer card;
private Integer hu;
public Person(Integer bao, Integer card,Integer hu) {
this.bao = bao;
this.card = card;
this.hu = hu;
}
public Integer getBao() {
return bao;
}
public void setBao(Integer bao) {
this.bao = bao;
}
public Integer getCard() {
return card;
}
public void setCard(Integer card) {
this.card = card;
}
public Integer getHu() {
return hu;
}
public void setHu(Integer hu) {
this.hu = hu;
}
@Override
public String toString() {
return "Person{" +
"bao=" + bao +
", card=" + card +
", hu=" + hu +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(bao, person.bao) && Objects.equals(card, person.card);
}
@Override
public int hashCode() {
return Objects.hash(bao, card);
}
}
测试类
public class test {
public static void main(String[] args) {
List<Person> list = new ArrayList<Person>();
list.add(new Person(123 ,8,1));
list.add(new Person(123,18,2));
list.add(new Person(456, 28,3));
list.add(new Person(789,38,4));
list.add(new Person(123, 8,5));
list.add(new Person(564, 38,6));
/**对list集合的Person类根据bao字段和card字段去重,因为我的Person类只重写了bao和card字段的
hashcode和equals方法,你可以根据不同的业务情况,根据不同的字段进行去重。*/
List<Person> collect1 = list.stream().distinct().collect(Collectors.toList());
//对去重后的集合collect1进行分组计算人数
Map<Integer, Long> collect = collect1.stream().collect(Collectors.groupingBy(Person::getBao, Collectors.counting()));
System.out.println("collect = " + collect);
}
}
打印输出结果
注:我在想这个重写的hashCode方法为什么就能让对象的hashcode值相等了??
1. 整型的哈希函数
对于整型数据,他本来就是一个数值,JDK中直接将这个值返回作为他的哈希值!JDK中实现整型的哈希函数的方法:
2.字符串的哈希函数
字符串底层存储的还是用整型数据char存储的,比如存储 hello, 底层是['h', 'e', 'l', 'l', 'o']进行存储,JDK当中要尽可能的充分利用字符串里面的每个字符信息来进行计算。
计算公式可以用下图所示:
其中s,表示存储字符串的字符数组
n表示字符数组当中字符的个数
3.自定义hash函数
比如上文的Person类,重写它的hashcode方法
进入方法内可看到
实际上就是将类当中所有的字段封装成一个数组,然后像计算字符串的哈希值那样去计算我们自定义类的哈希值。
4.集合类型的hash函数
集合类型的哈希函数也是像字符串那样设计哈希函数
上述代码也可以用之前的公式来表示,其中s[i]表示集合当中第i个数据对象
5.这么多的hashcode方法为什么都是用了31
之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,
因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计
算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能:
31 * i == (i << 5)- i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。
所谓素数:质数又称素数,指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。
素数在使用的时候有一个作用就是,如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除!如:我们选择素数3来做系数,那么3*n只能被3和n或者1来整除,我们可以很容易的通过3n来计算出这个n来。这应该也是一个原因!
HashMap在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“Hash冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
31可以由31 * i == (i << 5) - i来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!在java乘法中如果数字相乘过大会导致溢出的问题,从而导致数据的丢失,而31则是素数(质数)而且不是很长的数字,最终它被选择为相乘的系数的原因。
可以看到,使用 31 最主要的还是为了性能。