集合复习
说明:对于以上的框架图有如下几点说明
集合接口:6个接口(短虚线表示),表示不同集合类型,是集合框架的基础。
抽象类:5个抽象类(长虚线表示),对集合接口的部分实现。可扩展为自定义集合类。
实现类:8个实现类(实线表示),对接口的具体实现。
Collection 接口是一组允许重复的对象。 Set 接口继承 Collection,集合元素不重复。 List 接口继承
Collection,允许重复,维护元素插入顺序。 Map接口是键-值对象,与Collection接口没有什么关系。
List
元素是有序的、可重复
ArrayList、Vector默认初始容量为10
Vector:线程安全,但速度慢,底层数据结构是数组结构
加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容
扩容增量:原容量的 1倍
如 Vector的容量为10,一次扩容后是容量为20
ArrayList:线程不安全,查询速度快
底层数据结构是数组结构
扩容增量:原容量的 0.5倍+1
如 ArrayList的容量为10,一次扩容后是容量为16
ArrayList:
- ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长。
ArrayList不是线程安全的,只能用在单线程环境下。
实现了Serializable接口,因此它支持序列化,能够通过序列化传输;
实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问;
实现了Cloneable接口,能被克隆。 - remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC
- ArrayList是List接口的可变数组非同步实现,并允许包括null在内的所有元素。
- 该集合是可变长度数组,数组扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。
LinkedList
LinkedList底层使用的双向链表结构,有一个头结点和一个尾结点,双向链表意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。
Set
元素无序的、不可重复。
HashSet:
- 线程不安全,存取速度快
- 底层实现是一个HashMap(保存数据),实现Set接口
- 默认初始容量为16(为何是16,见下方对HashMap的描述)
- 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
- 扩容增量:原容量的 1 倍
- 如 HashSet的容量为16,一次扩容后是容量为32
- HashSet底层是基于HashMap 或者 LinkedHashMap实现的,所以HashSet数据结构就是HashMap或者LinkedHashMap的数据结构
说明:HashSet中由于只包含键,不包含值,由于在底层具体实现时,使用的HashMap或者是LinkedHashMap(可以指定构造函数来确定使用哪种结构),我们知道HashMap是键值对存储,所以为了适应HashMap存储, HashSet增加了一个PRESENT类域(类所有),所有的键都有同一个值(PRESENT)。
Map
是一个双列集合
- 默认初始容量为16(为何是16:16是2^4,可以提高查询效率,另外,32=16<<1 -->至于详细的原因可另行分析,或分析源代码)
- 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
- 扩容增量:原容量的 1 倍,如 HashSet的容量为16,一次扩容后是容量为32
- Hashtable: 线程安全
- 加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
- HashMap是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序
- 底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。
- HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。HashMap底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。
- HashMap进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能
- 采用了Fail-Fast机制,通过一个modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常
hashmap
HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率
可以看到HashMap继承自父类(AbstractMap),实现了Map、Cloneable、Serializable接口。其中,Map接口定义了一组通用的操作;Cloneable接口则表示可以进行拷贝,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响被拷贝的对象;Serializable接口表示HashMap实现了序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 默认的填充因子 3/4
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
面试题
-
Hash原理,内存数据结构
底层使用哈希表(数组+链表),当链表过长试试会将链表转成红黑树来实现
-
讲一下hashmap的put方法过程
a 对key求hash值,然后再计算下标
b.如果没有碰撞,直接放入桶中
c.如果碰撞了,以链表方式链接到后面
d.如果链表长度超过阈值(8),就将链表转成红黑树
e.如果节点已经存在就替换旧值
f.如果桶满了(容量+加载因子),则需要扩容
-
hashmap中hash函数怎么来的?还有哪些实现hash方式?
a.高16bit 与低bit 做 异或
b.(n-1)& hash 得到下标
-
hash怎么解决冲突,讲一下扩容过程,加入一个值在原数组中,现在移动了新数据,位置肯定发生变化了,那是什么定位到这个值新数组中的位置
a.将新节点加到链表后
b. 容量扩容为原来的2倍,然后对每个节点重新计算哈希值
c.这个值可能在两个地方,一个是原下标位置,另一种下标为 <原下标+原容量>的位置
-
抛开hashmap, hash冲突有哪些解决办法
开放定址,莲地址法
LinkedHashMap
- LinkedHashMap增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序该迭代顺序可以是插入顺序或者是访问顺序。
- LinkedHashMap可以认为是HashMap+LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序。
四个关注点在LinkedHashMap上的答案
关 注 点 | 结 论 |
---|---|
LinkedHashMap是否允许空 | Key和Value都允许空 |
LinkedHashMap是否允许重复数据 | Key重复会覆盖、Value允许重复 |
LinkedHashMap是否有序 | 有序 |
LinkedHashMap是否线程安全 | 非线程安全 |