Set 接口是 Java Collections Framework 中的一员,它的特色是:不能包含重复的元素,容许且最多只有一个 null 元素。Java 中有三个经常使用的 Set 实现类:html
JDK 在实现时,这 3 个 Set 集合的核心功能其实分别委托给了: HashMap, LinkedHashMap 和 TreeMap,关于这 3 个 Map 的源码分析可查看本站发布的其余文章。java
接下来对这 3 个 Set 集合的源码简单分析,并解决一些面试可能会遇到的问题。面试
若是去除注释,HashSet 源码也就 200 行左右,除了序列化和克隆的方法,代码以下:数组
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { // 实际存储元素的对象 private transient HashMap<E,Object> map; // 存储在 HashMap 中全部 key 的共享的 value 值 private static final Object PRESENT = new Object(); // 空构造函数 public HashSet() { map = new HashMap<>(); // 0.75f 加载因子 } // 使用已有集合填充并初始化 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); } // 只指定初始容量 public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } // 包访问权限的构造方法,仅用于 LinkedHashSet 初始化 // 使用 LinkedHashMap 做为底层存储 HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } // HashSet 中的元素就至关于 HashMap 中的 key public Iterator<E> iterator() { return map.keySet().iterator(); } // 如下这些方法,都是对 Set 接口中定义的方法的实现 public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } // 全部键值对的 value 值都是 PRESENT 这个 Object 对象 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(); } // JDK 8 提供的一种并行遍历机制 - 可分割迭代器 public Spliterator<E> spliterator() { return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0); } }
能够看到,底层使用 HashMap 用于实际存放数据,而 PRESENT 就是全部写入 map 的 value 值。实现比较简单,核心功能都委托给了 HashMap。缓存
不论是 Set 仍是 Map,存储的都是对象,在 Java 中,判断两个对象是否相等,都是经过 equals 和 hashCode 两个方法:安全
因此,HashSet 存储的对象,都要正确覆盖实现 equals 和 hashCode 两个方法。并发
其实,HashSet 中的元素其实就是 HashMap 的 key,在插入时:ide
关于迭代器,就是利用的 HashMap 中的 KeyIterator。函数
LinkedHashSet 的代码就更简单了,它继承自 HashSet,代码以下:源码分析
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { // 调用父类特定的构造方法,初始一个 LinkedHashMap public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } public LinkedHashSet() { super(16, .75f, true); } public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); } @Override public Spliterator<E> spliterator() { return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); } }
所有代码就这些,值得注意的是构造方法中的 super 调用的是 HashSet 中的一个默认包访问权限的构造方法,核心功能都委托给了 LinkedHashMap。
像 HashSet 那样,它能在常量时间内完成集合的基本操做 add, contains 和 remove。性能略低于 HashSet,由于要额外维护一个链表。但有一个例外,在遍历时,LinkedHashSet 花费的时间与元素个数成比例,而 HashSet 花费时间较多,由于它与集合容量成比例。
TreeSet 是一个有序的 Set 集合,元素大小比较方式能够是天然顺序,也能够指定一个 Comparator 比较器。
它是对 TreeMap 的封装,提供了在有序集合上的遍历 API 好比,lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素。能在 log(n) 时间内完成集合的基本操做 add, contains 和 remove。
有一点能够了解下,Set 接口定义的是使用 equals 方法比较元素是否相等,而 TreeSet 使用则是 compareTo 或者 compare 方法进行比较,这知足集合的行为,只不过没有遵照 Set 接口的规范。
TreeSet 源码也比较简单,毕竟只是对 TreeMap 封装了一下,这里再也不贴出。
以前分析了一部分经常使用集合的源码,这些集合都各有各的特色,它们的区别也常常出如今面试中,本文最后就对常见的面试题进行下总结。
fast-fail,即快速失败,在遍历集合的过程当中,若是发现集合结构发生了变化,会抛出 ConcurrentModificationException 运行时异常。
注意,在不一样步修改的状况下,它不能保证会发生,它只是尽力检测并发修改的错误。
原理是经过一个 modCount 字段来实现的,这个字段记录了列表结构的修改次数,当调用 iterator() 返回迭代器时,会缓存 modCount 当前的值,若是这个值发生了不指望的变化,那么就会在 next, remove 操做中抛出异常。
本文以及以前介绍的集合都是常规的,经常使用的,非线程安全的集合实现,接下来将会介绍 Java 并发包下的线程安全的集合,以及一些有特殊用途的集合实现。
原文出处:https://www.cnblogs.com/wskwbog/p/11260056.html