8.3 Set集合 8.3.1 HashSet类
8.3 Set集合
Set集合无序
前面已经介绍过Set集合,它类似于一个罐子,程序可以依次把多个对象”丢进”Set集合,而Set集合通常不能记住元素的添加顺序。
Set集合不可重复
Set
集合与Collection
基本相同,没有提供任何额外的方法。实际上Set
就是Collection
,只是行为略有不同(Set
不允许包含重复元素)。
Set
集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set
集合中,则添加操作失败,add()
方法返回false
,并且新元素不会覆盖旧元素。
上面介绍的是Set
集合的通用知识,因此完全适合后面介绍的HashSet
、 TreeSet
和EnumSet
三个实现类,只是三个实现类还各有特色。
8.3.1 HashSet类
HashSet
是Set
接口的典型实现,大多数时候使用Set
集合时就是使用这个实现类。 HashSet
按Hash
算法来存储集合中的元素,具有很好的存取和査找性能。
HashSet特点
HashSet
具有以下特点。
- 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
HashSet
不是同步的,如果多个线程同时访问一个HashSet
,假设有两个或者两个以上线程同时修改了HashSet
集合时,则必须通过代码来保证其同步。- 集合元素值可以是
null
HashSet判断两个元素相等的标准
当向HashSet
集合中存入一个元素时, HashSet
会调用该对象的hashCode()
方法来得到该对象的hashCode
值,然后根据该hashCode
值决定该对象在HashSet
中的存储位置。
如果有两个元素通过equals()
方法比较返回true
,但它们的hashCode()
方法返回值不相等, HashSet
将会把它们存储在不同的位置,依然可以添加成功。
也就是说, HashSet
集合判断两个元素相等的标准是两个对象通过equals
方法比较相等,并且两个对象的hashCode()
方法返回值也相等。
程序 HashSet判断元素相同的标准
下面程序分别提供了三个类A、B和C,它们分别重写了equals
、 hashCode()
两个方法的一个或全部,通过此程序可以让读者看到HashSet
判断集合元素相同的标准。
1 | import java.util.*; |
上面程序中向books
集合中分别添加了两个A对象、两个B对象和两个C对象,其中C类重写了equals
方法总是返回true
, hashCode()
方法总是返回2,这将导致HashSet
把两个C对象当成同一个对象。运行上面程序,看到如下运行结果:
1 | [A@7852e922, B@1, B@1, C@2, A@4e25154f] |
从上面程序可以看出:
- 即使两个A对象通过
equals()
方法比较返回true
,但HashSet
依然把它们当成两个对象; - 即使两个B对象的
hashCode()
返回相同值(都是1),但HashSet
依然把它们当成两个对象。
HashSet中的元素要求 equals方法和hashCode方法返回值相同
这里有一个注意点:当把一个对象放入HashSet
中时,如果需要重写该对象对应类的equals()
方法,则也应该重写其hashCode()
方法。
规则是:如果两个对象通过equals
方法比较返回true
,这两个对象的hashCode()
方法的返回值也应该相同
hashCode返回值相同 equals返回值不同时会怎样
如果两个对象的hashCode()
方法返回的hashCode
值相同,但它们通过equals()
方法比较返回false
时将更麻烦:因为两个对象的hashCode
值相同, HashSet
将试图把它们保存在同一个位置,但是放在同一个位置是不行的,因为新的对象会覆盖旧的对象导致数据丢失,所以实际上会在这个位置用链式结构来保存多个对象;
而HashSet
访问集合元素时也是根据元素的hashCode
值来快速定位的,如果HashSet
中两个以上的元素具有相同的hashCode
值,这需要链表上继续查找,这将会导致性能下降。
hashCode()
方法对于HashSet
是不是十分重要
hash
算法可以快速查找被检索的对象,hash
算法的价值在于速度。当需要查询集合中某个元素时,hash
算法可以直接根据该元素的hashCode
值计算出该元素的存储位置,从而快速定位到该元素。
通过索引可以计算数组元素在内存中的位置
为了理解这个概念,可以先看数组(数组是所有能存储一组元素里最快的数据结构)数组可以包含多个元素,每个元素都有索引,如果需要访问某个数组元素,只需提供该元素的索引,接下来即可根据索引计算该元素在内存里的存储位置。
通过hashCode也可以计算元素的存储位置
表面上看起来, HashSet
集合里的元素都没有索引,实际上,当程序向HashSet
集合中添加元素时, HashSet
会根据该元素的hashCode
值来计算它的存储位置,这样也可快速定位到该元素。
为什么不直接使用数组、还需要使用HashSet
呢?
为什么不直接使用数组、还需要使用HashSet
呢?因为数组元素的索引是连续的,而且数组的长度是固定的,无法自由增加数组的长度。
而HashSet
就不一样了, HashSet
采用每个元素的hashCode
值来计算其存储位置,从而可以自由增加HashSet
的长度,并可以根据元素的hashCode
值来访问元素。
因此,当从HashSet
中访问元素时, HashSet
先调用该对象的hashCode
方法来计算该元素的hashCode
值,然后直接到该hashCode
值对应的内存地址去取出该元素,这就是HashSet
速度很快的原因。
HashSet的桶
HashSet
中每个能存储元素的”槽位”(slot
)通常称为”桶”(bucket
),如果有多个元素的hashCode
值相同,但它们通过equals
方法比较返回false
,就需要在一个”桶”里放多个元素,这样会导致性能下降。
重写hashCode()方法的基本规则
前面介绍了hashCode()
方法对于HashSet
的重要性(实际上,对象的hashCode
值对于后面的HashMap
同样重要),下面给出重写hashCode()
方法的基本规则。
- 在程序运行过程中,同一个对象多次调用
hashCode()
方法应该返回相同的值。 - 当两个对象通过
equals()
方法比较返回true
时,这两个对象的hashCode()
方法应返回相等的值。 - 对象中用作
equals
方法比较标准的实例变量,都应该用于计算hashCode
值。
重写hashCode()方法的一般步骤
下面给出重写hashCode()
方法的一般步骤。
- 把对象内每个参与
equals
方法比较标准的实例变量都计算出一个int
类型的hashCode
值。计算方式如下表所示:
实例变量类型 | 计算方式 |
---|---|
boolean 变量f |
hashCode=(f?0:1) |
整数类型(byte,short,char,int )变量f |
hashCode=(int) f; |
long 类型变量f |
hashCode=(int)(f^(f>>>32)); |
float 类型变量f |
hashCode=Float.frontToIntBits(f) ; |
double 类型变量f |
long l=Double.doubleToLongBits(f); hashCode=(int)(l^(l>>>32)); |
引用类型变量f | hashCode=f.hashCode(); |
- 用第1步计算出来的多个
hashCode
值组合计算出一个hashCode
值返回。例如如下代码:为了避免直接相加产生偶然相等(两个对象的1
return f1.hashCode()+(int)f2;
f1
、f2
实例变量并不相等,但它们的hash Code
的和恰好相等),可以通过为各实例变量的hashCode
值各自乘以任意一个质数
后再相加。例如如下代码:1
return f1.hashCode() * 19 +(int) f2 *31;
HashSet中添加可变对象出现的问题
如果向HashSet
中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equals
方法比较返回true
,两个对象的hashCode
值也相等),这就有可能导致HashSet
中包含两个相同的对象。
下面程序演示了这种情况。
1 | import java.util.*; |
上面程序中提供了R类,R类重写了equals(Object object)
方法和hashCode()
方法,这两个方法都是根据R对象的count
实例变量来判断的。
上面程序的①号代码处改变了Set
集合中第一个R对象的count
实例变量的值,这将导致该R对象与集合中的其他对象相同。
程序运行结果如下所示:
1 | [R[count:-2], R[count:-3], R[count:5], R[count:9]] |
正如上面运行结果中所见到的,HashSet
集合中的第1个元素和第2个元素完全相同,这表明两个元素已经重复。
此时HashSet
会比较混乱:当试图删除count
为-3
的R对象时, HashSet
会计算出该对象的hashCode
值,从而找出该对象在集合中的保存位置,然后把此处的对象与count
为-3
的R对象通过equals
方法进行比较,如果相等则删除该对象。
HashSet
只有第2个元素才满足该条件(第1个元素实际上保存在count
为-2
的R对象对应的位置),所以第2个元素被删除。
至于第一个count
为-3
的R对象,它保存在count
为-2
的R对象对应的位置,但使用equals
方法拿它和count
为-2
的R对象比较时又返回false
这将导致HashSet
不可能准确访问该元素。
元素放到HashSet后,不要修改参与计算hashCode和equals的实例变量的值
由此可见,当程序把可变对象添加到HashSet
中之后,尽量不要去修改该集合元素中参与计算hashCode()
、 equals()
的实例变量,否则将会导致HashSet
无法正确操作这些集合元素。
注意:
当向HashSet
中添加可变对象时,必须十分小心。如果修改HashSet
集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致HashSet
无法准确访问该对象。