写在文章开头
又到了每年的面试旺季,看到很多读者反馈希望笔者可以根据近几年的面试对这篇文章进行补充迭代,所以笔者结合各大网站的面试题型对这篇文章进行进一步的补充和迭代。

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,实时获取笔者最新的技术推文同时还能和笔者进行深入交流。
Java集合概述
从Java顶层设计角度分类而言,集合整体可分为两大类型:
- 存放单元素的Collection。
- 存放键值对的Map类型。
Collection包的整体架构如下图,根据存储的特点我们又可以分为:
- 有序不重复的Set。
- 有序可重复的LIst。
- FIFO的队列类型- Queue。

另一大类就是映射集,他的特点就是每一个元素都是由键值对组成,我们可以通过key找到对应的value,类图如下,集合具体详情笔者会在后文阐述。

List集合详解
List集合概览
List即顺序表,它是按照插入顺序存储的,元素可以重复。从底层结构角度,顺序表还可以分为以下两种:
- ArrayList:- ArrayList实现顺序表的选用的底层结构为数组,以下便是笔者从- ArrayList源码找到的list底层存储元素的变量- elementData,可以看到- ArrayList本质上就是对数组的封装。
            
            
              java
              
              
            
          
          transient Object[] elementData;- LinkedList: 顺序链表底层是- 双向链表,由一个个节点构成,节点有双指针,分别指向前驱节点和后继节点。
            
            
              java
              
              
            
          
          private static class Node<E> {
        E item;
        // 指向后继节点
        Node<E> next;
        //指向前驱节点
        Node<E> prev;
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }- Vector:底层同样使用的是数组与ArrayList的区别是它在操作时是有上锁的,所以在多线程场景下它是可以保证线程安全的,但- vector现在基本不用了,这里仅仅做个了解,它底层用的也是数组。
            
            
              java
              
              
            
          
           protected Object[] elementData;ArrayList容量是10,给它添加一个元素会发生什么?
我们不妨看看这样一段代码,可以看到我们将集合容量设置为10,第11次添加元素时,由于list底层使用的数组已满,会进行动态扩容,这个动态扩容说白了就是创建一个更大的容器将原本的元素拷贝过去,我们不妨基于下面的代码进行debug一下
            
            
              java
              
              
            
          
          ArrayList<Integer> arrayList=new ArrayList<>(10);
      for (int i = 0; i < 10; i++) {
         arrayList.add(i);
      }
      arrayList.add(10);add源码如下,可以看到在添加元素前会对容量进行判断
            
            
              java
              
              
            
          
          public boolean add(E e) {
      //判断本次插入位置是否大于容量
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }步入ensureCapacityInternal,会看到它调用ensureExplicitCapacity,它的逻辑就是判断当前插入元素后的最小容量是否大于数组容量,如果大于的话会直接调用动态扩容方法grow。
            
            
              java
              
              
            
          
          private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        //如果插入的元素位置大于数组位置,则会进行动态扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }可以看到扩容的逻辑很简单创建一个新容器大小为原来的1.5倍,然后再将原数组元素拷贝到新容器中:
            
            
              java
              
              
            
          
          private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //创建一个新容器大小为原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
       ....略去细节
       //将原数组元素拷贝到新容器中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }针对动态扩容导致的性能问题,你有什么解决办法嘛?
我们可以提前调用ensureCapacity顶下最终容量一次性完成动态扩容提高程序执行性能。
            
            
              java
              
              
            
          
          public static void main(String[] args) {
        int size = 1000_0000;
        ArrayList<Integer> list = new ArrayList<>(1);
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.add(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("无显示扩容,完成时间:" + (end - start));
        //显示扩容显示扩容,避免多次动态扩容的拷贝
        ArrayList<Integer> list2 = new ArrayList<>(size);
        start = System.currentTimeMillis();
        list2.ensureCapacity(size);
        for (int i = 0; i < size; i++) {
            list2.add(i);
        }
        end = System.currentTimeMillis();
        System.out.println("显示扩容,完成时间:" + (end - start));
    }输出结果如下,可以看到在显示指明大小空间的情况下,性能要优于常规插入:
            
            
              bash
              
              
            
          
          无显示扩容,完成时间:6122
显示扩容,完成时间:761ArrayList和LinkedList性能差异体现在哪
我们给出头插法的示例代码:
            
            
              java
              
              
            
          
          public static void main(String[] args) {
        int size = 10_0000;
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            arrayList.add(0, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("arrayList头插时长:" + (end - start));
        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            linkedList.add(0, i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList 头插时长:" + (end - start));
        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            ((LinkedList<Integer>) linkedList).addFirst(i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList addFirst 耗时:" + (end - start));
    }从性能表现上来看arrayList表现最差,而linkedList 的addFirst 表现最出色。
            
            
              bash
              
              
            
          
          arrayList头插时长:562
linkedList 头插时长:8
linkedList addFirst 耗时:4这里我们不妨说一下原因,arrayList性能差原因很明显,每次头部插入都需要挪动整个数组,linkedList的add方法在进行插入时,若是头插法,它会通过node方法定位头节点,然后在使用linkBefore完成头插法。
            
            
              java
              
              
            
          
           public void add(int index, E element) {
        checkPositionIndex(index);
        if (index == size)
            linkLast(element);
        else
           //通过node定位到头节点,再进行插入操作
            linkBefore(element, node(index));
    }而addFirst 就不一样,它直接定位到头节点,进行头插法,正是这一点点性能上的差距造成两者性能表现上微小的差异。
            
            
              java
              
              
            
          
          private void linkFirst(E e) {
      //直接定位到头节点,进行头插法
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }再来看看尾插法:
            
            
              java
              
              
            
          
          public static void main(String[] args) {
        int size = 10_0000;
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            arrayList.add(i, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("arrayList 尾插时长:" + (end - start));
        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            linkedList.add(i, i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList 尾插时长:" + (end - start));
        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            ((LinkedList<Integer>) linkedList).addLast(i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList 尾插时长:" + (end - start));
    }输出结果,可以看到还是链表稍快一些,为什么arraylist这里性能也还不错呢?原因也很简单,无需为了插入一个节点维护其他位置。
            
            
              bash
              
              
            
          
          arrayList 尾插时长:5
linkedList 尾插时长:2
linkedList 尾插时长:3最后再来看看随机插入,为了公平实验,笔者将list初始化工作都放在计时之外,避免arrayList动态扩容的时间影响最终实验结果:
            
            
              java
              
              
            
          
          public static void main(String[] args) {
        int size = 10_0000;
        //填充足够量的数据
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            arrayList.add(i);
        }
        //随机插入
        long begin = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            arrayList.add(RandomUtil.randomInt(0, size), RandomUtil.randomInt());
        }
        long end = System.currentTimeMillis();
        System.out.println("arrayList随机插入耗时:" + (end - begin));
        //填充数据
        LinkedList<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            linkedList.add(i);
        }
        //随机插入
        begin = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            linkedList.add(RandomUtil.randomInt(0, size), RandomUtil.randomInt());
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList随机插入耗时:" + (end - begin));
    }从输出结果来看,随机插入也是arrayList性能较好,原因也很简单,arraylist随机访问速度远远快与linklist:
            
            
              makefile
              
              
            
          
          arrayList随机插入耗时:748
linkedList随机插入耗时:27741ArrayList 和 Vector 的异同
这个问题我们可以从以下几个维度分析:
- 底层数据结构:两者底层存储都是采用数组,ArrayList存储用的是new Object[initialCapacity];
            
            
              java
              
              
            
          
          public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }Vector底层存储元素用的也是是 new Object[initialCapacity];,即一个对象数组:
            
            
              java
              
              
            
          
          public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }- 线程安全性:Vector为线程安全类,ArrayList线程不安全,如下所示我们使用ArrayList进行多线程插入出现的索引越界问题。
            
            
              java
              
              
            
          
          public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        Thread.sleep(5000);
        System.out.println(list.size());
    }因为多线程访问的原因,底层索引不安全操作的自增,导致插入时得到一个错误的索引位置从而导致插入失败:
            
            
              java
              
              
            
          
          Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 823
   at java.util.ArrayList.add(ArrayList.java:463)
   at com.sharkChili.Main.lambda$main$0(Main.java:15)
   at java.lang.Thread.run(Thread.java:748)Vector 线程安全代码示例:
            
            
              java
              
              
            
          
           public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new Vector<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        Thread.sleep(5000);
        System.out.println(list.size());//2000
    }原因很简单,vector的add方法有加synchronized 关键字
            
            
              java
              
              
            
          
          //任何操作都是上锁的,保证一次插入操作互斥和原子性
 public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }ArrayList 与 LinkedList 的区别
- 底层存储结构:ArrayList底层使用的是数组,LinkedList底层使用的是链表
- 线程安全性:两者都是线程不安全,因为add方法都没有任何关于线程安全的处理。
- 随机访问性:虽然两者都支持随机访问,但是链表随机访问不太高效。感兴趣的读者可以使用下面这段代码分别使用100w数据量的数组或者链表get数据就会发现,ArrayList随机访问速度远远高于LinkedList。
            
            
              java
              
              
            
          
          @Test
    public void arrTest() {
        int size = 100_0000;
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();
        add(size, arrayList, "arrayList");
//        要维护节点关系和创建节点耗时略长
        /**
         * void linkLast(E e) {
         *         final Node<E> l = last;
         *         final Node<E> newNode = new Node<>(l, e, null);
         *         last = newNode;
         *         if (l == null)
         *             first = newNode;
         *         else
         *             l.next = newNode;
         *         size++;
         *         modCount++;
         *     }
         */
        add(size, linkedList, "linkedList");
        /**
         * 输出结果
         * arrayList插入元素时间 52
         * linkedList插入元素时间 86
         */
        get(size, arrayList, "arrayList");
        /**
         * Node<E> node(int index) {
         *         // assert isElementIndex(index);
         *
         *         if (index < (size >> 1)) {
         *             Node<E> x = first;
         *             for (int i = 0; i < index; i++)
         *                 x = x.next;
         *             return x;
         *         } else {
         *             Node<E> x = last;
         *             for (int i = size - 1; i > index; i--)
         *                 x = x.prev;
         *             return x;
         *         }
         *     }
         */
        get(size, linkedList, "linkedList");
    }
    private void get(int size, List<Integer> list, String arrType) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.get(i);
        }
        long end = System.currentTimeMillis();
        System.out.println(arrType + "获取元素时间 " + (end - start));
    }
    private void add(int size, List<Integer> list, String arrType) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.add(i);
        }
        long end = System.currentTimeMillis();
        System.out.println(arrType + "插入元素时间 " + (end - start));
    }输出结果:
            
            
              bash
              
              
            
          
          arrayList插入元素时间 44
linkedList插入元素时间 89
arrayList获取元素时间 5
linkedList获取元素时间 1214464可以看到链表添加时间和访问时间都远远大于数组,原因也很简单,之所以随机访问时间长是因为底层使用的是链表,所以无法做到直接的随机存取。而插入时间长是因为需要插入节点时要遍历位置且维护前驱后继节点的关系。
            
            
              java
              
              
            
          
           /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }- 内存空间占用:ArrayList的空 间浪费主要体现在在List列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
ArrayList 的扩容机制
Java的ArrayList 底层默认数组大小为10,的动态扩容机制即ArrayList 确保元素正确存放的关键,了解核心逻辑以及如何基于该机制提高元素存储效率也是很重要的,感兴趣的读者可以看看读者编写的这篇博客:
Java数据结构与算法(动态数组ArrayList和LinkList小结)
尽管从上面来看两者各有千秋,但是设计者认为若无必要,都使用用Arraylist。

Set集合详解
Set集元素不可重复,存储也不会按照插入顺序排序。适合存储那些需要去重的场景。set大概有两种:
- HashSet:- HashSet要求数据唯一,但是存储是无序的,所以基于面向对象思想复用原则,- Java设计者就通过聚合关系封装- HashMap,基于- HashMap的- key实现了- HashSet。
从源码我们就可以看到HashSet的add方法就是通过HashMap的put方法实现存储唯一元素(key作为set的值,value统一使用PRESENT这个object对象)
            
            
              java
              
              
            
          
          public boolean add(E e) {
      // 底层逻辑是插入时发现这个元素有的话就不插入直接返回集合中的值,反之插入成功返回null,所以判断添加成功的代码才长下面这样
        return map.put(e, PRESENT)==null;
    }- 
LinkedHashSet:LinkedHashSet即通过聚合封装LinkedHashMap实现的。`
- 
TreeSet:TreeSet底层也是TreeMap,一种基于红黑树实现的有序树。关于红黑树可以参考笔者之前写过的这篇文章:
            
            
              java
              
              
            
          
          public TreeSet() {
        this(new TreeMap<E,Object>());
    }Map集合详解
Map即映射集,适合存储键值对类型的元素,key不可重复,value可重复,我们可以更具key找到对应的value。
HashMap(重点)
JDK1.8的HashMap默认是由数组+链表/红黑树组成,通过key算得hash从而定位到Map底层数组的索引位置。 在进行put操作时,若冲突时使用拉链法 解决冲突,如下面这段代码所示,当相同索引位置存储的是链表时,它会进行for循环定位到相同hash值的索引位置的尾节点进行追加。当链表长度大于8且数组长度大于64的情况下,链表会变成红黑树,减少元素搜索时间。(注意若长度小于64链表长度大于8只会进行数组扩容)
            
            
              java
              
              
            
          
                //若索引i位置是链表时,则for循环定位到尾节点,然后将元素追加上去
      for (int binCount = 0; ; ++binCount) {
      //定位到空位将元素插入
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //binCount 达到阈值转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }LinkedHashMap
LinkedHashMap继承自HashMap,他在HashMap基础上增加双向链表,由于LinkedHashMap维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的,所以遍历速度远远快于HashMap,具体可以查阅笔者写的这篇文章:
Hashtable详解
数组+链表组成的,操作时都会上锁,可以保证线程安全,底层数组是 Hashtable 的主体,在插入发生冲突时,会将节点追加到同索引位置的节点后面:
            
            
              java
              
              
            
          
          public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        //修改操作时,通过遍历对应index位置的链表完成覆盖操作
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
      //发生冲突时,会将节点追加到同索引位置的节点后面
        addEntry(hash, key, value, index);
        return null;
    }Set和Map常见面试题
HashMap 和 Hashtable 的区别(重点)
- 从线程安全角度:HashMap线程不安全、Hashtable线程安全。
- 从底层数据结构角度:HashMap初始情况是数组+链表,特定情况下会变数组+红黑树,Hashtable则是数组,核心源码:private transient Entry<?,?>[] table;。
- 从保存数值角度:HashMap允许null键或null值,但是只允许一个。
- 从初始容量角度考虑:HashMap默认16,扩容都是基于当前容量*2。Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。
- 从性能角度考虑:Hashtable每次添加都会上synchronized锁,所以性能很差。
HashMap 和 HashSet有什么区别
HashSet 聚合了HashMap ,通俗来说就是将HashMap 的key作为自己的值存储来使用。
HashMap 和 TreeMap 有什么区别
类图如下,TreeMap 底层是有序树,所以对于需要查找最大值或者最小值等场景,TreeMap 相比HashMap更有优势。因为他继承了NavigableMap接口和SortedMap 接口。

如下源码所示,我们需要拿最大值或者最小值可以用这种方式或者最大值或者最小值
            
            
              java
              
              
            
          
           @Test
    public void treeMapTest(){
        TreeMap<Integer,Object> treeMap=new TreeMap<>();
        treeMap.put(3213,"231");
        treeMap.put(434,"231");
        treeMap.put(432,"231");
        treeMap.put(2,"231");
        treeMap.put(432,"231");
        treeMap.put(31,"231");
        System.out.println(treeMap.toString());
        System.out.println(treeMap.firstKey());
        System.out.println(treeMap.lastEntry());
        /**
         * 输出结果
         * 
         * {2=231, 31=231, 432=231, 434=231, 3213=231}
         * 2
         * 3213=231
         */
    }HashSet 实现去重插入的底层工作机制了解嘛?
当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode 值作比较,如果没有相符的 hashcode,HashSet 会认为对象没有重复出现,直接允许插入了。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet就会将其直接覆盖返回插入前的值。
对此我们不妨基于下面这样一段代码进行debug了解一下究竟
            
            
              java
              
              
            
          
            HashSet<String> set=new HashSet<>();
        set.add("1");
        set.add("1");而通过源码我们也能看出,底层就是调用HashMap的put方法,若返回空则说明这个key没添加过
            
            
              typescript
              
              
            
          
          public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }map.put底层返回值的核心逻辑是基于hashMap的源码如下,可以看到hashset将onlyIfAbsent设置为false,若插入成功返回null,反之则会将用新值将旧值进行覆盖,返回oldValue
            
            
              java
              
              
            
          
          final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
      //通过哈希计算新元素要插入的位置有没有元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//走到这里则说明新元素的位置有元素插入了
        
            Node<K,V> e; K k;
           
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                 //完全相等用用新元素直接去覆盖旧的元素
                e = p;
         //下面两种情况则说明只是计算的位置一样,所以就将新节点挂到后面去
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e不为空说明当前位置之前有过元素,将新值覆盖旧的值并返回旧值
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }如下图所示,第2次add就会返回上次add的value,只不过对于hashSet而言返回的就是private static final Object PRESENT = new Object();全局不可变对象而已。

HashSet、LinkedHashSet 和 TreeSet 使用场景
- HashSet:可在不要求元素有序但唯一的场景。
- LinkedHashSet:可用于要求元素唯一、插入或者访问有序性的场景,或者FIFO的场景。
- TreeSet:要求支持有序性且按照自定义要求进行排序的元素不可重复的场景。
更多关于HashMap的知识
更多ConcurrentHashMap
优先队列PriorityQueue
关于优先队列的文章,笔者已将该文章投稿给了Javaguide,感兴趣的读者可以参考一下这篇文章:
Java集合使用以及工具类小结
一道比较偏门的校招笔试题
以下代码分别输出多少?
            
            
              java
              
              
            
          
           List a=new ArrayList<String>();
        a.add(null);
        a.add(null);
        a.add(null);
        System.out.println(a.size());//3
        Map map=new HashMap();
        map.put("a",null);
        map.put("a",null);
        map.put("a",null);
        System.out.println(map.size());//1小结
我是sharkchili ,CSDN Java 领域博客专家 ,开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程 、JVM 、MySQL数据库个人专栏导航。
参考文献
Java集合常见面试题总结(上):javaguide.cn/java/collec...
ArrayList源码&扩容机制分析:javaguide.cn/java/collec...