您的位置:首页 > 编程语言 > Java开发

JAVA HashSet 原理分析

2015-10-24 21:23 666 查看
1,HashSet本质上是HashMap。它使用HashMap的Key来保存HashSet中存放的元素,而HashMap的Value则为一个final static 的Object对象PRESENT。其部分实现源码如下:

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map;//用HashMap的Key来保存HashSet的元素E
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();//PRESENT为HashMap的Value


2,使用HashSet/HashMap存储对象时,如果该对象是自定义类型的对象,最好重写equals 方法和 hashCode 方法。

我们知道,对于HashSet而言,它不会存储重复的元素。先看下面一个示例:

import java.util.HashSet;
import java.util.Set;

public class HashSetTest {

public static void main(String[] args) {
Set<Name> s = new HashSet<Name>();
s.add(new Name("abc", "123"));
System.out.println(s.contains(new Name("abc", "123")));//print false
}

}

class Name{
private String first;
private String last;

public Name(String first, String last){
this.first = first;
this.last = last;
}

@Override
public boolean equals(Object o){
if(this == o)
return true;
if(o.getClass() == Name.class)
{
Name n = (Name)o;
return n.first.equals(first) && n.last.equals(last);
}
return false;
}
}
Name 类重写了equals 方法,但没有重写hashCode方法。main函数的System.out.println输出false。为什么?从源代码来分析。HashSet的contains方法源码如下:

public boolean contains(Object o) {
return map.containsKey(o);
}
表明,它调用的是HashMap的containsKey方法,这也说明了HashSet的本质是一个HashMap,它使用HashMap的Key来保存HashSet中的元素。再看HashMap的containsKey方法如下:

public boolean containsKey(Object key) {
return getEntry(key) != null;
}
它调用getEntry方法来判断是否包含Key,那getEntry方法的源代码如下:

final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}

int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}


现在我们来分析,为什么上面的HashSetTest中的语句输出false

System.out.println(s.contains(new Name("abc", "123")));//print false
在getEntry方法中,计算了key的hash值。在HashSetTest.java 中的Name类只重写了equals方法没有重写hashCode方法。因此,
System.out.println(s.contains(new Name("abc", "123")));//print false
使用new 创建的Name对象存储在内存中的某个位置,而
s.add(new Name("abc", "123"));
中new 创建的Name对象存储在内存中的另一个位置。这两个位置是不同的,因此这两个对象的hashCode方法计算得到的散列码是不相同的。因此,getEntry方法中的 for 循环
Entry<K,V> e = table[indexFor(hash, table.length)]
e
为 null。因为 indexFor(hash, table.length)的参数hash 并不是 s.add(new Name("abc", "123"))添加的这个Name对象的hash码。

最终,getEntry方法直接返回一个null,使得 containsKey返回false,从而使得s.contains(new Name("abc", "123"))返回false。

从整个过程可以看出,在HashSet中判断某个元素是否已经存在时,首先得到这个元素的hashCode(散列码),根据散列码定位到这个元素存储的实际位置,如果这个存储位置上已经存储了某个元素了(表明散列冲突了)(进入getEntry方法的for循环),再进一步判断元素的key是否相同。

从这里也可以看出,重写了equals方法后,也需要去重写hashCode方法,使得当equals方法返回true时,这两个对象的hashCode值应该相同。


3,为什么HashSet的add方法不会添加重复的元素?

①什么是重复的元素?在这里,重复的标准就是:如果两个元素的hashCode相同 且 equals方法比较也相同(返回true),则说明这两个元素是重复的。

②可以存在两个对象的hashCode相同,但是它们是两个不同的对象(在不同的内存地址空间)。

看一个示例如下:

import java.util.HashSet;
import java.util.Set;

public class HashSetTest {

public static void main(String[] args) {
Set<Name> s = new HashSet<Name>();
s.add(new Name("abc", "123"));
s.add(new Name("abc", "456"));//add 方法返回 false
for(Name n : s){
String first = n.getFirst();
String last = n.getLast();
System.out.println("first:" + first + " last:" + last);// first:abc last:123
}
}

}

class Name{
private String first;
private String last;

public Name(String first, String last){
this.first = first;
this.last = last;
}

public String getFirst(){
return first;
}
public String getLast(){
return last;
}

@Override
public boolean equals(Object o){
if(this == o)
return true;
if(o.getClass() == Name.class)
{
Name n = (Name)o;
return n.first.equals(first) ;
}
return false;
}

@Override
public int hashCode(){
return first.hashCode();
}
}


现在判断Name类对象相同的标准就是:如果两个Name对象的first属性相同,则称它们有相同的hashCode且equals返回true。

在HashSetTest2中,创建了两个Name对象,这两个对象存在于内存的不同地址空间中,但是它们的散列码hashCode是相同的。当欲将 new Name("abc", "456")添加到HashSet中去时,实质上是没有将该对象添加进去的。为什么呢?分析HashSet的add方法的源代码得知,它调用的是HashMap的put方法。HashMap的put方法如下:

public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);// 获得对象的散列码
int i = indexFor(hash, table.length);//根据散列码来查找该对象存储的地址
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//e!=null时,表明该地址已经有元素在存储了
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//判断已存储的元素与当前元素是否“相同”
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}
因为:Name类只根据first属性来判断Name对象是否相同,这里的两个Name对象的first属性都是相同的。因此,对于对象Name("abc", "456")而言,在HashMap的put方法中 int hash = hash(key); 得到的是 Name("abc", "123")的hash ;

接着,int i = indexFor(hash, table.length);

得到的地址是存储对象 Name("abc", "123")的地址,即 e 就是代表对象Name("abc", "123");再进入for循环中的if判断,

由于:  对象 Name("abc", "123").equals (对象Name("abc", "456")) 返回的是true(因为它们的first属性相同),if判断成功,e.value是Object类型的PRESENT对象(参考1,HashMap的Key保存的是HashSet的元素,HashMap的Value保存的是一个static
final Object PRESENT),也即,执行for 循环对原来的元素没有任何影响。s.add(new Name("abc", "456")); 语句本质上说没有起到任何作用。。。

再来分析下为什么 s.add(new Name("abc", "456")) 会返回 false,由于执行完HashMap的put方法的for循环后,HashMap的put方法返回一个oldValue,该oldValue
就是 Object PRESENT,不为null,导致HashSet的add方法返回了false。HashSet的add方法源码如下:

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息