zoukankan      html  css  js  c++  java
  • 这 3 个 Set 集合的实现有点简单,那来做个总结吧

    Set 接口是 Java Collections Framework 中的一员,它的特点是:不能包含重复的元素,允许且最多只有一个 null 元素。Java 中有三个常用的 Set 实现类:

    • HashSet: 将元素存储在哈希表中,性能最佳,但不能保证元素的迭代顺序
    • LinkedHashSet: 维护一个链表贯穿所有元素,按插入顺序对元素进行迭代
    • TreeSet: 将元素存储在一个红黑树中,按元素大小排序的序列迭代

    JDK 在实现时,这 3 个 Set 集合的核心功能其实分别委托给了: HashMap, LinkedHashMap 和 TreeMap,关于这 3 个 Map 的源码分析可查看本站发布的其他文章。

    接下来对这 3 个 Set 集合的源码简单分析,并解决一些面试可能会遇到的问题。

    HashSet

    如果去除注释,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 中,判断两个对象是否相等,都是通过 equalshashCode 两个方法:

    • 两个对象通过 equals 判断相等,那么它们肯定返回相同的 hashCode
    • 反之,不要求必须拥有相同的 hashCode

    所以,HashSet 存储的对象,都要正确覆盖实现 equalshashCode 两个方法。

    其实,HashSet 中的元素其实就是 HashMap 的 key,在插入时:

    1. 首先计算元素的 hashCode 值,找到底层数组存储位置
    2. 然后和该位置上的所有元素使用 equals 方法进行比较
    3. 如果都不相等,则插入;否则不插入,本质上这里做了一次 value 的更新,但 key 不变化。

    关于迭代器,就是利用的 HashMap 中的 KeyIterator。

    LinkedHashSet

    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

    TreeSet 是一个有序的 Set 集合,元素大小比较方式可以是自然顺序,也可以指定一个 Comparator 比较器。

    它是对 TreeMap 的封装,提供了在有序集合上的遍历 API 比如,lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素。能在 log(n) 时间内完成集合的基本操作 add, contains 和 remove。

    有一点可以了解下,Set 接口定义的是使用 equals 方法比较元素是否相等,而 TreeSet 使用则是 compareTo 或者 compare 方法进行比较,这满足集合的行为,只不过没有遵守 Set 接口的规范。

    TreeSet 源码也比较简单,毕竟只是对 TreeMap 封装了一下,这里不再贴出。

    常用集合面试问题总结

    之前分析了一部分常用集合的源码,这些集合都各有各的特点,它们的区别也经常出现在面试中,本文最后就对常见的面试题进行下总结。

    ArrayList 与 LinkedList 有什么区别?

    • 存储结构不同,ArrayList 底层使用数组;LinkedList 使用双向链表
    • 性能上,ArrayList 能够随机访问,但增加和删除效率较慢,涉及到内存拷贝;LinkedList 只能顺序或逆序访问,占用内存稍大,但插入删除效率高
    • LinkedList 还能当做栈和队列来使用
    • 两者均与允许存储 null 也允许存储重复元素
    • 两者都是线程不安全的,都可以使用 Collections.synchronizedList(List list) 方法生成一个线程安全的 List

    ArrayList 与 Vector 有什么区别?

    • ArrayList 非线程安全,Vector 线程安全
    • 扩容时,ArrayList 增加 1.5 倍的容量 ; Vector 增加 2倍的容量

    JDK 8 对 HashMap 做了哪些优化?

    • 底层结构改为单链表 + 数组 + 红黑树的存储结构,在有大量哈希冲突时,将查询时间复杂度从 O(n) 降为 O(log(n))
    • 优化哈希函数,将 1.7 中的4次位运算 + 5次异或运算,降低到1次位运算 + 1次异或运算
    • 优化扩容机制,1.7 中会重新哈希计算新的位置,而 1.8 则是根据2的次幂扩展机制,不重新计算位置,只根据原散列值计算偏移量,要么位置不变,要么偏移旧数组容量的偏移量

    HashMap 和 HashTable 的区别

    • HashMap 线程不安全 ; HashTable 线程安全
    • HashMap 允许 key 和 Vale 为 null ; HashTable 不允许 key、value 为 null
    • HashMap 默认容量为 2^4 且容量一定是 2^n ; HashTable 默认容量是11(素数), 不一定是 2^n
    • HashTable 直接使用模运算计算哈希桶下标 ; HashMap 使用 & 位运算 进行优化

    HashMap 和 LinkedHashMap 的区别

    • LinkedHashMap 继承自 HashMap 它们有相同的存储结构和扩容机制
    • LinkedHashMap 内部需要额外维护一个链表
    • LinkedHashMap 按插入顺序对元素进行迭代 ; 而 HashMap 迭代顺序不可预测
    • LinkedHashMap 可按按访问顺序遍历元素,用于构建 LRU 缓存

    什么是 fast-fail,原理是什么?

    fast-fail,即快速失败,在遍历集合的过程中,如果发现集合结构发生了变化,会抛出 ConcurrentModificationException 运行时异常。

    注意,在不同步修改的情况下,它不能保证会发生,它只是尽力检测并发修改的错误。

    原理是通过一个 modCount 字段来实现的,这个字段记录了列表结构的修改次数,当调用 iterator() 返回迭代器时,会缓存 modCount 当前的值,如果这个值发生了不期望的变化,那么就会在 next, remove 操作中抛出异常。

    小结

    本文以及之前介绍的集合都是常规的,常用的,非线程安全的集合实现,接下来将会介绍 Java 并发包下的线程安全的集合,以及一些有特殊用途的集合实现。

  • 相关阅读:
    Linux内核网络协议栈优化总纲
    Java实现 蓝桥杯VIP 算法训练 连续正整数的和
    Java实现 蓝桥杯VIP 算法训练 连续正整数的和
    Java实现 蓝桥杯VIP 算法训练 寂寞的数
    Java实现 蓝桥杯VIP 算法训练 寂寞的数
    Java实现 蓝桥杯VIP 算法训练 学做菜
    Java实现 蓝桥杯VIP 算法训练 学做菜
    Java实现 蓝桥杯VIP 算法训练 判断字符位置
    Java实现 蓝桥杯VIP 算法训练 判断字符位置
    Java实现 蓝桥杯VIP 算法训练 链表数据求和操作
  • 原文地址:https://www.cnblogs.com/chuonye/p/11260056.html
Copyright © 2011-2022 走看看