Java集合-HashSet

前言

为什么会有HashSet,其特点是什么?解决了什么问题?接下来我们就学习HashSet,以及其源码。

上帝视角

因为HashSet的实现方式是使用HashMap,其最大特点就是无序不可重复。在下面源码中可以看到,HashSet操作的都是new出的"HashMap"类型的成员变量map。在HashSet存值得过程中是允许为null的,和HashMap一样是非同步的

 extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

源码分析

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
	//序列版本号
    static final long serialVersionUID = -5024744406713321676L;
	//不需要被序列化的map变量
    private transient HashMap<E,Object> map;
    
    // 与支持映射中的对象关联的虚拟值
    private static final Object PRESENT = new Object();

     //创建一个map实例,Map初始大小为16,负载因子为0.75
    public HashSet() {
        map = new HashMap<>();
    }

    //创建一个HashMap,初始容量为Math.max((int) (c.size()/.75f) + 1, 16)
    //为什么是(int) (c.size()/.75f) + 1和16中取一个;这里要知道Hashmap扩容机制,Hashmap中的容量达到阈值就扩容为原来的两倍,阈值=宿数组容量*0.75;
    //(c.size()/.75f) + 1,反过来就是容纳这个数据的总长度
    //使用16是因为初始容量必须是2的指数倍数,当int) (c.size()/.75f) + 1得到的不是一个2的指数倍数,hashMap会重新计算一个符合条件的初始长度,这里设置16算是一个优化
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    //创建一个指定长度和负载因子的HashMap
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

boolean add(E e)

 public boolean add(E e) {
 //因为hashMap的put()方法成功的情况下返回null,如果hashMap返回null那么HashSet的add方法就成功了
        return map.put(e, PRESENT)==null;
    }
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
...
  	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1 判断当前table是否为空,第一次会为空在resize()中就会创建一tablle,下次就不为空
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
// 2 (n - 1) & hash 计算的到一个数组下标i,取出tab[]数组中i 的值p,如果为空,就直接在tab[i]上进行赋值
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
//3 如果当前下标i上已经有值,就可能产生哈希冲突或者是相同元素,
        Node<K,V> e; K k;
//4 如果hash相等,并且key相等或者key.eques相等,则说明是重复元素,直接覆盖
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
//5 如果不是同一元素,产生冲突前判断p节点是否是树结构
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
//6 这里就是说不是树,但是产生了hash冲突,开始要循环遍历树了
            for (int binCount = 0; ; ++binCount) {
//7 首先判断p的next节点是否有数据
                if ((e = p.next) == null) {
//8 没有数据直接将新数据添加到p的next节点上
                    p.next = newNode(hash, key, value, null);
//9 当链表长度大于8就转成红黑树处理
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
//10 如果可key存在就直接覆盖
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
//11 一直循环到p.next 为空,或者有key存在就跳出循环
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);//这里的方法使用使用多态,如果是LinkedHashMap就会调用其相关代码
            return oldValue;
        }
    }
    ++modCount;
//12 如果超过最大容量threshold就扩容,threshold在初始化的时候是没有赋值的,在第1步resize()扩容的时候进行了赋值
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);//这里的方法使用使用多态,如果是LinkedHashMap就会调用其相关代码
    return null;
}
...    
 }

boolean remove(Object o)

  public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
   ...
   public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

	final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //如果添加过数据table就是有值的
        //首先判断table不为空并且tab长度>0,并且tab[index]下标的值不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //tab[index]的p.hash相等,key相等,或者key不为空且equals相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //获得了对应的值,可以删除了
                node = p;
            else if ((e = p.next) != null) {//比如上面hash相同key不同,就是冲突了。冲突后,下面判断是否已经是红黑树
                if (p instanceof TreeNode)
                //是树,取出值
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {     
                    do {
                     //不是,还是根据hash和key同时相等,或者key不为空,key的equals相等,取出值
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                        //循环到e.next不为空,找到后跳出
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //如果是树
                if (node instanceof TreeNode)
                //走删除树逻辑
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)//不是树,判是否为空
                    tab[index] = node.next;//设置空
                else
                    p.next = node.next;//将关联进行交换
                ++modCount;
                --size;//hashMap的长度减少
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
    ...
 }   

iterator()

HashSet的迭代器只使用HashMap的key,在 KeySet类的iterator()方法中调用KeyIteratornext()方法返回nextNode().key

 public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
   ...

   public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }


	final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        //具体返回调用KeyIterator的方法,其next返回的是key
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (Node<K,V> e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

  final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

 ...
 }

重写equal()方法

这里提到重写equal()方法,是基于一个场景,当我们将两个对象存储到HashSet中时,对于数据的唯一性,同一个类生成的对象,如果具有相同的属性,那么我们希望HashSet只存储一个,但结果可能和我们想的不一样,看下面代码

	    MyBean m = new MyBean(1, 1, "2");
		MyBean m2 = new MyBean(1, 1, "2");
		HashSet h = new HashSet();
		h.add(m);
		h.add(m2);
		System.out.println(h.size());

对于对象m和m2,我们认为是同一数据,但是HashSet并不能区分会保存两个,这里要看一下hashMap判读重复数据的方法(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) ,这里要先说明下==号对比和equals对比;

  • ==:等号比较的是两个对象的内存地址是否相等,也就是比较的是否是一个对象
  • equals:比较的是两个对象是否相等,但对于equals方法实现方式分为两种情况

1、没有重写equals()方法,使用的是父类Object的方法,比较对象使用==对比
2、如String类重写equals()方法,会比较两个对象相关属性值;
Object类中的equals方法

  public boolean equals(Object obj) {
        return (this == obj);
    }

回到HashMap的方法,或方法两边一边使用((k = e.key) == key判断对象相等,一边使用key.equals(k)比较,显然在 m和m2对象不相等,MyBean类没有实现重写equals情况下key.equals(k)也是不相等的。所以HashSet会添加两个数据;

要想保证只能HashSet能够区分,我们需要在MyBean模型中重写equals()方法,判断对象的值是否相等;在此我们需要了解HashCode的协定

1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠。
2.hashCode()相等的两个对象他们的equal()不一定相等,就是hashCode()不是绝对可靠。

这说明在重新MyBean的equal()方法的时候需要重写hashCode()方法;
重写后的MyBean()如下:

public class MyBean {

	@MyAnnotation(age = 10, value = 20)
	private int value;

	@MyAnnotation(age = 40, value = 30)
	private int age;

	@MyAnnotations(name = "张珊")
	private String name;

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public MyBean(int value, int age, String name) {
		super();
		this.value = value;
		this.age = age;
		this.name = name;
	}

	public MyBean() {
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}

		// 如果是同一个对象返回true,反之返回false
		if (this == obj) {
			return true;
		}

		// 判断是否类型相同
		if (this.getClass() != obj.getClass()) {
			return false;
		}

		MyBean person = (MyBean) obj;
		return name.equals(person.name) && age == person.age && value == person.value;
	}

	@Override
	public int hashCode() {
		int nameHash = name.toUpperCase().hashCode();
		return nameHash ^ age;
	}
}

对于重写hashCode()是是一个技术活,重写的好坏会影响在Hash表中的性能,可以参考String类的hashCode()方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值