自学内容网 自学内容网

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)!