Java中HashSet和LinkedHashSet详解
作者:给你两窝窝
一、HashSet介绍
HashSet是Set接口的子类,其内部采用了HashMap作为数据存储,HashSet其实就是在操作HashMap的key。
- HashSet是无序存储的,不能保证元素的顺序;
- HashSet并没有进行同步处理,因此是线程不安全的;
- HashSet可以存储null元素,但只能存储一个。
二、源码解析
1、HashSet实现的接口
如下图:
观察上图:
- AbstractSet类:该类提供了Set接口的骨架实现,通过扩展此类来实现集合的过程与通过扩展AbstractCollection实现集合的过程相同,除了此类的子类中的所有方法和构造函数都必须遵守由Set接口施加的附加约束(例如,添加方法不能允许将一个对象的多个实例添加到集合中)。
- Set接口:继承Collection接口,添加了所有构造函数的约定以及add,equals和hashCode方法的约定
- Serializable接口:主要用于序列化,即:能够将对象写入磁盘。与之对应的还有反序列化操作,就是将对象从磁盘中读取出来。因此如果要进行序列化和反序列化,ArrayList的实例对象就必须实现这个接口,否则在实例化的时候程序会报错(java.io.NotSerializableException)。
- Cloneable接口:实现Cloneable接口的类能够调用clone方法,如果没有实现Cloneable接口就调用方法,就会抛出异常(java.lang.CloneNotSupportedException)。
2、HashSet中的变量
序列化ID
static final long serialVersionUID = -5024744406713321676L
底层使用HashMap来保存所有元素,确切说存储在map的key中,并使用transient关键字修饰,防止被序列化
private transient HashMap<E,Object> map
常量,构造一个虚拟的对象PRESENT,默认为map的value值(HashSet中只需要用到键,而HashMap是key-value键值对,使用PRESENT作为value的默认填充值,解决差异问题)
private static final Object PRESENT = new Object()
3、HashSet的构造方法
(1)无参构造
public HashSet() { map = new HashMap<>(); }
总结:默认的无参构造,其底层会初始化一个空的HashMap,并使用默认初始容量16和负载因子0.75。
(2)带集合参数的构造方法
public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); }
总结:带集合参数的构造方法,底层使用默认的负载因子0.75和足以包含指定集合中所有怨怒是的初始容量来构造一个HashMap。
(3)带初始容量的构造方法
public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); }
总结:以指定初始容量的HashMap构造一个HashSet。
(4) 带初始容量和负载因子的构造方法
public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); }
总结:构造一个具有初始容量和负载因子的HashMap。
4、常用方法分析
(1)add()方法
add方法底层实际上是将该元素作为key添加到HashMap中,而PRESENT是作为默认的map的value值。
如果添加的元素不存在,则加入到map中,并返回true。如果该元素已经存在,则返回false。
map的put方法在添加key-value对时,如果新放入HashMap的Entry中key与集合中原有的Entry的key相同,则新添加的Entry的value会覆盖原来Entry的value,但是key不会改变。
因此,如果向HashSet中添加一个已经存在的元素时,新添加的集合元素不会被放入HashMap中,原有的元素也不会有任何改变,因此Set中的元素也就不会重复了
public boolean add(E e) { return map.put(e, PRESENT)==null; }
(2)remove()方法
如果给定的元素在HashSet中,则将其移除,其底层调用HashMap的remove()方法删除指定Entry。
public boolean remove(Object o) { return map.remove(o)==PRESENT; }
(3) size()方法
返回HashSet中元素的个数
public int size() { return map.size(); }
(4)isEmpty()方法
判断HashSet是否为空
public boolean isEmpty() { return map.isEmpty(); }
(5)contains()方法
如果HashSet中包含指定元素,则返回true。否则返回false
public boolean contains(Object o) { return map.containsKey(o); }
5、HashSet与LinkedHashSet
在HashSet的源码中有这样一个构造函数:
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
可以看到该构造函数为包访问权限,不对外公开。该构造函数以指定的初始容量和负载因子构造一个新的空链表哈希集合,其实它是对LinkedHashSet的支持。
我们可以看看LinkedHashSet的继承关系:
因此,在这里我们可以简单的对LinkedHashSet进行分析,它在实现了Set接口、Cloneable接口和Serializable接口的同时,也继承了HashSet。
LinkedHashSet的特点如下:
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个数组+双向链表
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
- LinkedHashSet 不允许添重复元素
来看看LinkedHashSet的构造方法
//使用指定的初始容量和负载因子构造一个新的哈希集合 public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } //使用指定的初始容量和负载因子构造新的哈希集合 public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } //无参构造方法,默认容量16,负载因子0.75 public LinkedHashSet() { super(16, .75f, true); } //基于集合构造一个带有指定元素的非空哈希集合 public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); }
LinkedHashSet维护了一个hash表和双向链表,且通过head和tail分别指向链表的头和尾,每一个节点有before和after属性,这样可以形成双向链表。
在添加一个元素时,先求hash值,再求索引,确定该元素在table表中的位置,然后将添加的元素加入到双向链表(如果该元素已经存在,则不添加)
三、总结
1、HashSet总结
(1)基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
(2)当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。
(3)HashSet的其他操作都是基于HashMap的。
2、LinkedHashSet总结
LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承于 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
到此这篇关于Java中HashSet和LinkedHashSet详解的文章就介绍到这了,更多相关HashSet和LinkedHashSet内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!