全网阅读过20k的Java集合框架常见面试题总结!

本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star!【Java学习+面试指南】 一份涵盖大部分Java程序员所须要掌握的核心知识。欢迎 Star!)。html

文末有个人公众号,公众号里有我最新整理的Java学习资料,免费分享。java

这么好的文章,必定好先赞后看!!!建议养成这个好习惯!!爱大家!😍git

剖析面试最多见问题之Java集合框架

当了会标题党,这是第一次,后面还有不少次!不过这文章全网阅读确定是超过 20 k 的,并且通过了不少同行的优化,质量有保障哦!程序员

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

  • List(对付顺序的好帮手): List接口存储一组不惟一(能够有多个元素引用相同的对象),有序的对象
  • Set(注重独一无二的性质): 不容许重复的集合。不会有多个元素引用相同的对象。
  • Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key能够引用相同的对象,但Key不能重复,典型的Key是String类型,但也能够是任何对象。

Arraylist 与 LinkedList 区别?

  • 1. 是否保证线程安全: ArrayListLinkedList 都是不一样步的,也就是不保证线程安全;github

  • 2. 底层数据结构: Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6以前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)面试

  • 3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,因此插入和删除元素的时间复杂度受元素位置的影响。 好比:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种状况时间复杂度就是O(1)。可是若是要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。由于在进行上述操做的时候集合中第 i 和第 i 个元素以后的(n-i)个元素都要执行向后位/向前移一位的操做。 ② LinkedList 采用链表存储,因此对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),若是是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))由于须要先移动到指定位置再插入。算法

  • 4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是经过元素的序号快速获取元素对象(对应于get(int index)方法)。spring

  • 5. 内存空间占用: ArrayList的空 间浪费主要体如今在list列表的结尾会预留必定的容量空间,而LinkedList的空间花费则体如今它的每个元素都须要消耗比ArrayList更多的空间(由于要存放直接后继和直接前驱以及数据)。shell

补充内容:RandomAccess接口

public interface RandomAccess {
}

查看源码咱们发现实际上 RandomAccess 接口中什么都没有定义。因此,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具备随机访问功能。后端

binarySearch()方法中,它要判断传入的list 是否 RamdomAccess 的实例,若是是,调用indexedBinarySearch()方法,若是不是,那么调用iteratorBinarySearch()方法

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为何呢?我以为仍是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组自然支持随机访问,时间复杂度为 O(1),因此称为快速随机访问。链表须要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),因此不支持快速随机访问。,ArrayList 实现了 RandomAccess 接口,就代表了他具备快速随机访问功能。 RandomAccess 接口只是标识,并非说 ArrayList 实现 RandomAccess 接口才具备快速随机访问功能的!

下面再总结一下 list 的遍历方式选择:

  • 实现了 RandomAccess 接口的list,优先选择普通 for 循环 ,其次 foreach,
  • 未实现 RandomAccess接口的list,优先选择iterator遍历(foreach遍历底层也是经过iterator实现的,),大size的数据,千万不要使用普通for循环

补充内容:双向链表和双向循环链表

双向链表: 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。

双向链表

双向循环链表: 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。

双向循环链表

ArrayList 与 Vector 区别呢?为何要用Arraylist取代Vector呢?

Vector类的全部方法都是同步的。能够由两个线程安全地访问一个Vector对象、可是一个线程访问Vector的话代码要在同步操做上耗费大量的时间。

Arraylist不是同步的,因此在不须要保证线程安全时建议使用Arraylist。

说一说 ArrayList 的扩容机制吧

详见笔主的这篇文章:经过源码一步一步分析ArrayList 扩容机制

HashMap 和 Hashtable 的区别

  1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都通过synchronized 修饰。(若是你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率: 由于线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
  3. 对Null key 和Null value的支持: HashMap 中,null 能够做为键,这样的键只有一个,能够有一个或多个键所对应的值为 null。。可是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不一样 : ①建立时若是不指定容量初始值,Hashtable 默认的初始大小为11,以后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。以后每次扩充,容量变为原来的2倍。②建立时若是给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 老是使用2的幂做为哈希表的大小,后面会介绍到为何是2的幂次方。
  5. 底层数据结构: JDK1.8 之后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。Hashtable 没有这样的机制。

HashMap 中带有初始容量的构造函数:

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

下面这个方法保证了 HashMap 老是使用2的幂做为哈希表的大小。

/**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap 和 HashSet区别

若是你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码很是很是少,由于除了 clone()writeObject()readObject()是 HashSet 本身不得不实现以外,其余方法都是直接调用 HashMap 中的方法。

HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用 put()向map中添加元素 调用 add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来讲hashcode可能相同,因此equals()方法用来判断对象的相等性,

HashSet如何检查重复

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其余加入的对象的hashcode值做比较,若是没有相符的hashcode,HashSet会假设对象没有重复出现。可是若是发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。若是二者相同,HashSet就不会让加入操做成功。(摘自个人Java启蒙书《Head fist java》第二版)

hashCode()与equals()的相关规定:

  1. 若是两个对象相等,则hashcode必定也是相同的
  2. 两个对象相等,对两个equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不必定是相等的
  4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。若是没有重写hashCode(),则该class的两个对象不管如何都不会相等(即便这两个对象指向相同的数据)。

==与equals的区别

  1. ==是判断两个变量或实例是否是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是否是相同
  2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
  3. ==指引用是否相同 equals()指的是值是否相同

HashMap的底层实现

JDK1.8以前

JDK1.8 以前 HashMap 底层是 数组和链表 结合在一块儿使用也就是 链表散列HashMap 经过 key 的 hashCode 通过扰动函数处理事后获得 hash 值,而后经过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),若是当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,若是相同的话,直接覆盖,不相同就经过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数以后能够减小碰撞。

JDK 1.8 HashMap 的 hash 方法源码:

JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,可是原理不变。

static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

对比一下 JDK1.7的 HashMap 的 hash 方法源码.

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,由于毕竟扰动了 4 次。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说建立一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中便可。

jdk1.8以前的内部结构-HashMap

JDK1.8以后

相比于以前的版本, JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。

jdk1.8以后的内部结构-HashMap

TreeMap、TreeSet以及JDK1.8以后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,由于二叉查找树在某些状况下会退化成一个线性结构。

推荐阅读:

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

为了能让 HashMap 存取高效,尽可能较少碰撞,也就是要尽可能把数据分配均匀。咱们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,先后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,通常应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。因此这个散列值是不能直接拿来用的。用以前还要先作对数组的长度取模运算,获得的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n表明数组长度)。这也就解释了 HashMap 的长度为何是2的幂次方。

这个算法应该如何设计呢?

咱们首先可能会想到采用%取余的操做来实现。可是,重点来了:“取余(%)操做中若是除数是2的幂次则等价于与其除数减一的与(&)操做(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 而且 采用二进制位操做 &,相对于%可以提升运算效率,这就解释了 HashMap 的长度为何是2的幂次方。

HashMap 多线程操做致使死循环问题

主要缘由在于 并发下的Rehash 会形成元素之间会造成一个循环链表。不过,jdk 1.8 后解决了这个问题,可是仍是不建议在多线程下使用 HashMap,由于多线程下使用 HashMap 仍是会存在其余问题好比数据丢失。并发环境下推荐使用 ConcurrentHashMap 。

详情请查看:https://coolshell.cn/articles/9606.html

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体如今实现线程安全的方式上不一样。

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构同样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 以前的 HashMap 的底层数据结构相似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要):在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不一样数据段的数据,就不会存在锁竞争,提升并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操做。(JDK1.6之后 对 synchronized锁作了不少优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,可是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率很是低下。当一个线程访问同步方法时,其余线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另外一个线程不能使用 put 添加元素,也不能使用 get,竞争会愈来愈激烈效率越低。

二者的对比图:

图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html

HashTable:

HashTable全表锁

JDK1.7的ConcurrentHashMap:

JDK1.7的ConcurrentHashMap

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

JDK1.8的ConcurrentHashMap

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

JDK1.7(上面有示意图)

首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其余段的数据也能被其余线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

Segment 实现了 ReentrantLock,因此 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap相似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每一个 HashEntry 是一个链表结构的元素,每一个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先得到对应的 Segment的锁。

JDK1.8 (上面有示意图)

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构相似,数组+链表/红黑二叉树。Java 8在链表长度超过必定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提高N倍。

comparable 和 Comparator的区别

  • comparable接口其实是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
  • comparator接口其实是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

通常咱们须要对一个集合使用自定义排序时,咱们就要重写compareTo()方法或compare()方法,当咱们须要对某一个集合实现两种排序方式,好比一个song对象中的歌名和歌手名分别采用一种排序方法的话,咱们能够重写compareTo()方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种表明咱们只能使用两个参数版的 Collections.sort().

Comparator定制排序

ArrayList<Integer> arrayList = new ArrayList<Integer>();
        arrayList.add(-1);
        arrayList.add(3);
        arrayList.add(3);
        arrayList.add(-5);
        arrayList.add(7);
        arrayList.add(4);
        arrayList.add(-9);
        arrayList.add(-7);
        System.out.println("原始数组:");
        System.out.println(arrayList);
        // void reverse(List list):反转
        Collections.reverse(arrayList);
        System.out.println("Collections.reverse(arrayList):");
        System.out.println(arrayList);

        // void sort(List list),按天然排序的升序排序
        Collections.sort(arrayList);
        System.out.println("Collections.sort(arrayList):");
        System.out.println(arrayList);
        // 定制排序的用法
        Collections.sort(arrayList, new Comparator<Integer>() {

            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        System.out.println("定制排序后:");
        System.out.println(arrayList);

Output:

原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]

重写compareTo方法实现按年龄来排序

// person对象没有实现Comparable接口,因此必须实现,这样才不会出错,才可使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细能够查看String类的API文档,另外其余
// 像Integer类等都已经实现了Comparable接口,因此不须要另外实现了

public  class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    /**
     * TODO重写compareTo方法实现按年龄来排序
     */
    @Override
    public int compareTo(Person o) {
        // TODO Auto-generated method stub
        if (this.age > o.getAge()) {
            return 1;
        } else if (this.age < o.getAge()) {
            return -1;
        }
        return age;
    }
}
public static void main(String[] args) {
        TreeMap<Person, String> pdata = new TreeMap<Person, String>();
        pdata.put(new Person("张三", 30), "zhangsan");
        pdata.put(new Person("李四", 20), "lisi");
        pdata.put(new Person("王五", 10), "wangwu");
        pdata.put(new Person("小红", 5), "xiaohong");
        // 获得key的值的同时获得key所对应的值
        Set<Person> keys = pdata.keySet();
        for (Person key : keys) {
            System.out.println(key.getAge() + "-" + key.getName());

        }
    }

Output:

5-小红
10-王五
20-李四
30-张三

集合框架底层数据结构总结

Collection

1. List

  • Arraylist: Object数组
  • Vector: Object数组
  • LinkedList: 双向链表(JDK1.6以前为循环链表,JDK1.7取消了循环)

2. Set

  • HashSet(无序,惟一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSet 继承于 HashSet,而且其内部是经过 LinkedHashMap 来实现的。有点相似于咱们以前说的LinkedHashMap 其内部是基于 HashMap 实现同样,不过仍是有一点点区别的
  • TreeSet(有序,惟一): 红黑树(自平衡的排序二叉树)

Map

  • HashMap: JDK1.8以前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间
  • LinkedHashMap: LinkedHashMap 继承自 HashMap,因此它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增长了一条双向链表,使得上面的结构能够保持键值对的插入顺序。同时经过对链表进行相应的操做,实现了访问顺序相关逻辑。详细能够查看:《LinkedHashMap 源码详细分析(JDK1.8)》
  • Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

如何选用集合?

主要根据集合的特色来选用,好比咱们须要根据键值获取到元素值时就选用Map接口下的集合,须要排序时选择TreeMap,不须要排序时就选择HashMap,须要保证线程安全就选用ConcurrentHashMap.当咱们只须要存放元素值时,就选择实现Collection接口的集合,须要保证元素惟一时选择实现Set接口的集合好比TreeSet或HashSet,不须要就选择实现List接口的好比ArrayList或LinkedList,而后再根据实现这些接口的集合的特色来选用。

开源项目推荐

做者的其余开源项目推荐:

  1. JavaGuide:【Java学习+面试指南】 一份涵盖大部分Java程序员所须要掌握的核心知识。
  2. springboot-guide : 适合新手入门以及有经验的开发人员查阅的 Spring Boot 教程(业余时间维护中,欢迎一块儿维护)。
  3. programmer-advancement : 我以为技术人员应该有的一些好习惯!
  4. spring-security-jwt-guide :从零入门 !Spring Security With JWT(含权限验证)后端部分代码。
相关文章
相关标签/搜索