一文带你速通Java集合核心面试题

写在文章开头

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

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,实时获取笔者最新的技术推文同时还能和笔者进行深入交流。

Java集合概述

Java顶层设计角度分类而言,集合整体可分为两大类型:

  1. 存放单元素的Collection
  2. 存放键值对的Map类型。

Collection包的整体架构如下图,根据存储的特点我们又可以分为:

  1. 有序不重复的Set
  2. 有序可重复的LIst
  3. FIFO的队列类型Queue

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

List集合详解

List集合概览

List即顺序表,它是按照插入顺序存储的,元素可以重复。从底层结构角度,顺序表还可以分为以下两种:

  1. ArrayList : ArrayList实现顺序表的选用的底层结构为数组,以下便是笔者从ArrayList源码找到的list底层存储元素的变量elementData,可以看到ArrayList本质上就是对数组的封装。
java 复制代码
transient Object[] elementData;
  1. 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;
        }
    }
  1. 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
显示扩容,完成时间:761

ArrayList和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表现最差,而linkedListaddFirst 表现最出色。

bash 复制代码
arrayList头插时长:562
linkedList 头插时长:8
linkedList addFirst 耗时:4

这里我们不妨说一下原因,arrayList性能差原因很明显,每次头部插入都需要挪动整个数组,linkedListadd方法在进行插入时,若是头插法,它会通过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随机插入耗时:27741

ArrayList 和 Vector 的异同

这个问题我们可以从以下几个维度分析:

  1. 底层数据结构:两者底层存储都是采用数组,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;
    }
  1. 线程安全性: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

    }

原因很简单,vectoradd方法有加synchronized 关键字

java 复制代码
//任何操作都是上锁的,保证一次插入操作互斥和原子性
 public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

ArrayList 与 LinkedList 的区别

  1. 底层存储结构:ArrayList 底层使用的是数组,LinkedList 底层使用的是链表
  2. 线程安全性:两者都是线程不安全,因为add方法都没有任何关于线程安全的处理。
  3. 随机访问性:虽然两者都支持随机访问,但是链表随机访问不太高效。感兴趣的读者可以使用下面这段代码分别使用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++;
    }
  1. 内存空间占用:ArrayList 的空 间浪费主要体现在在 List列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)

ArrayList 的扩容机制

JavaArrayList 底层默认数组大小为10,的动态扩容机制即ArrayList 确保元素正确存放的关键,了解核心逻辑以及如何基于该机制提高元素存储效率也是很重要的,感兴趣的读者可以看看读者编写的这篇博客:

Java数据结构与算法(动态数组ArrayList和LinkList小结)

尽管从上面来看两者各有千秋,但是设计者认为若无必要,都使用用Arraylist

Set集合详解

Set集元素不可重复,存储也不会按照插入顺序排序。适合存储那些需要去重的场景。set大概有两种:

  1. HashSet:HashSet要求数据唯一,但是存储是无序的,所以基于面向对象思想复用原则,Java设计者就通过聚合关系封装HashMap,基于HashMapkey实现了HashSet

从源码我们就可以看到HashSetadd方法就是通过HashMapput方法实现存储唯一元素(key作为set的值,value统一使用PRESENT这个object对象)

java 复制代码
public boolean add(E e) {
      // 底层逻辑是插入时发现这个元素有的话就不插入直接返回集合中的值,反之插入成功返回null,所以判断添加成功的代码才长下面这样
        return map.put(e, PRESENT)==null;
    }
  1. LinkedHashSet:LinkedHashSet即通过聚合封装LinkedHashMap实现的。`

  2. TreeSet:TreeSet底层也是TreeMap,一种基于红黑树实现的有序树。关于红黑树可以参考笔者之前写过的这篇文章:

数据结构与算法之红黑树小结

java 复制代码
public TreeSet() {
        this(new TreeMap<E,Object>());
    }

Map集合详解

Map即映射集,适合存储键值对类型的元素,key不可重复,value可重复,我们可以更具key找到对应的value

HashMap(重点)

JDK1.8HashMap默认是由数组+链表/红黑树组成,通过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,具体可以查阅笔者写的这篇文章:

Java集合LinkedHashMap小结

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 的区别(重点)

  1. 从线程安全角度:HashMap 线程不安全、Hashtable 线程安全。
  2. 从底层数据结构角度:HashMap 初始情况是数组+链表,特定情况下会变数组+红黑树Hashtable 则是数组,核心源码:private transient Entry<?,?>[] table;
  3. 从保存数值角度:HashMap 允许null键或null值,但是只允许一个。
  4. 从初始容量角度考虑:HashMap默认16,扩容都是基于当前容量*2Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1
  5. 从性能角度考虑: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 值作比较,如果没有相符的 hashcodeHashSet 会认为对象没有重复出现,直接允许插入了。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet就会将其直接覆盖返回插入前的值。

对此我们不妨基于下面这样一段代码进行debug了解一下究竟

java 复制代码
  HashSet<String> set=new HashSet<>();
        set.add("1");
        set.add("1");

而通过源码我们也能看出,底层就是调用HashMapput方法,若返回空则说明这个key没添加过

typescript 复制代码
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

map.put底层返回值的核心逻辑是基于hashMap的源码如下,可以看到hashsetonlyIfAbsent设置为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就会返回上次addvalue,只不过对于hashSet而言返回的就是private static final Object PRESENT = new Object();全局不可变对象而已。

HashSet、LinkedHashSet 和 TreeSet 使用场景

  1. HashSet:可在不要求元素有序但唯一的场景。
  2. LinkedHashSet:可用于要求元素唯一、插入或者访问有序性的场景,或者FIFO的场景。
  3. TreeSet:要求支持有序性且按照自定义要求进行排序的元素不可重复的场景。

更多关于HashMap的知识

Java集合hashMap小结

更多ConcurrentHashMap

Java并发容器小结

优先队列PriorityQueue

关于优先队列的文章,笔者已将该文章投稿给了Javaguide,感兴趣的读者可以参考一下这篇文章:

github.com/Snailclimb/...

Java集合使用以及工具类小结

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

小结

我是sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考文献

Java集合常见面试题总结(上):javaguide.cn/java/collec...

ArrayList源码&扩容机制分析:javaguide.cn/java/collec...

复制代码
相关推荐
程序员爱钓鱼1 小时前
Go语言实战案例-创建模型并自动迁移
后端·google·go
javachen__1 小时前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
uzong6 小时前
技术故障复盘模版
后端
GetcharZp7 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程7 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研7 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi8 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国9 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy9 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack9 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt