ArrayList和HashMap详解
Java集合
集合体系结构
Collection
- List系列集合:添加元素是有序、可重复、有索引
- Set系列集合:添加元素是无序、不重复、无索引
ArrayList
数组
数组如何获取元素的地址值?
数组有一个寻址公式:array[i] = baseAddress + i * dataTypeSize
- baseAddeess: 数据的首地址
- dataTypeSize: 代表数组中元素类型的大小,int型的数据,dataTypeSize=4个字节
为什么数组索引从0开始呢?假如从1开始不行吗?
如果从1开始寻址公式多一次减法,即array[i] = baseAddress + (i - 1) * dataTypeSize
从程序设计角度讲,肯定是选择性能最优的方式,即从0开始计算
总结:在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址 + 索引 * 存储数据的大小
如果数组的索引从1开始,寻址公式中就需要多增加一次减法操作,对于CPU来说多了一次指令,性能不高
数组时间复杂度
-
已知下标查询
O(1)
-
未知索引查询
O(logn)
-
插入、删除
由于数组是连续的内存空间,为了保证数组的连续性,插入和删除都会移动操作位置后的元素,导致效率低
O(n)
插入数组末尾O(1),最坏O(n),平均情况O(n)
ArrayList源码分析
成员变量
/**
* 默认初始大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组实例
* new ArrayList(0)
* new ArratList(Collection<? extends E> c) c.length = 0
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 空数组实例 new ArrayList()
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* ArrayList实现存储元素的数组
*/
transient Object[] elementData;
/**
* ArrayList的大小
*/
private int size;
添加、扩容
- 第一次添加数据,数组原始长度是0要进行扩容,最终长度是10
- 第二到十次添加数据,不扩容,第十一次插入元素时,数组扩大为原来的1.5倍,即elementData.length=15
- ArrayList最大容量Integer.Max_Value(2^31 - 1) - 8 = 2147483647 - 8
总结:ArrayList底层是用动态数组实现的
ArrayList初始容量是0,第一次添加时才初始化容量为10
ArrayList扩容是原来的1.5倍,即原容量 + 原容量 >> 1,扩容后都是将原数组拷贝到扩容后的新数组
数组添加元素时:
- 确保size+1之后足够存下下一元素
- 如果size+1 > elementData.length则进行扩容,为原来的1.5倍
- 确保新数据有地方存储后,elementData[size++] = e进行赋值
- 成功则返回true
new ArrayList(10)中的List扩容了几次?
直接创建了一个容量为10的ArrayList,不存在扩容
数组和List之间的转换
List l = Arrays.asList(数组)
String[] array = list.toArray(new String[list.size()]);
-
Arrays.asList()修改原数组,List的值也会改变,因为过程只是将数组引用直接赋值给了Arrays内部的ArrayList(该List不同于常说的集合ArrayList)
-
list.toArray()修改原List,数组的值不会改变,是将原数组进行拷贝,然后将拷贝后的新数组引用赋值给数组变量
ArrayList和LinkedList的区别
单向链表:查询时间复杂度:O(n)
插入、删除时间复杂度是O(n),主要是操作前要先查询,单纯增删时间复杂度O(1)
双向链表时间复杂度类似
总结:
1. 底层数据结构:ArrayList使用动态数组;LinkedList使用双向链表
2. 操作数据效率:
已知下标的情况:ArrayList下标查询时间复杂度O(1),LinkedList不支持
未知:两者都需要遍历,时间复杂度是O(n)
新增和删除:ArrayList尾部操作O(1),其他部分O(n),涉及元素移动;LinkedList头尾节点增删O(1),其他部分O(n),涉及遍历元素
3. 内存空间占用
- ArrayList底层是数组,内存连续,节省内存
- LinkedList是双向链表,除了存储数据,还要存储前驱指针和后继指针。更占内存
4. 线程安全
-
两者都不是线程安全的
-
保证线程安全的方案:
-
在方法内使用,局部变量是在虚拟机栈中,属于线程私有
-
使用Collections工具类的方法
Collections.synchronizedList(list)
-
HashMap
二叉搜索树
又名二叉查找树,有序二叉树或排序二叉树,它要求树中的任意一个节点,其左子树中的每个节点的值,都小于这个节点的值,而右子树的值都大于这个节点的值
时间复杂度
O(logn)
当元素顺序插入时,二叉树则会形成链表,时间复杂度O(n)
红黑树
自平衡的二叉搜索树,也叫平衡二叉B树
特性:
- 节点要么是红色,要么是黑色
- 根节点是黑色
- 叶子节点都是黑色的空节点
- 红黑树中红色节点的子节点都是黑色
- 从任意节点到叶子节点所有路径都包含相同数目的黑色接地那
在不满足五大特性的时候,会发生旋转,以达到特性
时间复杂度
O(logn)
散列表
又名Hash表,是根据Key直接访问在内存存储位置值value的数据结构,它是由数组演化而来,利用数组支持下标进行随机访问的特性
将key映射为数组下标的函数叫做散列函数,一般成为hash函数 hashValue = hash(key)
散列函数的基本要求:
- 计算的hash值必须要大于等于0,它对应数组下标
- key不同,可能hash值会相同,尽量规避这种
hash冲突解决
- 链表法,最坏情况时间复杂度O(n),最好情况下O(1)
- 在链表达到一定长度时,会替换成红黑树O(logn)
HashMap 实现原理
数据结构:动态数组+链表+红黑树
JDK1.7和1.8区别
1.7数组+链表
1.8升级了红黑树,链表长度大于等于8并且数组长度大于等于64转为红黑树,长度下雨等于6时,退化为链表
put方法流程
常见属性:
// 初始化大小16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// hashMap桶数组, Node是其内部类
transient Node<K,V>[] table;
// hashMap元素个数
transient int size;
-
HashMap采用懒加载,在创建对象时并没有初始化数组
-
new HashMap()时只是初始化负载因子为默认0.75
流程:
- 判断table是否为null,是 则resize()扩容
- 根据key计算数组的索引值,判断table[i]==null,是 则直接插入,否 则判断key是否存在,存在则直接覆盖value,不存在则判断table[i]是否为红黑树,是则在红黑树中插入元素,否则遍历链表查看key是否存在,存在则覆盖,不存在则添加到尾部插入,插入后判断长度是否大于8, 是则转换红黑树
- 插入结束,判断++size是否 > threshold(数组长度 * 0.75),是 则调用resize(),否 则结束 由此看出HashMap是先插入再扩容
扩容机制
流程:
- 判断oldCap(当前table大小)> 0,否则说明第一次插入,直接设置数组容量为16,扩容阈值为12,新建数组
- 是则newCap = OldCap * 2(oldCap<<1),新建数组,遍历旧数组
- 遍历旧数组,e.next == null,是则重新计算下标添加进新数组中
- 否则判断是否红黑树,是则进行红黑树添加
- 否则遍历链表,判断(e.hash&oldCap)==0),是则newTab[i] = oldTab[i],否则newTab[i + oldCap] = oldTab[i]
HashMap 支持传默认大小、负载因子
寻址算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
二次hash,扰动算法:主要是为了hash值更加均匀,减少hash冲突
h>>>16(无符号右移16位),然后再做异或运算^
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
数组标=(n-1)&hash,得到数组中的索引,代替取模,性能更好,数组长度必须是2的n次幂
只有当长度是2的n次幂,按位&运算才能代替取模
为什么HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高,如果是2的n次幂可以使用按位与运算代替取模
- 扩容时重新计算索引效率更高: hash&oldCap == 0的元素留在原来的位置,否则新位置 = 旧位置 + oldCap
1.7多线程死循环问题?
数组扩展采用头插法,在数据迁移过程中,有可能导致死循环
两个线程同时读取hashmap的桶中原链表A.next指向B,由于是头插法,线程一扩容后是B.next指向A,而此时线程二中遍历插入元素时,插入完B后,发现B.next是A,而A.next又是B,故此产生死循环
1.8使用尾插法,避免了扩容时的死循环
原文地址:https://blog.csdn.net/qq_38668544/article/details/137714176
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!