HashSet和HashMap分析

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;
	}
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return  Id + ":" + name;
	}
	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		return this.name.hashCode()^this.Id.hashCode();
	}
	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		if (obj instanceof Person ) {
			Person p = (Person)obj;
			if (this.Id == p.Id || (this.Id != null && this.Id.equals(p.Id)))
				return true;
			else
				return 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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值