JAVA HashSet 原理分析
2015-10-24 21:23
666 查看
1,HashSet本质上是HashMap。它使用HashMap的Key来保存HashSet中存放的元素,而HashMap的Value则为一个final static 的Object对象PRESENT。其部分实现源码如下:
2,使用HashSet/HashMap存储对象时,如果该对象是自定义类型的对象,最好重写equals 方法和 hashCode 方法。
我们知道,对于HashSet而言,它不会存储重复的元素。先看下面一个示例:
现在我们来分析,为什么上面的HashSetTest中的语句输出false
为 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相同,但是它们是两个不同的对象(在不同的内存地址空间)。
看一个示例如下:
现在判断Name类对象相同的标准就是:如果两个Name对象的first属性相同,则称它们有相同的hashCode且equals返回true。
在HashSetTest2中,创建了两个Name对象,这两个对象存在于内存的不同地址空间中,但是它们的散列码hashCode是相同的。当欲将 new Name("abc", "456")添加到HashSet中去时,实质上是没有将该对象添加进去的。为什么呢?分析HashSet的add方法的源代码得知,它调用的是HashMap的put方法。HashMap的put方法如下:
接着,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 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; }
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 从源码安装Mysql/Percona 5.5
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c语言实现hashmap(转载)
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序