HashSet和HashMap分析

来源:互联网 发布:python中迭代器 编辑:程序博客网 时间:2024/06/13 00:42

HashSet

HashSet背后主要是一个HashMap在支持。HashSet的元素都作为HashMap每一对key-valueKey来存储,每个KeyValue都等于PRESENT

以下是HashSet的部分源代码:

public class HashSet<E>    extends AbstractSet<E>    implements Set<E>, Cloneable, java.io.Serializable{    static final long serialVersionUID = -5024744406713321676L;    private transient HashMap<E,Object> map;    // Dummy value to associate with an Object in the backing Map    private static final Object PRESENT = new Object();    public HashSet() {        map = new HashMap<>();    }    public HashSet(Collection<? extends E> c) {        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));        addAll(c);    }    public HashSet(int initialCapacity, float loadFactor) {        map = new HashMap<>(initialCapacity, loadFactor);    }    public HashSet(int initialCapacity) {        map = new HashMap<>(initialCapacity);    }    HashSet(int initialCapacity, float loadFactor, boolean dummy) {        map = new LinkedHashMap<>(initialCapacity, loadFactor);    }    public Iterator<E> iterator() {        return map.keySet().iterator();    }     public int size() {        return map.size();    }        public boolean isEmpty() {        return map.isEmpty();    }    public boolean contains(Object o) {        return map.containsKey(o);    }    public boolean add(E e) {        return map.put(e, PRESENT)==null;    }       public boolean remove(Object o) {        return map.remove(o)==PRESENT;    }    public void clear() {        map.clear();    }}

有上面的源码可以看出HashSet的相关操作都是HashMap的操作,元素不重复主要是通过HashMap来实现。因为HashMapKey是不允许重复的,所以就保证了HashSet的元素不重复。那么这里对重复的判断是怎样实现的?那就得看看HashMap的实现。

HashMap:

内部主要有一个Entry<K,V>类型的数组table,即

    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16    static final int MAXIMUM_CAPACITY = 1 << 30;    static final float DEFAULT_LOAD_FACTOR = 0.75f;    static final Entry<?,?>[] EMPTY_TABLE = {};    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;    transient int size;    int threshold;    final float loadFactor;     transient int modCount;    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

实际上java容器都是自动增长类型的数据结构,他们实现自动增长的方式都类似,都是通过capacityloadFactor来动态调整容器大小。当达到loadfactor的数据比例时候,容器申请2更大的空间,然后将原来的数据拷贝到新空间中。但是当数据量变小,并不会自动缩小。因此对于提前预知数据量很大的时候,可以直接先设置capacity的初始值大一点,以防止自动增长时候内存拷贝的开销。如果提前预知数据量很小,那么就不需要设置很大的capacity以免浪费内存。


table数组的类型是Entry<K,V>我们来看看这是什么数据结构。

以下是Entry的部分源代码:

static class Entry<K,V> implements Map.Entry<K,V> {        final K key;        V value;        Entry<K,V> next;        int hash;        Entry(int h, K k, V v, Entry<K,V> n) {            value = v;            next = n;            key = k;            hash = h;        }        public final boolean equals(Object o) {            if (!(o instanceof Map.Entry))                return false;            Map.Entry e = (Map.Entry)o;            Object k1 = getKey();            Object k2 = e.getKey();            if (k1 == k2 || (k1 != null && k1.equals(k2))) {                Object v1 = getValue();                Object v2 = e.getValue();                if (v1 == v2 || (v1 != null && v1.equals(v2)))                    return true;            }            return false;        }        public final int hashCode() {            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());        }        public final String toString() {            return getKey() + "=" + getValue();        }        void recordAccess(HashMap<K,V> m) {        }        void recordRemoval(HashMap<K,V> m) {        }    }

Entry是由final Kkey;value;Entry<K,V>next;int hash;组成的。其实就是一个链表的结点,数据域有hash,key,value,指针域是next(当然java中是引用)。也就是HashMap是一个数组链表法解决hash冲突实现的hash结构。这里的keyfinal类型,是不可以改变的;也就是说一旦你put一个新key,那么他就不能再改变指向了。



Entryequals方法已经被重写了,当且仅当两个对象都是Entry对象且keyvalue同时相等时才相等。

hashCode方法也被重写成keyvaluehashCode异或值。这是为什么呢?似乎HashMap并没有对Entry的比较,HashMap比较的都是Entry.keyEntry.hash可能有的代码通过HashMap.entrySet()方法得到Entry集合,需要比较里面的EntryHashMap.Entry是接口Map.Entry的实现类,需要重写这两个方法以便确保两个Entry的正确比较。


知道了table的结构,就来看看主要操作putremove的实现,这里只介绍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) {            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;    }final int hash(Object k) {        int h = hashSeed;        if (0 != h && k instanceof String) {            return sun.misc.Hashing.stringHash32((String) k);        }        h ^= k.hashCode();        // This function ensures that hashCodes that differ only by        // constant multiples at each bit position have a bounded        // number of collisions (approximately 8 at default load factor).        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }    private V putForNullKey(V value) {/*这里之所以是循环,是因为可能还有其他非空key也会映射到0地址处*/        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            if (e.key == null) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(0, null, value, 0);        return null;    }    private void putForCreate(K key, V value) {        int hash = null == key ? 0 : hash(key);        int i = indexFor(hash, table.length);        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            if (e.hash == hash &&                ((k = e.key) == key || (key != null && key.equals(k)))) {                e.value = value;                return;            }        }        createEntry(hash, key, value, i);    }void addEntry(int hash, K key, V value, int bucketIndex) {        if ((size >= threshold) && (null != table[bucketIndex])) {            resize(2 * table.length);            hash = (null != key) ? hash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }        createEntry(hash, key, value, bucketIndex);    }/*头插法,先保存头table[bucketIndex],在将新Entry的next域指向为table[bucketIndex],最后将头指向新Entry*/void createEntry(int hash, K key, V value, int bucketIndex) {        Entry<K,V> e = table[bucketIndex];        table[bucketIndex] = new Entry<>(hash, key, value, e);        size++;    }


如果table还是空的,如果第一次调用put,这时候table首先会生成一个16个元素大小的hash表。也就是调用时才申请空间copy on write

然后分以下情况:

a.key不是空且keynull,则放入null。这里说明HashMap支持nullkey。并且所有的nullkey都放在table0地址中。这也说明HashSet可以存放一个null元素。HashMap也只能存放一个nullkeymap,再次存入肯定value会被覆盖。

b.以上情况都不是,那么计算keyhash值。每个对象的hashCode方法默认都是本地方法。其实本地方法hashCode返回的就是对象的地址值。hashMap里面的hash函数,实际上是分两种情况处理, 1.String对象,那么直接sun.misc.Hashing.stringHash32((String)k)2.其他对象,先算出hashCode,然后再映射到table数组下标。然后搜索是否存在e使得e.hash==hashe.key==key同时成立。这里可以看出,即使是不同的key,也有可能最终得到相同的index。如果存在,那么修改value即可,不存在则生成一个新的entry加入。因此如果想保证hashSet或者HashMap只放入内容不重复的元素,必须同时重写hashCode方法和equals方法。

通过返回关注内容的hashCode和比较关注内容(这里关注内容可以是对象的某些属性)重新定义hashCodeequals方法。

关于头插法的示意图:



下面是一个小测试代码:

package test;import java.util.HashMap;import java.util.HashSet;import java.util.Iterator;import java.util.Map;import java.util.Set;class Person {String Id;String name;Person(String id, String name){this.Id = id;this.name = name;}@Overridepublic String toString() {// TODO Auto-generated method stubreturn  Id + ":" + name;}@Overridepublic int hashCode() {// TODO Auto-generated method stubreturn this.name.hashCode()^this.Id.hashCode();}@Overridepublic boolean equals(Object obj) {// TODO Auto-generated method stubif (obj instanceof Person ) {Person p = (Person)obj;if (this.Id == p.Id || (this.Id != null && this.Id.equals(p.Id)))return true;elsereturn false;}return false;}}public class JavaFscan {public static void main(String args[]){Map<Person,String> m = new HashMap<Person,String>();Set<Person> s = new HashSet<Person>();Person p1 = new Person("1", "ki");Person p2 = new Person("2", "ki");Person p3 = p1;Person p4 = new Person("4", "qi");s.add(p1);s.add(p2);s.add(p3);s.add(p4);//s.add(null);//m.put(null,null);//m.put(null, "si");//m.put(null, "ti");m.put(p1, "1");m.put(p2, "2");m.put(p3, "3");Iterator<Person> its = s.iterator();Set<Map.Entry<Person, String>> sets = m.entrySet();Iterator<Map.Entry<Person, String>> itm = sets.iterator();while(its.hasNext()){Person p = its.next();p.Id= "1";}while(itm.hasNext()){Map.Entry<Person, String> e = itm.next();Person pp = e.getKey();pp.Id = "1";}System.out.println(m.get(p1));System.out.println(s);System.out.println(m);}}


总结:

1.HashSet可以支持null元素,但最多放一个,HashSet不支持重复元素,因为元素是内部HashMap的Key;
2.HashSet由HashMap支持,HashMap支持null,但最多只能有一个NULL Key map;

3.判断HashSet或者HashMap元素重复,可以重写元素的hashCode和equals方法,比较需要关心的内容;

4.不要试图修改HashSet里面对象元素的内容,这样可能会导致修改后和其中一个已经存在的元素相等的情况,从而造成下次查询HashMap不知道返回哪一个;

0 0
原创粉丝点击