必知必会:集合
- 1.List有哪些?
java.util包: ArrayList、LinkedList、Stack、Vector
java.util.concurrent包:CopyOnWriteArrayList
- 2.在对List循环过程中能变更当前循环的List吗?为什么?
(1)使用foreach遍历集合时,使用remove移除当前元素会报错。
原因:移除后会改变modCount的值,每次遍历元素时,都会检查modCount==expectedModCount是否相等,不相等则会抛出ConcurrentModificationException。
(2)使用 for (int i=0; i<size; i++) 遍历集合时,看怎么写了,要保证每次调用remove后,index需减1才能保证不报错,且不会漏遍历元素。
示例:
List<String> arrayList = Lists.newArrayList("1", "2", "3", "4", "5", "6"); int size = arrayList.size(); for (int i=0,index = 0; i<size; i++,index++) { String s = arrayList.get(index); if ("2".equals(s) || "6".equals(s)) { arrayList.remove(index); index--; } } System.out.println(arrayList);
原因:每次移除元素,后面的元素的索引下标都会向前移动一位,所以每次移除一个元素后,后一个元素的索引下标会变更为和移除的这个元素一样。
- 3.如果想移除List中的元素,怎么做?
移除元素:
(1)使用上述第2个示例:for (int i=0; i<size; i++)遍历集合,另外还要定义个index,每次调用remove后,index减1,以此来保证移除后下一次get(index)获取到的是下个月元素。
(2)迭代器java.util.Iterator
(3)不操作原List,在目标List上实例化一个新List,在对这个List进行循环。
(4)java.util.Collection#removeIf,底层也是迭代器; arrayList.removeIf(obj -> obj.equals("1"));
(5)使用lambda表达式filter过滤; arrayList = arrayList.stream().filter(obj -> !obj.equals("1")).collect(Collectors.toList());
- 4.ArrayList和LinkedList区别
(1)数组结构不同:ArrayList基于数组实现,LinkedList基于双向链表实现
(2)增删查效率不同:ArrayList查询方便,支持随机访问,增删麻烦一些,LinkedList增删更方便,查询麻烦一些,不支持随机访问,需遍历。
原因:ArrayList增删元素时,只有在尾结点增加或删除尾结点,不需要移动其他元素位置;对于非尾结点增删操作,均需移动后面元素位置;获取元素时,可以直接通过数组下标获取即可,除非使用get(元素)方式才需要遍历数组。
LinkedList增删元素时,只需要维护前后节点以及当前元素的指向;但是获取元素时,需遍历链表。
(3)在内存占用上,ArrayList是预先定义好数组,分配一块连续的内存空间,但可能有空的,存在一定的浪费。LinkedList内存空间不连续,但由于需存储前后节点信息,所以每个节点占用内存空间更多。
- 5.ArrayList的初始容量和扩容机制
默认初始容量为?(1.8之前是10,1.8好像为0),每次插入会先检查是否需要扩容,如果再添加一个元素则超过当前容量,就需要扩容为原来的1.5倍,会创建一个1.5倍的新数组,然后把原数组的值拷贝过去后再新增元素。
- 6.ArrayList的序列化了解吗?
ArrayList通过readObject、writeObject两个方法,里面使用流ObjectInputStream和ObjectOutputStream来实现序列化和反序列化。
- 7.List哪些是线程安全的?
(1)Vector和其子类Stack是线程安全的,但不推荐使用,是历史遗留类。
(2)CopyOnWriteArrayList是线程安全的,其实线程安全版本的ArrayList。
采用的是读写分离的并发策略:读是无锁的,但是添加元素时,是先拷贝原数组,再添加元素到新数组,之后把原对象的引用指向新数组。
还有哪些手段保证线程安全?
(1)使用Collections.synchronizedList(arrayList);
(2)自己加锁控制。
- 8.Set有哪些?
(1)HashSet:基于哈希表实现的,存储无序不重复的元素,可以存储null值。
HashSet底层是由HashMap存储数据,HashSet构造方法会直接初始化一个HashMap。
添加元素时使用equals比较是否相等,如果相等则不添加。
(2)TreeSet:基于二叉树实现的,存储有序不重复的元素,且不允许存储null值。
TreeSet底层是由TreeMap存储数据,TreeSet构造方法会直接初始化一个TreeMap,TreeMap是基于红黑树(平衡二叉查找树)实现。
添加元素时使用compareTo比较是否相等,如果相等则不添加。
- 9.Map有哪些?
java.util包:HashMap、TreeMap
java.util.concurrent包:ConcurrentHashMap
- 10.HashMap的数据结构
HashMap的数据结构是数组+链表+红黑树。
数组是HashMap的主体,用来存储数据元素;
链表则是主要为了解决哈希冲突而存在的;达到一定情况,链表会变为红黑树。
红黑树是为了提高查询的效率,红黑树节点的大小是普通节点大小的两倍,链表转红黑树,是一个空间换时间的策略。
红黑树是一种平衡的二叉树,通过旋转(左转、右旋)和染色(红、黑),特点:根节点、叶子节点、每个红节点下的两个子节点都是黑色的,从任意节点的子树下每个叶子节点的路径都包含相同数量的黑色节点。
为什么table数组的长度大于64,链表长度大于8,则链表转红黑树,而红黑树转回链表的阈值为6?
使用随机哈希码,节点个数到8的情况发生概率只有0.00000006。为什么互转阈值不同,则是如果相同,如果节点刚好在8附件,则节点增减出现不断转换的概率变多,导致资源浪费。
- 11.HashMap初始容量和扩容机制
存放key值的桶数组的长度是由初始化参数确定的,默认初始容量是16,加载因子是0.75,扩容后是变为原来的两倍长度。在扩容时,JDK1.8不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap。
- 12.HashMap的put流程
根据key值获取哈希值,判断tab是否为空或长度为0,如果是则需先扩容;然后根据哈希值计算数组下标,如果对象下标tabl[i]没有存放数据,则直接插入;如果tabl[i]为树节点,则向树插入数据;否则向链表插入数据,如果链表长度大于等于8,则把链表转为红黑树。最后,插入完成后,判断是否超过阈值,超过则需要扩容。
- 13.HashMap怎么查找元素?
根据key值获取哈希值,计算数组下标,获取节点,如果当前节点和key匹配,直接返回;如果节点为树节点则查找红黑树,否则遍历链表。