ArrayList和HashMap详解

本文详细讨论了Java集合框架中的ArrayList、LinkedList和HashMap,包括它们的结构特点、时间复杂度、扩容机制以及多线程同步问题。重点讲解了数组和List之间的转换,以及ArrayList与LinkedList在底层实现和性能上的差异。
摘要由CSDN通过智能技术生成

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使用尾插法,避免了扩容时的死循环

  • 28
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ArrayListHashMap都有扩容的机制。 ArrayList的扩容是在添加元素时,如果当前元素个数已经达到了底层数组的容量,就会创建一个新的数组,将原数组中的元素复制到新数组中,并将新元素添加到新数组中。新数组的容量一般是原数组容量的1.5倍。 HashMap的扩容是在添加元素时,如果当前元素个数已经达到了负载因子(默认为.75)乘以当前数组容量的大小,就会创建一个新的数组,将原数组中的元素重新计算哈希值后,放入新数组中。新数组的容量一般是原数组容量的两倍。 需要注意的是,扩容会涉及到数组的复制和元素的重新计算哈希值,所以会有一定的性能开销。因此,在预知数据量较大的情况下,可以通过设置初始容量来减少扩容的次数,提高性能。 ### 回答2: ArrayListHashMap都是常用的数据结构,在实际开发中使用频率极高,而它们之所以被广泛使用,与它们内部的扩容机制密不可分。 ArrayList是数组实现的线性表,底层是通过数组来存储数据的,而在实际使用中,数组的长度是不确定的,因此需要进行扩容。ArrayList的扩容机制比较简单,当数组已满,需要添加新元素时,会申请一个新的长度为原来1.5倍的数组,并将原数组中的元素复制到新数组中。在大多数情况下,这种扩容机制是能够满足需求的,因为ArrayList中的操作大都是在末尾进行的。 而HashMap是基于哈希表实现的,其内部是由一个数组+链表/红黑树的结构来实现的。其扩容机制是,当HashMap中的元素数量超过阈值,即负载因子的设定值,会创建一个新的容量是原来的两倍的数组,并将原数组中的元素重新计算哈希值,然后重新插入到新数组中。需要注意的是,这个重新计算哈希值和重新插入元素的过程是比较耗时的,因此需要充分考虑扩容阈值的设置。 总之,ArrayListHashMap在扩容时,都要考虑合理设置扩容因子以及重新分配数组后元素的复制和重新插入等问题,以保证它们的性能和稳定性。 ### 回答3: ArrayListHashMapJava中非常常用的两个集合类。它们在使用过程中都要考虑到其容量的问题,因为当空间不足时,它们需要进行扩容操作来添加更多的元素。下面我们将分别来介绍ArrayListHashMap的扩容规则。 ArrayList的扩容规则: ArrayList是一个数组实现的集合类,其内部维护了一个Object数组作为底层容器。当在ArrayList中添加元素时,如果当前容量不足,那么ArrayList会按照一定的扩容规则来扩容数组的容量。 默认情况下,ArrayList的初始容量为10个,每次扩容时,原来的容量会自动增加50%。例如,如果当前ArrayList的容量是8,需要添加第9个元素时,ArrayList会自动将当前容量扩展为12。而当需要添加的元素个数超过了当前容量,那么ArrayList会直接将当前容量扩展到至少需要的大小。例如,如果当前ArrayList的容量是10,需要添加第15个元素时,ArrayList会直接将容量扩展为15。在进行ArrayList扩容时,Java虚拟机会新建一个长度为新容量的Object数组,然后将原数组中的元素复制到新数组中,最后将新数组作为底层容器。 HashMap的扩容规则: HashMap是一个基于哈希表的映射集合类,其内部维护了一个Entry[]数组作为底层容器。当在HashMap中添加元素时,如果当前容量不足,那么HashMap会按照一定的扩容规则来扩容数组的容量。 默认情况下,HashMap的初始容量为16个,扩容因子为0.75。也就是说,当HashMap中元素的个数达到容量的75%时,就会进行扩容操作。在扩容时,HashMap会新建一个长度为原数组两倍的Entry[]数组,然后将原数组中的元素重新放入新数组中。需要注意的是,由于哈希函数针对哈希表长度进行取模运算,不与哈希表长度互质的直接值也很少,所以哈希表的容量必须扩展为2的幂次方。如果容量为非2的幂次方,则在取模运算时需要进行额外的计算,会降低哈希表的访问效率。 总体上来说,ArrayListHashMap的扩容规则都是在容量不足时,新建一个更大的底层数组,并将原数组中的原有元素复制到新数组中,再将新数组作为底层容器。对于ArrayList而言,每次扩容都是原来容量的50%,而对于HashMap而言,容量 超过0.75倍时就进行扩容,且扩容后长度必须是2的幂次方。扩容过程虽然会增加运算时间,但是带来的好处是能够使数据结构在快速插入元素的同时,也能保证空间的充分利用和高效的查询和访问速度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值