一文搞懂全部Java集合面试题

Java集合

刚刚经历过秋招,看了大量的面经,顺便将常见的Java集合常考知识点总结了一下,并根据被问到的频率大体作了一个标注。一颗星表示知识点须要了解,被问到的频率不高,面试时起码能说个差很少。两颗星表示被问到的频率较高或对理解Java有着重要的做用,建议熟练掌握。三颗星表示被问到的频率很是高,建议深刻理解并熟练掌握其相关知识,方便面试时拓展(方便装逼),给面试官留下个好印象。html

推荐阅读:一文搞懂全部Java基础知识面试题java

经常使用的集合类有哪些? ***

Map接口和Collection接口是全部集合框架的父接口。下图中的实线和虚线看着有些乱,其中接口与接口之间若是有联系为继承关系,类与类之间若是有联系为继承关系,类与接口之间则是类实现接口。重点掌握的抽象类有HashMapLinkedListHashTableArrayListHashSetStackTreeSetTreeMap。注意:Collection接口不是Map的父接口。面试

在这里插入图片描述

在这里插入图片描述

List,Set,Map三者的区别? ***

  • List有序集合(有序指存入的顺序和取出的顺序相同,不是按照元素的某些特性排序),可存储重复元素,可存储多个null
  • Set无序集合(元素存入和取出顺序不必定相同),不可存储重复元素,只能存储一个null
  • Map:使用键值对的方式对元素进行存储,key是无序的,且是惟一的。value值不惟一。不一样的key值能够对应相同的value值。

经常使用集合框架底层数据结构 ***

  • List:算法

    1. ArrayList:数组
    2. LinkedList:双线链表
  • Setshell

    1. HashSet:底层基于HashMap实现,HashSet存入读取元素的方式和HashMap中的Key是一致的。
    2. TreeSet:红黑树
  • Map数组

    1. HashMap: JDK1.8以前HashMap由数组+链表组成的, JDK1.8以后有数组+链表/红黑树组成,当链表长度大于8时,链表转化为红黑树,当长度小于6时,从红黑树转化为链表。这样作的目的是能提升HashMap的性能,由于红黑树的查找元素的时间复杂度远小于链表。
    2. HashTable:数组+链表
    3. TreeMap:红黑树

哪些集合类是线程安全的? ***

  • Vector:至关于有同步机制的ArrayList
  • Stack:栈
  • HashTable
  • enumeration:枚举

迭代器 Iterator 是什么 *

Iterator 是 Java 迭代器最简单的实现,它不是一个集合,它是一种用于访问集合的方法,Iterator接口提供遍历任何Collection的接口。缓存

Java集合的快速失败机制 “fail-fast”和安全失败机制“fail-safe”是什么? ***

  • 快速失败安全

    Java的快速失败机制是Java集合框架中的一种错误检测机制,当多个线程同时对集合中的内容进行修改时可能就会抛出ConcurrentModificationException异常。其实不只仅是在多线程状态下,在单线程中用加强for循环中一边遍历集合一边修改集合的元素也会抛出ConcurrentModificationException异常。看下面代码数据结构

    public class Main{
        public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
            for(Integer i : list){
                list.remove(i);  //运行时抛出ConcurrentModificationException异常
            }
        }
    }

    正确的作法是用迭代器的remove()方法,即可正常运行。多线程

    public class Main{
        public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Iterator<Integer> it = list.iterator();
            while(it.hasNext()){
                it.remove();
            }
        }
    }

    形成这种状况的缘由是什么?细心的同窗可能已经发现两次调用的remove()方法不一样,一个带参数据,一个不带参数,这个后面再说,通过查看ArrayList源码,找到了抛出异常的代码

    final void checkForComodification() {
          if (modCount != expectedModCount)
          		throw new ConcurrentModificationException();
    }

    从上面代码中能够看到若是modCountexpectedModCount这两个变量不相等就会抛出ConcurrentModificationException异常。那这两个变量又是什么呢?继续看源码

    protected transient int modCount = 0; //在AbstractList中定义的变量
    int expectedModCount = modCount;//在ArrayList中的内部类Itr中定义的变量

    从上面代码能够看到,modCount初始值为0,而expectedModCount初始值等于modCount。也就是说在遍历的时候直接调用集合的remove()方法会致使modCount不等于expectedModCount进而抛出ConcurrentModificationException异常,而使用迭代器的remove()方法则不会出现这种问题。那么只能在看看remove()方法的源码找找缘由了

    public E remove(int index) {
            rangeCheck(index);
    
            modCount++;
            E oldValue = elementData(index);
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }

    从上面代码中能够看到只有modCount++了,而expectedModCount没有操做,当每一次迭代时,迭代器会比较expectedModCountmodCount的值是否相等,因此在调用remove()方法后,modCount不等于expectedModCount了,这时就了报ConcurrentModificationException异常。但用迭代器中remove()的方法为何不抛异常呢?原来**迭代器调用的remove()方法和上面的remove()方法不是同一个!**迭代器调用的remove()方法长这样:

    public void remove() {
                if (lastRet < 0)
                    throw new IllegalStateException();
                checkForComodification();
    
                try {
                    ArrayList.this.remove(lastRet);
                    cursor = lastRet;
                    lastRet = -1;
                    expectedModCount = modCount;    //这行代码保证了expectedModCount和modCount是相等的
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException();
                }
            }

    从上面代码能够看到expectedModCount = modCount,因此迭代器的remove()方法保证了expectedModCountmodCount是相等的,进而保证了在加强for循环中修改集合内容不会报ConcurrentModificationException异常。

    上面介绍的只是单线程的状况,用迭代器调用remove()方法便可正常运行,但若是是多线程会怎么样呢?

    答案是在多线程的状况下即便用了迭代器调用remove()方法,仍是会报ConcurrentModificationException异常。这又是为何呢?仍是要从expectedModCountmodCount这两个变量入手分析,刚刚说了modCountAbstractList类中定义,而expectedModCountArrayList内部类中定义,因此modCount是个共享变量而expectedModCount是属于线程各自的。简单说,线程1更新了modCount和属于本身的expectedModCount,而在线程2看来只有modCount更新了,expectedModCount并未更新,因此会抛出ConcurrentModificationException异常。

  • 安全失败

    采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。因此在遍历过程当中对原集合所做的修改并不能被迭代器检测到,因此不会抛出ConcurrentModificationException异常。缺点是迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生了修改,迭代器是没法访问到修改后的内容。java.util.concurrent包下的容器都是安全失败,能够在多线程下并发使用。

如何边遍历边移除 Collection 中的元素? ***

从上文**“快速失败机制”**可知在遍历集合时若是直接调用remove()方法会抛出ConcurrentModificationException异常,因此使用迭代器中调用remove()方法。

Array 和 ArrayList 有何区别? ***

  • Array能够包含基本类型和对象类型,ArrayList只能包含对象类型。
  • Array大小是固定的,ArrayList的大小是动态变化的。(ArrayList的扩容是个常见面试题)
  • 相比于ArrayArrayList有着更多的内置方法,如addAll()removeAll()
  • 对于基本类型数据,ArrayList 使用自动装箱来减小编码工做量;而当处理固定大小的基本数据类型的时候,这种方式相对比较慢,这时候应该使用Array

comparable 和 comparator的区别? ** 

  • comparable接口出自java.lang包,能够理解为一个内比较器,由于实现了Comparable接口的类能够和本身比较,要和其余实现了Comparable接口类比较,可使用compareTo(Object obj)方法。compareTo方法的返回值是int,有三种状况:
    1. 返回正整数(比较者大于被比较者)
    2. 返回0(比较者等于被比较者)
    3. 返回负整数(比较者小于被比较者)
  • comparator接口出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序,返回值一样是int,有三种状况,和compareTo相似。

它们之间的区别:不少包装类都实现了comparable接口,像IntegerString等,因此直接调用Collections.sort()直接可使用。若是对类里面自带的天然排序不满意,而又不能修改其源代码的状况下,使用Comparator就比较合适。此外使用Comparator能够避免添加额外的代码与咱们的目标类耦合,同时能够定义多种排序规则,这一点是Comparable接口无法作到的,从灵活性和扩展性讲Comparator更优,故在面对自定义排序的需求时,能够优先考虑使用Comparator接口。

Collection 和 Collections 有什么区别? **

  • Collection 是一个集合接口。它提供了对集合对象进行基本操做的通用接口方法。
  • Collections 是一个包装类。它包含有各类有关集合操做的静态多态方法,例如经常使用的sort()方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

List集合

遍历一个 List 有哪些不一样的方式? **

先说一下常见的元素在内存中的存储方式,主要有两种:

  1. 顺序存储(Random Access):相邻的数据元素在内存中的位置也是相邻的,能够根据元素的位置(如ArrayList中的下表)读取元素。
  2. 链式存储(Sequential Access):每一个数据元素包含它下一个元素的内存地址,在内存中不要求相邻。例如LinkedList

主要的遍历方式主要有三种:

  1. for循环遍历:遍历者本身在集合外部维护一个计数器,依次读取每个位置的元素。
  2. Iterator遍历:基于顺序存储集合的Iterator能够直接按位置访问数据。基于链式存储集合的Iterator,须要保存当前遍历的位置,而后根据当前位置来向前或者向后移动指针。
  3. foreach遍历:foreach 内部也是采用了Iterator 的方式实现,但使用时不须要显示地声明Iterator

那么对于以上三种遍历方式应该如何选取呢?

在Java集合框架中,提供了一个RandomAccess接口,该接口没有方法,只是一个标记。一般用来标记List的实现是否支持RandomAccess。因此在遍历时,能够先判断是否支持RandomAccesslist instanceof RandomAccess),若是支持可用 for 循环遍历,不然建议用Iteratorforeach 遍历。

ArrayList的扩容机制 ***

先说下结论,通常面试时须要记住,ArrayList的初始容量为10,扩容时对是旧的容量值加上旧的容量数值进行右移一位(位运算,至关于除以2,位运算的效率更高),因此每次扩容都是旧的容量的1.5倍。

具体的实现你们可查看下ArrayList的源码。

ArrayList 和 LinkedList 的区别是什么? ***

  • 是否线程安全:ArrayListLinkedList都是不保证线程安全的
  • 底层实现:ArrayList的底层实现是数组,LinkedList的底层是双向链表。
  • 内存占用:ArrayList会存在必定的空间浪费,由于每次扩容都是以前的1.5倍,而LinkedList中的每一个元素要存放直接后继和直接前驱以及数据,因此对于每一个元素的存储都要比ArrayList花费更多的空间。
  • 应用场景:ArrayList的底层数据结构是数组,因此在插入和删除元素时的时间复杂度都会收到位置的影响,平均时间复杂度为o(n),在读取元素的时候能够根据下标直接查找到元素,不受位置的影响,平均时间复杂度为o(1),因此ArrayList更加适用于多读,少增删的场景LinkedList的底层数据结构是双向链表,因此插入和删除元素不受位置的影响,平均时间复杂度为o(1),若是是在指定位置插入则是o(n),由于在插入以前须要先找到该位置,读取元素的平均时间复杂度为o(n)。因此LinkedList更加适用于多增删,少读写的场景

ArrayList 和 Vector 的区别是什么? ***

  • 相同点

    1. 都实现了List接口
    2. 底层数据结构都是数组
  • 不一样点

    1. 线程安全:Vector使用了Synchronized来实现线程同步,因此是线程安全的,而ArrayList是线程不安全的。
    2. 性能:因为Vector使用了Synchronized进行加锁,因此性能不如ArrayList
    3. 扩容:ArrayListVector都会根据须要动态的调整容量,可是ArrayList每次扩容为旧容量的1.5倍,而Vector每次扩容为旧容量的2倍。

简述 ArrayList、Vector、LinkedList 的存储性能和特性? ***

  • ArrayList底层数据结构为数组,对元素的读取速度快,而增删数据慢,线程不安全。
  • LinkedList底层为双向链表,对元素的增删数据快,读取慢,线程不安全。
  • Vector的底层数据结构为数组,用Synchronized来保证线程安全,性能较差,但线程安全。

Set集合

说一下 HashSet 的实现原理 ***

HashSet的底层是HashMap,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMapHashSet的值存放于HashMapkey上,HashMapvalue统一为PRESENT

HashSet如何检查重复?(HashSet是如何保证数据不可重复的?) ***

这里面涉及到了HasCode()equals()两个方法。

  • equals()

    先看下String类中重写的equals方法。

    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }

    从源码中能够看到:

    1. equals方法首先比较的是内存地址,若是内存地址相同,直接返回true;若是内存地址不一样,再比较对象的类型,类型不一样直接返回false;类型相同,再比较值是否相同;值相同返回true,值不一样返回false。总结一下,equals会比较内存地址、对象类型、以及值,内存地址相同,equals必定返回true;对象类型和值相同,equals方法必定返回true
    2. 若是没有重写equals方法,那么equals==的做用相同,比较的是对象的地址值。
  • hashCode

    hashCode方法返回对象的散列码,返回值是int类型的散列码。散列码的做用是肯定该对象在哈希表中的索引位置。

    关于hashCode有一些约定:

    1. 两个对象相等,则hashCode必定相同。
    2. 两个对象有相同的hashCode值,它们不必定相等。
    3. hashCode()方法默认是对堆上的对象产生独特值,若是没有重写hashCode()方法,则该类的两个对象的hashCode值确定不一样

介绍完equals()方法和hashCode()方法,继续说下HashSet是如何检查重复的。

HashSet的特色是存储元素时无序且惟一,在向HashSet中添加对象时,首相会计算对象的HashCode值来肯定对象的存储位置,若是该位置没有其余对象,直接将该对象添加到该位置;若是该存储位置有存储其余对象(新添加的对象和该存储位置的对象的HashCode值相同),调用equals方法判断两个对象是否相同,若是相同,则添加对象失败,若是不相同,则会将该对象从新散列到其余位置。

HashSet与HashMap的区别 ***

HashMap HashSet
实现了Map接口 实现了Set接口
存储键值对 存储对象
key惟一,value不惟一 存储对象惟一
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode
速度相对较快 速度相对较慢

Map集合

HashMap在JDK1.7和JDK1.8中有哪些不一样?HashMap的底层实现 ***

  • JDK1.7的底层数据结构(数组+链表)

在这里插入图片描述

  • JDK1.8的底层数据结构(数组+链表)

在这里插入图片描述

  • JDK1.7的Hash函数

    static final int hash(int h){
    	h ^= (h >>> 20) ^ (h >>>12);
        return h^(h >>> 7) ^ (h >>> 4);
    }
  • JDK1.8的Hash函数

    static final int hash(Onject key){    
        int h;    
        return (key == null) ? 0 : (h = key.hashCode())^(h >>> 16);
    }

    JDK1.8的函数通过了一次异或一次位运算一共两次扰动,而JDK1.7通过了四次位运算五次异或一共九次扰动。这里简单解释下JDK1.8的hash函数,面试常常问这个,两次扰动分别是key.hashCode()key.hashCode()右移16位进行异或。这样作的目的是,高16位不变,低16位与高16位进行异或操做,进而减小碰撞的发生,高低Bit都参与到Hash的计算。如何不进行扰动处理,由于hash值有32位,直接对数组的长度求余,起做用只是hash值的几个低位。

HashMap在JDK1.7和JDK1.8中有哪些不一样点:

JDK1.7 JDK1.8 JDK1.8的优点
底层结构 数组+链表 数组+链表/红黑树(链表大于8) 避免单条链表过长而影响查询效率,提升查询效率
hash值计算方式 9次扰动 = 4次位运算 + 5次异或运算 2次扰动 = 1次位运算 + 1次异或运算 能够均匀地把以前的冲突的节点分散到新的桶(具体细节见下面扩容部分)
插入数据方式 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) 尾插法(直接插入到链表尾部/红黑树) 解决多线程形成死循环地问题
扩容后存储位置的计算方式 从新进行hash计算 原位置或原位置+旧容量 省去了从新计算hash值的时间

HashMap 的长度为何是2的幂次方 ***

由于HashMap是经过key的hash值来肯定存储的位置,但Hash值的范围是-2147483648到2147483647,不可能创建一个这么大的数组来覆盖全部hash值。因此在计算完hash值后会对数组的长度进行取余操做,若是数组的长度是2的幂次方,(length - 1)&hash等同于hash%length,能够用(length - 1)&hash这种位运算来代替%取余的操做进而提升性能。

HashMap的put方法的具体流程? **

HashMap的主要流程能够看下面这个流程图,逻辑很是清晰。

在这里插入图片描述

HashMap的扩容操做是怎么实现的? ***

  • 初始值为16,负载因子为0.75,阈值为负载因子*容量

  • resize()方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize()方法进行扩容。

  • 每次扩容,容量都是以前的两倍

  • 扩容时有个判断e.hash & oldCap是否为零,也就是至关于hash值对数组长度的取余操做,若等于0,则位置不变,若等于1,位置变为原位置加旧容量。

    源码以下:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) { //若是旧容量已经超过最大值,阈值为整数最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;  //没有超过最大值就变为原来的2倍
        }
        else if (oldThr > 0) 
            newCap = oldThr;
    
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                        Node<K,V> loHead = null, loTail = null;//loHead,loTail 表明扩容后在原位置
                        Node<K,V> hiHead = null, hiTail = null;//hiHead,hiTail 表明扩容后在原位置+旧容量
                        Node<K,V> next;
                        do {             
                            next = e.next;
                            if ((e.hash & oldCap) == 0) { //判断是否为零,为零赋值到loHead,不为零赋值到hiHead
                                if (loTail == null)
                                    loHead = e;
                                else                                
                                    loTail.next = e;
                                loTail = e;                           
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;   //loHead放在原位置
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;  //hiHead放在原位置+旧容量
                        }
                    }
                }
            }
        }
        return newTab;
    }

HashMap默认加载因子为何选择0.75?

这个主要是考虑空间利用率和查询成本的一个折中。若是加载因子太高,空间利用率提升,可是会使得哈希冲突的几率增长;若是加载因子太低,会频繁扩容,哈希冲突几率下降,可是会使得空间利用率变低。具体为何是0.75,不是0.74或0.76,这是一个基于数学分析(泊松分布)和行业规定一块儿获得的一个结论。

为何要将链表中转红黑树的阈值设为8?为何不一开始直接使用红黑树?

可能有不少人会问,既然红黑树性能这么好,为何不一开始直接使用红黑树,而是先用链表,链表长度大于8时,才转换为红红黑树。

  • 由于红黑树的节点所占的空间是普通链表节点的两倍,但查找的时间复杂度低,因此只有当节点特别多时,红黑树的优势才能体现出来。至于为何是8,是经过数据分析统计出来的一个结果,链表长度到达8的几率是很低的,综合链表和红黑树的性能优缺点考虑将大于8的链表转化为红黑树。
  • 链表转化为红黑树除了链表长度大于8,还要HashMap中的数组长度大于64。也就是若是HashMap长度小于64,链表长度大于8是不会转化为红黑树的,而是直接扩容。

HashMap是怎么解决哈希冲突的? ***

哈希冲突:hashMap在存储元素时会先计算key的hash值来肯定存储位置,由于key的hash值计算最后有个对数组长度取余的操做,因此即便不一样的key也可能计算出相同的hash值,这样就引发了hash冲突。hashMap的底层结构中的链表/红黑树就是用来解决这个问题的。

HashMap中的哈希冲突解决方式能够主要从三方面考虑(以JDK1.8为背景)

  • 拉链法

    HasMap中的数据结构为数组+链表/红黑树,当不一样的key计算出的hash值相同时,就用链表的形式将Node结点(冲突的keykey对应的value)挂在数组后面。

  • hash函数

    key的hash值通过两次扰动,keyhashCode值与keyhashCode值的右移16位进行异或,而后对数组的长度取余(实际为了提升性能用的是位运算,但目的和取余同样),这样作可让hashCode取值出的高位也参与运算,进一步下降hash冲突的几率,使得数据分布更平均。

  • 红黑树

    在拉链法中,若是hash冲突特别严重,则会致使数组上挂的链表长度过长,性能变差,所以在链表长度大于8时,将链表转化为红黑树,能够提升遍历链表的速度。

HashMap为何不直接使用hashCode()处理后的哈希值直接做为table的下标? ***

hashCode()处理后的哈希值范围太大,不可能在内存创建这么大的数组。

可否使用任何类做为 Map 的 key? ***

能够,但要注意如下两点:

  • 若是类重写了 equals() 方法,也应该重写hashCode()方法。
  • 最好定义key类是不可变的,这样key对应的hashCode() 值能够被缓存起来,性能更好,这也是为何String特别适合做为HashMapkey

为何HashMap中String、Integer这样的包装类适合做为Key? ***

  • 这些包装类都是final修饰,是不可变性的, 保证了key的不可更改性,不会出现放入和获取时哈希值不一样的状况。
  • 它们内部已经重写过hashcode(),equal()等方法。

若是使用Object做为HashMap的Key,应该怎么办呢? **

  • 重写hashCode()方法,由于须要计算hash值肯定存储位置
  • 重写equals()方法,由于须要保证key的惟一性。

HashMap 多线程致使死循环问题 ***

因为JDK1.7的hashMap遇到hash冲突采用的是头插法,在多线程状况下会存在死循环问题,但JDK1.8已经改为了尾插法,不存在这个问题了。但须要注意的是JDK1.8中的HashMap仍然是不安全的,在多线程状况下使用仍然会出现线程安全问题。基本上面试时说到这里既能够了,具体流程用口述是很难说清的,感兴趣的能够看这篇文章。HASHMAP的死循环

ConcurrentHashMap 底层具体实现知道吗? **

  • JDK1.7

    在JDK1.7中,ConcurrentHashMap采用Segment数组 + HashEntry数组的方式进行实现。Segment实现了ReentrantLock,因此Segment有锁的性质,HashEntry用于存储键值对。一个ConcurrentHashMap包含着一个Segment数组,一个Segment包含着一个HashEntry数组,HashEntry是一个链表结构,若是要获取HashEntry中的元素,要先得到Segment的锁。

在这里插入图片描述

  • JDK1.8

    在JDK1.8中,不在是Segment+HashEntry的结构了,而是和HashMap相似的结构,Node数组+链表/红黑树,采用CAS+synchronized来保证线程安全。当链表长度大于8,链表转化为红黑树。在JDK1.8中synchronized只锁链表或红黑树的头节点,是一种相比于segment更为细粒度的锁,锁的竞争变小,因此效率更高。

在这里插入图片描述

总结一下:

  • JDK1.7底层是ReentrantLock+Segment+HashEntry,JDK1.8底层是synchronized+CAS+链表/红黑树
  • JDK1.7采用的是分段锁,同时锁住几个HashEntry,JDK1.8锁的是Node节点,只要没有发生哈希冲突,就不会产生锁的竞争。因此JDK1.8相比于JDK1.7提供了一种粒度更小的锁,减小了锁的竞争,提升了ConcurrentHashMap的并发能力。

HashTable的底层实现知道吗?

HashTable的底层数据结构是数组+链表,链表主要是为了解决哈希冲突,而且整个数组都是synchronized修饰的,因此HashTable是线程安全的,但锁的粒度太大,锁的竞争很是激烈,效率很低。

在这里插入图片描述

HashMap、ConcurrentHashMap及Hashtable 的区别 ***

HashMap(JDK1.8) ConcurrentHashMap(JDK1.8) Hashtable
底层实现 数组+链表/红黑树 数组+链表/红黑树 数组+链表
线程安全 不安全 安全(Synchronized修饰Node节点) 安全(Synchronized修饰整个表)
效率 较高
扩容 初始16,每次扩容成2n 初始16,每次扩容成2n 初始11,每次扩容成2n+1
是否支持Null key和Null Value 能够有一个Null key,Null Value多个 不支持 不支持

Java集合的经常使用方法 **

这些经常使用方法是须要背下来的,虽然面试用不上,可是笔试或者面试写算法题时会常常用到。

Collection经常使用方法

方法
booean add(E e) 在集合末尾添加元素
boolean remove(Object o) 若本类集中有值与o的值相等的元素,移除该元素并返回true
void clear() 清除本类中全部元素
boolean contains(Object o) 判断集合中是否包含该元素
boolean isEmpty() 判断集合是否为空
int size() 返回集合中元素的个数
boolean addAll(Collection c) 将一个集合中c中的全部元素添加到另外一个集合中
Object[] toArray() 返回一个包含本集全部元素的数组,数组类型为Object[]
`boolean equals(Object c)`` 判断元素是否相等
int hashCode() 返回元素的hash值

List特有方法

方法
void add(int index,Object obj) 在指定位置添加元素
Object remove(int index) 删除指定元素并返回
Object set(int index,Object obj) 把指定索引位置的元素更改成指定值并返回修改前的值
int indexOf(Object o) 返回指定元素在集合中第一次出现的索引
Object get(int index) 返回指定位置的元素
List subList(int fromIndex,int toIndex) 截取集合(左闭右开)

LinkedList特有方法

方法
addFirst() 在头部添加元素
addLast() 在尾部添加元素
removeFirst() 在头部删除元素
removeLat() 在尾部删除元素
getFirst() 获取头部元素
getLast() 获取尾部元素

Map

方法
void clear() 清除集合内的元素
boolean containsKey(Object key) 查询Map中是否包含指定key,若是包含则返回true
Set entrySet() 返回Map中所包含的键值对所组成的Set集合,每一个集合元素都是Map.Entry的对象
Object get(Object key) 返回key指定的value,若Map中不包含key返回null
boolean isEmpty() 查询Map是否为空,若为空返回true
Set keySet() 返回Map中全部key所组成的集合
Object put(Object key,Object value) 添加一个键值对,若是已有一个相同的key,则新的键值对会覆盖旧的键值对,返回值为覆盖前的value值,不然为null
void putAll(Map m) 将制定Map中的键值对复制到Map中
Object remove(Object key) 删除指定key所对应的键值对,返回所关联的value,若是key不存在返回null
int size() 返回Map里面的键值对的个数
Collection values() 返回Map里全部values所组成的Collection
boolean containsValue ( Object value) 判断映像中是否存在值 value

Stack

方法
boolean empty() 测试堆栈是否为空。
E peek() 查看堆栈顶部的对象,但不从堆栈中移除它。
E pop() 移除堆栈顶部的对象,并做为此函数的值返回该对象。
E push(E item) 把项压入堆栈顶部。
int search(Object o) 返回对象在堆栈中的位置,以 1 为基数。

Queue

方法
boolean add(E e) 将指定元素插入到队列的尾部(队列满了话,会抛出异常)
boolean offer(E e) 将指定元素插入此队列的尾部(队列满了话,会返回false)
E remove() 返回取队列头部的元素,并删除该元素(若是队列为空,则抛出异常)
E poll() 返回队列头部的元素,并删除该元素(若是队列为空,则返回null)
E element() 返回队列头部的元素,不删除该元素(若是队列为空,则抛出异常)
E peek() 返回队列头部的元素,不删除该元素(若是队列为空,则返回null)
相关文章
相关标签/搜索