面试必备java集合类(话术)
java集合接口及实现类介绍(必会)
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
├ ——–TreeMap 红黑树对所有的key进行排序
集合常用工具类:
Collections:
Arrays:
ArrayList源码分析(必会)
概述
ArrayList是一个动态数组,他是基于数组实现的集合类。在它内部有几个重要的属性。
初始化的长度、存储数据数组 及一个size属性, 我们执行的add方法将会把元素存入到该数组中,
数组的初始化长度为10,如果长度不满足要求 会 触发数组的扩容,size属性用来记录数组中元素的个数。
因为arrayList是基于数组的 , 所以它的查询的速度会非常快。 但指定位置插入数据 或删除指定位置数据,会引起其他数据的变化, 如果需要频繁的 随机插入 随机删除 推荐使用linkedList
但是 ArrayList 不是线程安全的 如果是多线程的环境, 建议使用 Vector 或 CopyOnWriteArrayList
扩容
当调用 add方法时, 会检查 size + 1位是否有效, 如果没有size + 1位 会触发grow扩容方法,
会进行1.5倍扩容。
当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=原始容量 + (原始容量 >> 1);
ArrayList删除指定元素 (fail-fast)
Iterator iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
if(“123”.equals(next)){
iterator.remove();
}
}
LinkedList源码分析(必会)
LinkedList 本质上是基于双向链表实现的, 它内部 主要维护了 一个first节点 和 一个last节点( Node节点分为 数据本身 和 上一个节点的变量 还有下一个节点的变量 )
它实现了 list 可以作为一个集合来使用
它也实现 Deque 可以作为一个双端队列来使用
linkedList 插入数据 和 删除数据非常快
也不是线程安全的。
HashMap 源码分析(1.7)(必会)
概念
HashMap是基于hash表的map实现类,它可以接收null的键值,是非线程安全的,底层基于数组加链表实现的,
当我们new 一个hashmap的时候,会默认初始化一个长度为16的Entry数组(长度是可以指定),我们使用hashmap存储数据的时候 会根据 key计算出hash值 根据hash值及数组的长度能够计算出 要存入数组中的位置,数据里面的每个位置都称作桶
,一个桶有可能会存放多个Entry (hash碰撞) ,多个Entry会已单向链表形式存放。 如果桶的使用达到一定数量会触发扩容,这个数量是根据负载因子 和 数据长度决定的 (数组长度 负载因子 ),默认的负载因子为0.75 ,默认数组长度为16 160.75=12,桶的使用超过12就会触发扩容 ,扩容会创建一个新的数组 长度为旧数组的2倍 ,并将旧数组中的数据迁入到新数组中。
hashmap不是线程安全的,
如果多线程考虑使用hashTable 或 ConcurrentHashMap 或 Map m = Collections.synchronizeMap(hashMap);
put方法源码详解
如果存入的key为null 那这个Entry会存入到0位数组中
不为null 会计算key的hash值
根据hash 及 数组长度会计算出桶的下标
查看下标下是否有对应Entry链表,如果有遍历该链表
对链表中Entry的key进行equals对比,如果结果为true替换
没有对比到对应的key
则会将新的Entry插入到链表的表头中
什么是哈希碰撞(hash碰撞)
指的是两个不同的key计算出相同的hashcode 称为hash碰撞
发生hash碰撞后,他们会存入相同的桶中
为什么负载因子要默认为0.75
HashMap负载因子为0.75是 空间和时间 成本的一种折中,
负载因子过小,扩容频率变高,空间使用率变低
负载因子过高,空间使用率变高,但hash碰撞增加,造成链表长度增加影响查询性能
使用时可根据需求更改负载因子
get方法详解
根据key的hash方法及数组的长度,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。因此,设计HashMap的key类型时,如果使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择
扩容详解
插入新的Entry对象时 需要判断size是否大于等于 负载因子*数组长度,如果大于需要
先对数组进行扩容,扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值迁移到新的数组中
什么是哈希表?(hash表、散列表)
哈希表(HashTable)又叫做散列表,是根据关键码值(即键值对)而直接访问的数据结构。也就是说,它通过把关键码映射到表中一个位置来访问记录,以加快查找速度。这个映射函数就叫做散列(哈希)函数,存放记录的数组叫做散列表。在数据结构中,我们对两种数据结构应该会非常熟悉:数组与链表。数组的特点就是查找容易,插入删除困难;而链表的特点就是查找困难,但是插入删除容易。既然两者各有优缺点,那么我们就将两者的有点结合起来,让它查找容易,插入删除也会快起来。哈希表就是讲两者结合起来的产物。
为什么String、Integer这样的类适合作为key
因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能
如果使用自定义的对象作为key要注意什么
一定要重写equals 及 hashcode的方法
jdk1.8对于hashmap的优化
单个链表长度超过8之后采用红黑树结构,优化查询速度
数组扩容时旧数据的迁移采用位运算 得到的值 为0或1 0位置不变 1位置为当前位置加原数组长度,避免重新hash的性能开销
hashmap的线程安全问题
当put数据时,多线程操作时可能会出现数据不一致
2.在多线程操作hashmap时,如果多条线程同时发现hashmap达到扩容要求,会同时进行resize 方法,在数据迁移的过程中可能会造成环形链表,当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环!(1.8不会出现这个问题)
HashTable 源码分析(必会)
HashTable 也是基于数组加链表, 和hashmap一样, 不同的是 在很多有可能出现线程安全的方法上加了synchronized 锁,这样保证了线程安全。但性能也会跟着下降。 hashTable不能够存null值。 所以现在用的不多 ,都会考虑使用ConcurrentHashMap
ConcurrentHashMap 源码分析(1.7)(必会)
概念
ConcurrentHashMap 和 HashMap 思路是差不多的,不过支持了并发安全。
整个 ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承Lock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
每一个Segment元素中数据的存储还是由数组+链表组成(相当于每个Segment中存储一个hash表)
put数据的过程
当我们put一个key value时
会根据key计算和Segment的长度计算出 要存入的Segment 中的位置
找到位置后 对该Segment上锁
上锁成功后 , 同hashmap的put操作很像,
想看该Segment中 有没有数组 如果有数组 计算应存储的数组下标
如果没数组 初始化数组。
然后 往数组中存入元素 发生hash碰撞 则已链表形式存储
1.8实现
1.8源码改动量很大,不在使用分段锁 而是采用1.8 hashmap的实现思路,引入红黑树, 在put 和 resize方法中使用CAS+synchronized控制线程安全
TreeMap 源码分析(必会)
TreeMap:基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
比较适用于按自然顺序或自定义顺序遍历键(key)。
HashSet 源码分析(必会)
对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成, HashSet的源代码如下:
TreeSet 源码分析(必会)
TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
能够保证数据唯一
实现自然排序 或 自定义排序
ArrayList、LinkedList、Vector对比(必会)
ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。
Vector 和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
HashMap、HashTable、ConcurrentHashMap(必会)
hashmap 允许一个NULL键和多个NULL值,基于数组+链表+红黑树 效率比另外两个高,但不是线程安全的
hashtable 也是基于数组 + 链表, 在关键操作的方法上 加了同步锁 Sychonized , 所以它是线程安全的
但hashTable 效率不高
ConcurrentHashMap 在1.7中采用分段锁的方式实现,每一段锁都有同步手段修饰,所以它既能保证多条线程同时操作
又能够保证数据线程安全
HashMap 、 TreeMap
HashMap:适用于在Map中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,
在需要排序的Map时候才用TreeMap。