当前位置: 首页 > article >正文

ArrayList 和 HashMap 源码解析

1、ArrayList

1.1、ArrayList 构造方法

无参创建一个 ArrayList 数组默认为空数组

transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private int size; // 数组容量大小

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

指定数组大小 创建一个 ArrayList 数组默认为 指定大小,
如果 initialCapacity == 0 默认为空数组
如果 initialCapacity < 0 则会抛出异常

transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

1.2、add 方法

1、判断数组是否为空,为空则扩容为10,然后修改次数加1,添加数据到数组
2、不为空,则获取扩容大小(原数组大小 + 原数组右移1位),相当于扩容为原来的1.5倍,判断扩容后若小于当前需要的容量,则扩容为当前需要容量大小,然后修改次数加1,添加数据到数组
3、不为空,则扩容(原数组大小 + 原数组右移1位),相当于扩容为原来的1.5倍,判断扩容后若不小于当前需要的容量,则1.5倍扩容,然后修改次数加1,添加数据到数组
4、第四重则是判断是否超过 Integer.MAX_VALUE,2的31次方-1.若是则抛出异常 OutOfMemoryError

  • 扩容机制:默认大小为0,在新增的时候才会扩容,正常情况默认扩容为原来的1.5倍,然后会重新创建一个1.5倍大小的数组,再把原来数组上面的数据复制到新数组上面。

第二种情况只有设置数组初始容量为1时,才会发生。因为 1 >> 1 = 0;这时扩容时才会出现扩容后的比需要的小,才会进入 if (newCapacity - minCapacity < 0) 中

	public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // size 是当前数组大小,目前是0,传入1
        elementData[size++] = e;
        return true;
    }

	private void ensureCapacityInternal(int minCapacity) { // 进入这个方法
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
	private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判断当前数组是否为空
            return Math.max(DEFAULT_CAPACITY, minCapacity); // 为空则返回一个大的值,10 和 1,10比较大,所以返回10
        }
        return minCapacity;
    }
    
	private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

总结:
ArrayList 底层基于数组实现,查询快O(1),新增修改慢O(n),
查询基于索引,新增删除可能涉及到索引移位,新增还会涉及扩容
数组的元素都是连续存储的,这意味着在内存中,数组的元素之间是相邻的

2、LinkedList

LinkedList 是一个双向链表结构,每个节点是一个 node 节点,如下:
item 中存储的是数据,next存储的是下一个node节点的地址,prev存储的是上一个node节点的地址

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

LinkedList 提供了针对头尾节点的操作,使得对头尾节点的 增删查 较快
在这里插入图片描述
总结:

LinkedList 底层基于双向链表结构,增删快O(1),查询慢O(n)。
链表中的节点并不一定是在内存中连续存放的。每个节点实际上是一个对象,而对象在内存中是根据可用的空间分配的,因此它们不一定是连续的。LinkedList 中的节点通过引用(指针)来连接,而这些节点在内存中的位置是不连续的。

应用场景:

因为 removeFirst 会返回删除的元素,所以可以模拟 FIFO(先进先出)

1、可以用于设计队列
2、可以用于设计栈

3、HashMap

  1. 初始化的时候设置大小,因为扩容、重新计算hash值非常费时,建议为2的n次方。如果没有设置初始值默认为16(首次put时才会创建16的数组)
  2. jdk1.8以前是数组+单向链表,jdk1.8以后是数组+单向链表+红黑树
  3. HashMap 不是线程安全的,涉及到线程安全建议使用 ConcurrentHashMap
  4. 当链表长度等于8时,如果容量没有达到64,会先扩容。如果容量达到了64,链表会转为红黑树。当红黑树节点小于6个时会转为链表
  5. 数组为Node类型,在jdk7中称为Entry类型
  6. 链表时间复杂度为O(n),红黑树为O(logn)

3.1、HashMap 扩容机制

扩容是为了减少哈希冲突,提高存取效率。
HashMap 负载因子默认为 0.75

  1. 当临界值 > HashMap数组长度 * 0.75 就会进行扩容
  2. 当链表长度为8,HashMap数组长度小于64,就会进行扩容

hashmap扩容时会创建一个原数组两倍大的数组,然后遍历原数组,将原来所有的数据都移到新数组中。扩容之后需要重新hash,因为哈希值 = hashcode % (length -1),扩容后长度改变,hash值也会随之改变。

3.2、Node节点

哈希值(hash)、键(key)、值(value)以及指向下一个节点的引用(next)。
在 HashMap 的实现中,存储的键值对数据结构是通过链表来实现的。每个 Node 对象就是链表中的一个节点,通过 next 引用指向下一个节点。这样就可以处理哈希冲突,因为具有相同哈希值的键值对会放在同一个哈希桶中,通过链表进行连接。当链表长度达到一定阈值时,会将链表转换为红黑树以提高查询效率。

Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

3.3、负载因子

为什么负载因子是0.75呢?

参数的设置是经过实际测试和经验得出的。加载因子的选择需要权衡空间和时间效率,
过小的加载因子会导致哈希表频繁扩容,
而过大的加载因子会导致链表过长,查找效率下降。
因此,0.75 是一个比较合理的折衷选择,可以在空间和时间上实现比较好的性能。


http://www.kler.cn/a/148895.html

相关文章:

  • 随机数
  • Vim 编辑器学习笔记
  • python 2小时学会八股文-数据结构
  • 硬件工程师之电子元器件—二极管(4)之热量对二极管温度特性的影响
  • git下载慢下载不了?Git国内国外下载地址镜像,git安装视频教程
  • 2024年11月12日Github流行趋势
  • MFC mysql 往数据库中写路径时,斜杠消失
  • Axios 并发请求指南 - 3 种简单实用的方法
  • 深入Android S (12.0) 探索Framework之输入系统IMS的构成与启动
  • linux下ffmpeg安装
  • 2023极客大挑战-AGRT战队wp
  • 多线程(补充知识)
  • [nlp] tokenizer
  • 与中通支付对接
  • 前端 vue 面试题(二)
  • leaflet对线设置渐变色
  • LLM大语言模型
  • 深入redis过程-命令
  • 代码随想录算法训练营第四十九天【动态规划part10】 | 121. 买卖股票的最佳时机、122.买卖股票的最佳时机II
  • Android:从源码看FragmentManager如何工作
  • Python内置类属性`__name__`属性的使用教程
  • WPF中DataGrid解析
  • Webshell混淆免杀的一些思路
  • 成绩排序(练习链表)
  • 《数据结构、算法与应用C++语言描述》-二叉树与其他树-二叉树的C++实现-设置信号放大器与并查集问题
  • Positive Technologies 公司发布了一种保护容器环境的产品 PT Container Security