Java集合
本资料整理参考:https://www.bilibili.com/video/BV1yT411H7YK/?spm_id_from=333.337.search-card.all.click
- 常用的集合类定义在 java.util 包中
- 集合类只能存放对象,不能存放基本数据类型,且是对象的引用,而非对象本身。
1、说一说Java提供的常见集合?
在java中提供了量大类的集合框架,主要分为两类:Collection 单列集合和Map 双列集合
- 在Collection中有两个子接口List和Set。在我们平常开发的过程中用的比较多像list 接口中的实现类ArrarList和LinkedList。 在Set接口中有实现类HashSet和 TreeSet。
- 在map接口中有很多的实现类,平时比较常见的是HashMap、TreeMap,还有一个 线程安全的map:ConcurrentHashMap。
2、为什么数组索引从0开始呢?假如从1开始不行吗?
- 在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址+索引乘以存储数据的类型大小。
- 如果数组的索引从1开始,寻址公式中,就需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高。
3、ArrayList底层是如何实现的?
-
ArrayList底层是动态数组。
-
ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10。
// 3个构造函数 /** *默认构造函数,使用初始容量10构造一个空列表(无参数构造) */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 带初始容量参数的构造函数。(用户自己指定容量) */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) {//初始容量大于0 //创建initialCapacity大小的数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) {//初始容量等于0 //创建空数组 this.elementData = EMPTY_ELEMENTDATA; } else {//初始容量小于0,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 *如果指定的集合为null,throws NullPointerException。 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
-
ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组。
-
通过****
ensureCapacity
****方法可以预分配空间,以减少增量重新分配的次数。 -
add方法的源码逻辑如下:
- 确保数组已使用长度(size)加1之后足够存下下一个数据。
ensureCapacityInternal(size + 1)
- 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用**
grow
方法**扩容(原来的1.5倍) - 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
- 返回添加成功布尔值。
- 确保数组已使用长度(size)加1之后足够存下下一个数据。
4、ArrayList list=new ArrayList(10)中的list扩容几次?
- 在ArrayList的源码中提供了一个带参数的构造方法,这个参数就是指定的集合初始长度,所以给了一个10的参数,就是指定了集合的初始长度是10,这里面并没有扩容。
5、如何实现数组和List之间的转换?
- 数组转List ,使用JDK中java.util.Arrays工具类的asList方法。
- List转数组,使用List的toArray方法。无参toArray方法返回 Object数组,传入初始化长度的数组对象,返回该对象数组。
String[] strs = {"aaa","bbb","ccc"};
// 数组 =>> list
List<String> list = Arrays.asList(strs);
// list =>> 数组
String[] array = list.toArray(new String[list.size()]);
- 继续探究:用Arrays.asList转List后,如果修改了数组内容,list受影响吗?List用toArray转数组后,如果修改了List内容,数组受影响吗?
- Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址。
- list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。
6、ArrayList 和 LinkedList 的区别是什么?
-
底层:ArrayList 是动态数组, LinkedList 是双向链表。
-
从操作数据效率来说
- 查询(索引):ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公 式】, LinkedList不支持下标查询。
- 查找(某个值): ArrayList需要遍历,链表也需要遍历,时间复杂度都是 O(n)。
- 插入和删除:
- ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)。
- LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是 O(n)。
-
内存空间:ArrayList底层是数组,内存连续,节省内存。LinkedList 是双向链表需要存储数据,和两个指针,更占用内存。
-
ArrayList和LinkedList都不是线程安全的。
-
探究:那你如何解决这个的线程安全问题的?
- 我们使用这个集合,优先在方法内使用,定义为局部变量,这样的话,就不会出现线程安全问题。
- ArrayList可以通过Collections 的 synchronizedList 方法将 ArrayList 转换成 程安全的容器后再使用。 LinkedList 换成ConcurrentLinkedQueue来使用。
List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>()); List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
7、说一下HashMap的实现原理?
- 底层使用hash表数据结构,即数组+(链表 | 红黑树)
- 添加数据时,计算key的值确定元素在数组中的下标
- 如果key相同,则覆盖原始值;
- 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
- 获取数据通过key的hash计算数组下标获取元素
8、HashMap的jdk1.7和jdk1.8有什么区别?
- jdk1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
- jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
9、你能说下HashMap的put方法的具体流程吗?
- HashMap是懒惰加载,在创建对象时并没有初始化数组。判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)。
- 根据键值key计算hash值得到数组索引
- table[i]==null,条件成立,直接新建节点添加。
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value。
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对。
- 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value。
- 在无参的构造函数中,设置了默认的加载因子是0.75。插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。
10、能讲一讲HashMap的扩容机制吗?
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75);
- 每次扩容的时候,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中;
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置;
- 如果是红黑树,走红黑树的添加;
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为 0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这 个位置上。
11、你了解hashMap的寻址算法吗?
- 计算对象的 hashCode()。
- 再进行调用 hash() 方法进行二次哈希, hashcode值右移16位再异或运算,让哈希分布更为均匀。
- 为了性能更好,并没有直接采用取模的方式,而是使用了数组长度-1 得到一个值,用这个值按位与运算hash值,最终得到数组的位置。
12、为何HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高:在进行扩容是会进行判断 hash值按位与运算旧数组长租是否 == 0。如果等于0,则把元素留在原来位置 ,否则新位置是等于旧位置的下标+旧数组长度。
13、你知道hashmap在jdk1.7下的多线程死循环问题吗?
- 因为jdk7的的数据结构是:数组+链表。在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环。
- 例子
- 线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
- 线程二也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
- 当线程一再继续执行的时候就会出现死循环的问题。
- JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。
14、如何解决hashmap的线程不安全问题?
- 采用ConcurrentHashMap进行使用,它是一个线程安全的 HashMap
15、HashTable与HashMap的区别有哪些?
- 数据结构:hashtable是数组+链表,hashmap在1.8之后改为了 数组+链表+红黑树
- hashtable存储数据的时候都不能为null,而hashmap是可以的
- hash算法不同,hashtable是用本地修饰的hashcode值,而hashmap使用了二次hash
- 扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻倍
- hashtable是线程安全的,操作数据的时候加了锁synchronized, hashmap不是线程安全的,效率更高一些
- 在实际开中不建议使用HashTable,在多线程环境下可以使用 ConcurrentHashMap类
16、有哪些线程安全的集合类?
- HashTable
- ConcurrentHashMap
- Vector
17、集合是否可以存储 NULL?
1)List 接口 ArrayList、LinkedList 以及 Vector 等都可以存储多个 null;
2)Set 接口中 HashSet、LinkedHashSet 可以存储一个 null,TreeSet 不能存储 null;
3)Map 接口中 HashMap、LinkedHashMap 的 key 与 value 均可以为 null。Treemap 的 key 不可以为 null,value 可以为 null。HashTable、ConcurrentHashMap 的 key 与 value 均不能为 null。
18、ArrayList 和 Vector 的区别?
- Vector 是线程安全的,它的方法之间都加了 synchronized 关键字修饰,而 ArrayList 是非线程安全的。因此在不考虑线程安全情况下使用 ArrayList 的效率更高。
- ArrayList 和 Vector 都有一个初始的容量大小,并在必要时对数组进行扩容。但 Vector 每次增长两倍,而 ArrayList 增长 1.5 倍。
19、ArrayList 可以添加 null 值吗?
ArrayList
中可以存储任何类型的对象,包括null
值。不过,不建议向ArrayList
中添加null
值,null
值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。
20、为什么使用transient修饰ArrayList里面的elementData?
- 根据ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。
- 所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。