1. 集合体系
- 集合和数组的区别
• 数组是固定长度的;集合可变长度的。
• 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
• 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
2. Collection
2.1 单列集合:List
- 概述:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个 null 元素,元素都有索引。常用的实现类有ArrayList、LinkedList 和 Vector。
- 多线程环境下开发:
使用Collections集合工具类,对集合进行同步处理:
List list = Collections.synchronizedList(new ArrayList<>());
2.1.1 ArrayList
- 数据结构:底层是一个数组,通过elementData来存储数据(非线程安全的)
- 扩容机制:(懒加载优化)
- jdk1.8之前:ArrayList默认容量是10,从jdk1.8开始,创建对象时,只是个elementData赋值了一个长度为0的空数组,当第一次使用add方法时,对集合进行扩容,长度为10.
- 扩容增量:增量为原容量的0.5倍,新容量为原容量的1.5倍
- 当数组长度不够时进行扩容,(基于位运算符)使用老数组+老数组右移1位的方式得到一个新数组长度,并通过复制的方法将老数组的数据复制到新数组中。
- 优点:因为有下标,查询快
- 缺点:增删慢,他是一个连续的数据结构,一旦出现了空缺,就需要移动数据来保证连续性。
- 注意点:
- ArrayList的底层是数组,一个索引对应一个元素,所以查询速度快;但是在增删时,需要调整整组数据的移动,所以增删较慢。
- 而LinkedList的底层是双向链表,每次查询时都要从两头开始查询(离头近就从头查,离尾近就从尾查),所以查询较慢;但是增删时,只需要将链表头结点和尾结点指向新插入的结点即可,所以增删速度较快。
- 但如果是新增的数据量较大的情况下,ArrayList的新增效率反面比LinkedList的效率更高。因为ArrayListr底层数组的扩容是1.5倍,数据量越大,扩容的速度就越快,而链表仍需一个个断开链接和重续新链接。
2.1.2 LinkedList
- 数据结构:底层是一个双向链表(非线程安全的)继承于AbstractSequentialList
- 优点:增删快(只需要修改上一个节点的next属性和下一个节点的prev属性即可)
- 缺点:查询慢(节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。)
2.2 单列集合:Set
- 概述:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个 null 元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
2.2.1 HashSet
- 数据结构:底层使用的是哈希表,基于 HashMap (数组+链表/红黑树)实现的,底层采用 HashMap 来保存元素
- 扩容机制:
- 默认容量16(与HashMap相同) 加载因子0.75(扩容时机)
- 扩容增量:增量为原容量的1倍,新容量为原容量的2倍
- 特点:无序,不重复,无索引
2.2.2 LinkedHashSet
- 继承了HashSet,所以它是在HashSet的基础上维护了元素添加顺序的功能
- 特点:有序,不重复,无索引(并且其内部是通过LinkedHashMap 来实现的。)
- 底层维护了一个数组+双向链表
2.2.3 TreeSet
- 数据结构:红黑树(自平衡的排序二叉树)
- 特点:可排序,不重复,无索引
- 可排序功能;
- 自然排序:实现Comparable 接口
- 比较器排序:重写compareTo方法
- == 和equals的区别:
- ==运算符用于比较两个操作数的值或内存地址是否相等,即是否指向同一块内存空间
- equals()方法是Object类中的方法,用于比较两个对象的内容是否相等。该方法在默认情况下使用==运算符进行比较,即它只比较两个对象的引用是否相同。equals()比较的是两个对象的内容是否相等,但需要注意equals()方法是否被重写实现。
- 为什么重写equals方法时要重写hashcode方法?
- 提高效率。hash类型的存储结构,添加元素重复性校验的标准就是先取hashCode值,后判断equals()。重写后,使用hashcode方法提前校验,可以避免每一次比对都调用equals方法。
- 保证是同一个对象。如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。
3. Collections
Conllection是一个单列集合的接口,Collections是一个操作集合的工作类,两者本质就不一样。
4. Map
- Map 是一个键值对集合,存储键、值和之间的映射。 Key 无序,唯一;value 不要求有序,允许重复。Map 没有继承于 Collection 接口,从 Map 集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
4.1 Hashtable
- 数据结构:底层使用的哈希表(一次只能执行一个线程(全表加锁),采取悲观锁,方法都有synchronized,线程安全的)
- 扩容机制:
- 初始容量为11,默认加载因子为:0.75
- 扩容增量:增量为原容量的1倍+1,新容量为原容量的2倍+1
- 注意:key和value都不能为null
4.2 HashMap(无序,不重复,无索引,非线程安全)
- 数据结构:底层是Hash表结构,元素的排列是根据哈希算法和哈希函数排序的,且不可重复。
- 扩容机制:Hash表中数组的分为手动初始化,和自动初始化,自动初初始会在第一次插入元素时开辟空间,默认长度为16,扩容因子为0.75,每次扩容量为自身的2倍长度,扩容之后存入数组的新索引位置就会改变。手动初始化的话,可以在创建对象时自定义初始数组长度,但HashMap不一定会自主设置的数值初始化数组,而按2的n次方创建。
- 版本区别:
- JDK8以前,Hash表的底层是【数组】+【链表】(单向链表),并且1.7版本中是头插法
HashMap1.7版本的的扩容时机是先判断是否达到阈值,达到先扩容,再添加元素,并且采用的是头插法,也就是旧元素挂在新元素下。
- JDK8及之后,变成了【数组】+【链表】(单向链表)+【红黑树】,1.8版本是尾插法。
而HashMap1.8的扩容时机是先添加元素是否达到阈值,达到直接扩容,且使用的是尾插法,即新元素挂在旧元素下面。
- 初始化后,当存入新的键值对时,会先判断数组长度是否大于64,再判断链表元素是否大于等于8时,如果两者都成立,链表会自动转换成红黑树,如果数组小于64,会从第9个开始先扩容,直到数组大于等于64时,链表长度再增加,就会转为红黑树。
- 注意:key和value都可以为空,key唯一(key判断唯一,依赖的是hashCode和equals方法)
- 为什么使用红黑树,为什么不直接使用红黑树??
- 链表取一个数需要遍历链表,而红黑树相对效率要高。
- 因为节点比较少时,红黑树在内存上的劣势,超过了查找等操作的优势,用链表更好。但是当节点比较多的时候,红黑树比链表要更好。
- 为什么1.7是头插法,1.8是尾插法?
- 1.7版本使用头插法是因为头插法是操作速度最快的,找到数组位置就直接找到插入位置了,但这样插入方法在并发场景下会因为多个线程同时扩容出现循环列表,也就是Hashmap的死锁问题。(循环引用问题)
- 1.8版本加入了红黑树来优化哈希桶中的遍历效率,相比头插法而言,尾插法在操作额外的遍历消耗(指遍历哈希桶)已经小很多,也可以避免之前的循环列表问题,同时如果已经变成红黑树了,也不能再用头插法了,而是按红黑树自己的规则排列了。
4.3 ConcurrentHashMap
- 线程安全的
- jdk7通过Segment(分段锁)保证线程安全
- jdk8通过cas+Syncronized(乐观锁)保证线程安全
- HashMap是线程不安全的,在多线程的时候,添加数据或者扩容的时候都有可能出现数据覆盖、丢失的现象,那么就不得不说到一个线程安全的集合ConcurrentHashMap ,首先在JDK1.7的时候,它是用synchronized同步代码块形式保证线程安全的,默认长度是16,默认分配16个哈希槽,第一次插入新元素时,会根据键的哈希值来计算出在数组中存入的位置,但它的数组一旦创建就无法扩容。那么在JDK1.8的时候,它的底层原理是:Node + CAS + sync 实现了每个节点一把锁,它的加载机制会变为在第一次添加元素的时候判断长度是否为0,如果是的话就初始化元素。
5. 集合面试题:
-
- 什么是HashMap双链循环/死锁?
- 双链循环是JDK1.7及更早的版本之前才有的问题。在多线程扩容的情况下,一个线程执行到一半,还未扩容,而另一个线程却抢走先行扩容了,这时候可能出现第一个线程的元素与第二个线程中的元素相互引用的情况,相互引用就会造成死锁。
- 比如一个数线长度为4,有两个数,一个为2,一个为10,那么这两个数都会在索引2上形成哈希桶结构,此时进行扩容,本来在新数组中是2指向10的,结果但之前那个前程正好断在10指向新数组的中间,这就会导至10又重新指向2,最终导while判断中的e永远不会等于null,造成死循环。
- JDK1.8版本避免了双链循环,但不是完全避免,看过一些测试文章,红黑树之间也可能出现死循环,只是比较1.7版本,几率降低。红黑树之间也可能出现死循环。