10.HashSet源码解析

1.类注释

  • HashSet的底层实现基于HashMap,因此迭代时不能保证按照插入顺序进行迭代。
  • HashSet的add、remove、contanins、size等方法的耗时性能,不会随着数据量的增加而增加,原因跟HashMap底层的数组数据结构有关,不管数据量多大,不考虑hash冲突的情况下,时间复杂度都是O(1)。
  • HashSet是线程不安全的,如果需要安全请自行加锁,或者使用 Collections的synchronizedSet方法。
  • 迭代过程中,如果数据结构被改变,会快速失败,会抛出 ConcurrentModificationException异常。

2.实现方式
从类注释中看到,HashSet的实现是基于HashMap的,在Java中,要基于基础类进行创新实现,有两种办法。一是继承基础类,覆写基础类的方法,比如说继承HashMap , 覆写其add 方法。另一种方法是组合基础类,通过调用基础类的方法,来复用基础类的能力。
HashSet使用的是后者,通过组合HashMap,来使用其提供的功能,其优点如下主要有两点。首先,继承表示父子类是同一个事物,而Set和Map表达的是两种事物,所以继承不妥,而且Java语法限制,子类只能继承一个父类,后续难以扩展。其次,组合更加灵活,可以任意的组合现有的基础类,并且可以在基础类方法的基础上进行扩展、编排等,而且方法命名相对灵活,无需和基础类的方法名称保持一致。具体实现源码如下所示。

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{
     
	//把HashMap组合进来,key是Hashset的key,value是下面的PRESENT
 	private transient HashMap<E,Object> map;
  	
  	//HashMap中的value
    private static final Object PRESENT = new Object();
}

源码解析

  1. 在使用HashSet时,比如add方法,只有一个入参,但组合的Map的add方法却有key和value两个入参,相对应上Map的key就是add的入参,value就是第二行代码中的 PRESENT,此处设计非常巧妙,用一个默认值PRESENT来代替Map的Value。
  2. 如果HashSet是被共享的,当多个线程访问的时候,就会有线程安全问题,因为后续的所有操作中,并没有加锁。

3.初始化
HashSet的初始化比较简单,直接new一下即可,当有原始集合数据进行初始化的情况下,会对HashMap的初始容量进行计算,源码如下。

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{
     
	//对HashMap的容量进行计算
	public HashSet(Collection<? extends E> c) {
     
    	map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    	addAll(c);
	}
	
	public boolean add(E e) {
     
    //直接使用HashMap的put方法,进行一些简单的逻辑判断
    	return map.put(e, PRESENT)==null;
	}
}

源码解析

  1. Math.max((int)(c.size()/.75f)+1,16),就是对HashMap的容量进行了计算,翻译成中文就是取括号中两个数的最大值(期望的值/0.75+1,默认值16),从计算中可以看出HashSet的实现者对HashMap的底层实现是非常清楚的,主要体现在两个方面,和16比较大小的意思是说,如果给定HashMap初始容量小于16 ,就按照HashMap默认的16初始化即可,如果大于 16,就按照给定值初始化。
    HashMap扩容阈值的计算公式是Map的容量 0.75f,一旦达到阀值就会扩容,此处用(int) (c.size()/.75f)+1来表示初始化的值,这样使期望的大小值正好比扩容的阀值还大1,就不会扩容,符合HashMap扩容的公式。
  2. HashSet的其他方法就相对简单了,就是对Map的api进行了一些包装,如上述的add方法。

你可能感兴趣的:(从头开始学Java源码)