arraylist扩容是创建新数组吗 java_Java 基础数据结构分析
java -version java version "13.0.2" 2020-01-14 Java(TM) SE Runtime Environment (build 13.0.2+8) Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)
Array
数组能够做到快速随机访问元素,这是因为当我们创建一个数组时:
var array = new Person[3];array[0] = new Person();
java 首先把这个数组的引用存入栈中,然后到堆空间开辟一片连续的地址空间,并将数组引用指向堆地址空间。
当我们访问指定的数组元素时,则只需要根据 array 的引用地址 + 下标地址, 就能快速定位元素了。
需要注意的是,数组需要连续空间的特性,让数组扩容难以实现,所以各种语言实现的数组,数组的大小都是固定的。
数组有 length 属性,这个属性记录的是数组的大小,而不是元素的个数。
List
java 中 list 常用的有 ArrayList 和 LinkedList。
ArrayList
底层基于数组 Object[] 实现,继承数组的优势,可快速随机访问元素,对于增删操作,则最坏需要 O(n) 。
我们知道 array 不能扩容,但是 ArrayList 明显可以,所以我们去看看,ArrayList ,是怎样扩容的。
public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable { private static final int DEFAULT_CAPACITY = 10; transient Object[] elementData; // non-private to simplify nested class access private int size; protected transient int modCount = 0;}
- DEFAULT_CAPACITYP : 默认的容量大小
- elementData : 用来记录元素的数组,当我们初始化时,如果未指定容量,则用默认值初始化该数组,但是注意,此时 size 的值是零
- size : 记录列表中的元素个数,与数组的容量大小无关
- modCount : 该属性继承自 AbstractList 。所有会修改 list 大小的操作,该值都会增加。当我们通过 iterator 遍历时, 如果该值发生了改变(被另一个线程增加/删除了元素),就会抛出 ConcurrentModificationException
ArrayList 实现了 List / RandomAccess / Cloneable / Serializable 四个标记接口,标记接口 RandomAccess 在排序时会用到,用来选择迭代方式(for or iterator) 。
通过源码能看到, 当我们执行 add 操作时:
public boolean add(E e) { modCount++; add(e, elementData, size); return true;}private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1;}
ArrayList 首先会比较数组长度和元素大小,如果相等,则执行 grow() 方法,进行扩容,扩容完成后,再把元素加到 elementData 数组元素中。注意,此时的 elementData ,事实上,已经不是它了,它已经变成可扩容后的它。
现在看看 grow 方法的实现:
private Object[] grow() { return grow(size + 1);}private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; }}
实现的也很清晰,把原始元素大小,需要的最小扩容量(左移一位),和期待的扩容量,做比较,最终得到一个新的容量大小。
之后通过 Arrays.copy 方法,将老数组的元素拷贝到新元素。
具体比较方式不贴了,简单描述下:
- 取最小扩容量和期待扩容量中的最大值,加上源数组大小,作为新的容量,我们设为 newLength。
- 如果这个值在允许的最大数组长度(Integer.MAX_VALUE - 8) 内,返回
- 否则,直接比较最大数组长度和 源数组长度 + 最小扩容量,满足则返回
- 否则,抛出 OutOfMemoryError
正常情况下,一次扩容的容量,会增加源数组大小的二分之一(即上面左移一位的操作)
LinkedList
底层基于双向链表 实现,对于增删,效率极高。
public class LinkedList extends AbstractSequentialList implements List, Deque, Cloneable, java.io.Serializable { transient int size = 0; transient Node first; transient Node last; protected transient int modCount = 0;}private static class Node { E item; Node next; Node prev; Node(Node prev, E element, Node next) { this.item = element; this.next = next; this.prev = prev; }}
可以看到, LinkedList 实现了 List,Deque,Cloneable 和 Serializable 接口。Deque 即我们所说的双端队列。在 util 包下,还有 ArrayDeque 的实现。
Node 的结构中,主要有 prev , next 和 item ,分别指向上一个节点,下一个节点,item 保存当前节点数据。
因为每个节点都需要保存上下两个节点的信息,所以必然比 ArrayList 要消耗更多的空间
Map
这里以 HashMap 为主。
public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { 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; int threshold; static final int TREEIFY_THRESHOLD = 8; static final int UNTREEIFY_THRESHOLD = 6; transient Node[] table;}
HashMap 几个重要的参数:
- TREEIFY_THRESHOLD : 树化阈值, 即当 map 中的链表长度大于该值时, 会将链表转为红黑树[1]
- UNTREEIFY_THRESHOLD : 当红黑树 size 小于该值时,重新转为链表
- DEFAULT_INITIAL_CAPACITY : hashmap 初始化的容量大小
- table : 即 hash 表,hashmap 做 hash 时,最终散列到这张表上,长度适中为 2n
- DEFAULT_LOAD_FACTOR :常说的装载因子 。hashmap 判断是否需要 resize 时,会根据 threshold 判断,而 threshold 则是根据 装载因子 * table.length 算出来的
我们 new HashMap(9) 时, HashMap 就会去初始化 loadFactor 和 threshold [2]
HashMap 大概就长这个样子:
HashMap 的扩容
当我们执行 put 操作时, HashMap 会先将元素添加到 Map 中,然后查看 size(即 Map 中的元素个数) 是否大于 threshold (阈值, 即上面根据 loadFactory * capacity 即 table.length 算出来的) ,如果大于,就进行扩容,即 resize() 方法。
if (++size > threshold) resize();
resize [3] 时, 首先判断 table 是否初始化,没有则先初始化 table ,然后根据 old table 以及初始化时的 loadFactor ,threshold 参数,计算新的 threshold ,以及需要扩容的大小,一般为 old table length 的 两倍 。
介绍数组时,我们已经说过,数组无法扩容,而 HashMap 使用数组来维护 hash 表的,所以需要新建一个数组,这个数组的长度就是之前数组的两倍 。
之后会进行元素移动,为什么说创建 HashMap 时,要先规划好大小,因为扩容这个操作是很消耗性能的,在一个 double for 循环中(for + while) 。
元素移动完毕后,扩容结束。
Queue
队列,FIFO, first in first out, 先进先出。
java 中 Queue 实现 Collection 接口。具体的实现有
- LinkedBlockingQueue :基于 LinkedList 的阻塞队列
- ArrayBlockingQueue :基于 Array 的阻塞队列
- ArrayDeque : 基于 Array 的双端队列
- LinkedList : 是的, LinkedList 实现了 Deque 接口,也是一个双端队列,Deque deque = new LinkedList();
队列常用方法:
- add :添加节点,加入队列尾部, tail
- remove :删除头节点, head
- peek :获取 head 节点,但是不删除 , 偷取元素
- poll :获取 head 节点,并删除
- offer :立即插入节点到 tail , 对于定长队列,有空间且插入成功则返回 true , 若插入失败,则返回 false 。add 会抛出异常。
- push : Deque 接口所有,插入元素到 head 。
- take : BlockingQueue 接口所有 ,获取并删除 head 节点。 如果当前 queue 没有元素,则会阻塞 。
Stack
栈, FILO , first in last out ,先进后出。
- peek : 获取 head ,但不会删除,偷取元素
- pop : 获取 head , 同时删除, 弹出元素
- push : 压入栈
注释
[1]
看 HashMap 源码的时候,发现在 resize 时,对 for i 循环的时候,都是用 for(;;++1) 的写法:
for (int j = 0; j < oldCap; ++j) { ... }
平时不都是 fro(;;i++) 的吗?然后想了下,i++ 会产生一个中间变量,用于暂时寄存自增前的变量,这一步会消耗一定的性能。
当然,平时我们写代码,最后编译的时候,编译器会把这部分优化成 ++i
[2]
HashMap 初始化时(带 capacity 参数),我们跟踪下代码,就会发现,最终调用:
static final int tableSizeFor(int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}// Integer.numberOfLeadingZerospublic static int numberOfLeadingZeros(int i) { if (i <= 0) return i == 0 ? 32 : 0; int n = 31; if (i >= 1 << 16) { n -= 16; i >>>= 16; } if (i >= 1 << 8) { n -= 8; i >>>= 8; } if (i >= 1 << 4) { n -= 4; i >>>= 4; } if (i >= 1 << 2) { n -= 2; i >>>= 2; } return n - (i >>> 1);}
这个方法在干嘛呢?
我们知道, java 是通过 补码 计数的。
原码 : 原码很好理解,就是一个数的二进制表示, 对于一个四位数,则 2 的原码为 0010, -2 为 1010
反码 : 对于正数而言,其原码 = 反码, 对于负数而言,则只需要将其原码除符号位以外的数取反即可。同样对于四位数,2 为 0010 , -2 为 1101
补码 : 对于正数而言, 其原码 = 补码,对于负数而言,则是将其反码 + 1 。 还是对于四位数, 2 为 0010 , -2 则为 1110
溢出 : java 中,我们偶尔会碰到溢出 。我们知道, java 的基本数据类型中, 如 byte ,长度是一个字节,也就是 8 位,范围是 -128 ~ 127 。1111 1111 = 255 啊! 为什么最大是 127 呢? 因为他们都是有符号数据类型(最高位 0 表示正数, 1 表示负数),最高位是符号位, 所以 1111 1111 事实上应该是 -1
当我们计算两个 byte 类型的 如 127 + 127 时,输出了 -2 。因为 0111 1111 + 0111 1111 = 1111 1110 , 对于 byte 类型,最高位符号位变成了 1 。这个二进制补码还原成十进制,就是 -2 。
关于补码的更多介绍,可参考 这里
现在回到代码。
numberOfLeadingZeros 这个方法,java init 是一个 32 位数,我们假设 i > 1<< 16 , 这表明 i 的高 16 位中,至少有一个 1 , 所以将 n (从左往右,连续的 0 的个数) - 16 ,即低 16 位可以不考虑了。
然后将 i 无符号右移(感觉这里 >> 和 >>> 都一样,因为负数已经在第一步就 return 掉了) 16 位 , 继续判断,直到最后。假设 i = 0011 , 则最后变成 : 31 - (0011 >>> 1) = 31 - 0001 = 30 。
取得这个数后,对 -1 做无符号右移, -1 是 ffff ffff , 右移 n 位后, 再加上 1,可保证得到一个 cap 向上的 2 的次幂。
至于为什么 HashMap 的 capacity 一定要是 2 次幂, 这就要说到它的 hash 算法了:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
可以看到, 这里将 key 的 hashcode 低 32 位和高 32 位做亦或运算 。
if ((p = tab[i = (n - 1) & hash]) == null) { tab[i] = newNode(hash, key, value, null);}
放 node 时, index 是根据 (n-1) & hash 来运算的。
因为 n 是一个 2 次幂,所以 n-1 是一个全 f 的数, 对 hash 做与运算,即拿到 hash 的低位。
如, HashMap capacity = 8 ,即 n = 8 ,则 n-1 = 0000 0111 假设 key 的 hash 为 0111 0010 , 则 (n-1) & hash = 0000 0010 = 2,会被放入 tab[2] 中。
这样有什么问题呢? 即每次都只有 key 的低位参与了运算,会导致较大概率的 hash 碰撞。
所以 HashMap 的 hash 算法,将其 hash 的低 16 位和高 16 位做了亦或,保证数据的更大的随机性,从而减小 hash 碰撞的可能性。
同时,全程的位操作,也给计算带来了性能上的优势。
[3]
参见 树 -- 算法浅析
arraylist扩容是创建新数组吗 java_Java 基础数据结构分析相关推荐
- arraylist扩容是创建新数组吗 java_Java ArrayList扩容问题实例详解
本文研究的主要是Java ArrayList扩容问题实例详解的相关内容,具体介绍如下. 首先我们需要知道ArrayList里面的实质的其实是一个Object类型的数组,ArrayList的扩容问题其实 ...
- arraylist扩容是创建新数组吗 java_Java集合干货——ArrayList源码分析
前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然.关于ArrayList的具体实现,一些基本的都也知道,譬 ...
- arraylist扩容是创建新数组吗 java_Java 集合,你肯定也会被问到这些
文章收录在 GitHub JavaKeeper ,N线互联网开发必备技能兵器谱 作为一位小菜 "一面面试官",面试过程中,我肯定会问 Java 集合的内容,同时作为求职者,也肯定会 ...
- arraylist扩容是创建新数组吗 java_Java编程之数组扩容
一.背景 数组在实际的系统开发中用的越来越少了,我们只有在阅读某些开源项目时才会看到数组的使用.在Java中,数组与List.Set.Map等集合类相比,后者使用起来方便,但是在基本数据类型处理方面, ...
- arraylist扩容是创建新数组吗 java_arraylist扩容机制要怎么实现?arraylist怎么扩容...
ArrayList大家都知道了吧,这是一个动态数组.以java语言来说,数组是定长的,在被创建之后就不能被加长或缩短了,因此,了解它的扩容机制对使用它尤为重要.下面,我们就一起来看看它的扩容机制是怎么 ...
- K3金碟新建账套及基础数据导入导出教程
K3金碟新建账套及基础数据导入导出教程
- python创建数组并运行_python-Cython中从现有数组和变量创建新数组...
与"普通" Python相比,Cython为我们提供了更多对array.array内部的访问,因此我们可以利用它来加速代码: >对于您的小示例,几乎减少了7倍(消除了大部分开 ...
- 由已有数组创建新数组:concat()splice()方法
//concat()方法var a = [ "1", "2", "3", "4", "5" ];va ...
- 数组序列化 java_Java基础之数组序列化、反序列化 小发现(不知道 是不是有问题)...
结论: 数组,无论是否声明为transient,都是可以序列化.反序列化的. 测试情况如下: 1.两种类型的数组:int .String: 2 声明为transient 或者不做任何修饰:. 3. ...
最新文章
- myeclipse的buildpath 和lib引入的区别
- python websocket库 安装_Python WebSocket
- 基本数据类型之间的运算
- 【bzoj 2435】[Noi2011]道路修建(dfs)
- Codeforces Round #722 (Div. 2)
- jyputer notebook 、jypyter、IPython basics
- JS 与Flex交互:html中的js 与flex中的actionScript通信
- Cooliris – 优雅的照片浏览工具[iOS/Android]
- 如何动态读取嵌入式资源
- 光标函数的使用 gotoxy()
- Android系统启动流程分析
- 域控服务器的ip地址,域控更改ip地址问题
- 论文写作:如何写论文
- 微信小程序——根据当前定位查询附近商家
- Centos7.5 BCM4322无线网卡驱动安装踩坑记录
- 解决win10下高分屏的字体显示模糊问题
- C# 淘宝商品微信返利助手开发-(九)编写一个vue页面用于复制淘口令
- 【数据增强】用cv2旋转图像并自定义填充背景颜色(主要用到cv2.getRotationMatrix2D 和 cv2.warpAffine)
- mmsegmentation导出onnx模型的问题
- 从键盘输入一个四位数,输出该四位数的个位,十位,百位和千位数分别是什么。
热门文章
- 值从哪里来_内存频率是怎么算出来的?2133MHz这么奇怪的数字是怎么来的?
- 基于HTML5的贪吃蛇游戏的设计与实现
- GDB watch的使用
- fullPage.js给网站加上全屏幻灯片的展示效果
- java se环境变量_Windows 7中配置JDK(Java SE)环境变量
- vbox 中ubuntu20.04和宿主机共享文件_如何在家搭建一套自己的实验平台(10)iSCSI 共享存储...
- sklearn之Model Selection 估计器
- 山东大学计算机应用基础期中测试,山东大学期末考试计算机应用基础模拟题三套题.pdf...
- 列式存储ClickHouse(一)概述
- Unity开发《一起来捉妖》教程 | 1.陀螺仪控制相机