Java 的集合

一、Collection 

1、ArrayList 

底层采用数组实现,操作大多基于对数组的操作。
在添加和删除时需要做 System.arraycopy(native层方法) 拷贝工作。
添加元素时可能会扩容,这要大量的拷贝工作,删除元素时,会把后面的元素向前拷贝。
所以增、删时效率不高。但set()、get()效率高。

1.ArrayList 的增、删方法

//这是添加元素的方法,size默认为0
public boolean add(E e) {ensureCapacityInternal(size + 1);//元素添加到集合 技术点:size++表示选赋值后加1,++size表示先加1后赋值。elementData[size++] = e;return true;
}private void ensureCapacityInternal(int minCapacity) {//如果是使用无参构造会进入到这里,那么minCapacity 值为10,第一次进来 minCapacity 是1,最大值就是 DEFAULT_CAPACITY 了。if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);
}//最核心的grow方法
private void ensureExplicitCapacity(int minCapacity) {modCount++;//如果第一次进入,minCapacity 为10,条件成立,进行扩容。当集合数据存满后,继续扩容。if (minCapacity - elementData.length > 0)grow(minCapacity);
}
//扩容核心方法 
private void grow(int minCapacity) {//默认为零int oldCapacity = elementData.length;//oldCapacity >> 1 是位运算右移一位,相当于是除以2。所以从这里可以看出扩容后newCapacity 是原来的1.5倍。//如果集合数据存满后,再次扩容newCapacity=10+5int newCapacity = oldCapacity + (oldCapacity >> 1);//如果是无参构造 第一次进入 newCapacity为0,minCapacity为10,条件成立if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);//扩容开始 拷贝elementData数组为新数组,长度为newCapacityelementData = Arrays.copyOf(elementData, newCapacity);
}//根据索引删除
public E remove(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));modCount++;E oldValue = (E) elementData[index];int numMoved = size - index - 1;//拷贝数组if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved);elementData[--size] = null; return oldValue;
}//根据对象删除
public boolean remove(Object o) {if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {//遍历进行查找for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;
}

2.ArrayList 的改、查方法

// set() 方法
public E set(int index, E element) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));E oldValue = (E) elementData[index];elementData[index] = element;return oldValue;
}// get() 方法
public E get(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));return (E) elementData[index];
}

2、ArrayDeque 

3、LinkedList 

双向链表结构,Node中保存了数据、前指针、后指针。
增删数据时,只更换修改节点的前后指针,无需拷贝,速度较快。
查询时需要遍历,速度较慢。
链表不存在容量不足的问题,没有扩容机制,更适合删除和添加。

//头节点指针
transient Node<E> first;//尾节点指针
transient Node<E> last;public LinkedList() {
}
//Node实例,next:上一个元素的指针;prev:下一个元素的指针。
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;}
}

1.LinkedList的增、删方法

add: 尾部时(默认新元素插入尾部)
void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;
}
add:插入到链表头部
private void linkFirst(E e) {final Node<E> f = first;final Node<E> newNode = new Node<>(null, e, f);first = newNode;if (f == null)last = newNode;elsef.prev = newNode;size++;modCount++;
}remove:删除头节点 
private E unlinkFirst(Node<E> f) {// assert f == first && f != null;final E element = f.item;final Node<E> next = f.next;f.item = null;f.next = null; // help GCfirst = next;if (next == null)last = null;elsenext.prev = null;size--;modCount++;return element;
}remove:尾结点
private E unlinkLast(Node<E> l) {// assert l == last && l != null;final E element = l.item;final Node<E> prev = l.prev;l.item = null;l.prev = null; // help GClast = prev;if (prev == null)first = null;elseprev.next = null;size--;modCount++;return element;
}remove:按对象删除,需要遍历节点
public boolean remove(Object o) {if (o == null) {for (Node<E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x);return true;}}} else {for (Node<E> x = first; x != null; x = x.next) {if (o.equals(x.item)) {unlink(x);return true;}}}return false;
}

2.LinkedList的set、get方法

set:根据index修改元素,需要遍历元素
public E set(int index, E element) {checkElementIndex(index);Node<E> x = node(index);E oldVal = x.item;x.item = element;return oldVal;
}get:查询采用二分查找
//node方法用于查找当前节点
//判断index值是不是小于整个链表长度的一半,整个if/else逻辑是在判断查找的位置是距离链表头近还是链表尾近
public E get(int key, E valueIfKeyNotFound) {int i = ContainerHelpers.binarySearch(mKeys, mSize, key);if (i < 0 || mValues[i] == DELETED) {return valueIfKeyNotFound;} else {return (E) mValues[i];}
}

4、TreeSet 

二、Map 

1、TreeMap 

2、HashMap 

默认长度16,扩容因子0.75。无序,线程不安全,key、value 都可以为 null,key是包装类型。
jdk1.7用头插法,由 数组 + 链表 组成,链表是为了解决哈希冲突。
jdk1.8用尾插法,由 数组+链表+红黑树(红黑树条件:链表长度大于8,且数组长度大于64)。

1.核心代码 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {// 声明了一个局部变量 tab,局部变量 Node 类型的数据 p,int 类型 n,iNode<K,V>[] tab; Node<K,V> p; int n, i;// 首先将当前 hashmap 中的 table(哈希表)赋值给当前的局部变量 tab,然后判断tab 是不是空或者长度是不是 0,实际上就是判断当前 hashmap 中的哈希表是不是空或者长度等于 0if ((tab = table) == null || (n = tab.length) == 0)// 如果是空的或者长度等于0,代表现在还没哈希表,所以需要创建新的哈希表,默认就是创建了一个长度为 16 的哈希表n = (tab = resize()).length;// 将当前哈希表中与要插入的数据位置对应的数据取出来,(n - 1) & hash])就是找当前要插入的数据应该在哈希表中的位置,如果没找到,代表哈希表中当前的位置是空的,否则就代表找到数据了, 并赋值给变量 pif ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);//创建一个新的数据,这个数据没有下一条,并将数据放到当前这个位置else { //代表要插入的数据所在的位置是有内容的// 声明了一个节点 e, 一个 key kNode<K,V> e; K k;if (p.hash == hash && //如果当前位置上的那个数据的 hash 和我们要插入的 hash 是一样,代表没有放错位置// 如果当前这个数据的 key 和我们要放的 key 是一样的,实际操作应该是就替换值((k = p.key) == key || (key != null && key.equals(k))))// 将当前的节点赋值给局部变量 ee = p;else if (p instanceof TreeNode)//如果当前节点的 key 和要插入的 key 不一样,然后要判断当前节点是不是一个红黑色类型的节点e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//如果是就创建一个新的树节点,并把数据放进去else {// 如果不是树节点,代表当前是一个链表,那么就遍历链表for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//如果当前节点的下一个是空的,就代表没有后面的数据了p.next = newNode(hash, key, value, null);//创建一个新的节点数据并放到当前遍历的节点的后面if (binCount >= TREEIFY_THRESHOLD - 1) // 重新计算当前链表的长度是不是超出了限制treeifyBin(tab, hash);//超出了之后就将当前链表转换为树,注意转换树的时候,如果当前数组的长度小于MIN_TREEIFY_CAPACITY(默认 64),会触发扩容,我个人感觉可能是因为觉得一个节点下面的数据都超过8 了,说明 hash寻址重复的厉害(比如数组长度为 16 ,hash 值刚好是 0或者 16 的倍数,导致都去同一个位置),需要重新扩容重新 hashbreak;}// 如果当前遍历到的数据和要插入的数据的 key 是一样,和上面之前的一样,赋值给变量 e,下面替换内容if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // 如果当前的节点不等于空,V oldValue = e.value;// 将当前节点的值赋值给 oldvalueif (!onlyIfAbsent || oldValue == null)e.value = value; // 将当前要插入的 value 替换当前的节点里面值afterNodeAccess(e);return oldValue;}}++modCount;// 增加长度if (++size > threshold)resize();// 如果当前的 hash表的长度已经超过了当前 hash 需要扩容的长度, 重新扩容,条件是 haspmap 中存放的数据超过了临界值(经过测试),而不是数组中被使用的下标afterNodeInsertion(evict);return null;
}

2.扩容的方法 

final Node<K,V>[] resize() {// 创建一个临时变量,用来存储当前的tableNode<K,V>[] oldTab = table;// 获取原来的table的长度(大小),判断当前的table是否为空,如果为空,则把0赋值给新定义的oldCap,否则以table的长度作为oldCap的大小int oldCap = (oldTab == null) ? 0 : oldTab.length;// 创建临时变量用来存储旧的阈值,把旧table的阈值赋值给oldThr变量int oldThr = threshold;// 定义变量newCap和newThr来存放新的table的容量和阈值,默认都是0int newCap, newThr = 0;// 判断旧容量是否大于0if (oldCap > 0) {// 判断旧容量是否大于等于 允许的最大值,2^30if (oldCap >= MAXIMUM_CAPACITY) {// 以int的最大值作为原来HashMap的阈值,这样永远达不到阈值就不会扩容了threshold = Integer.MAX_VALUE;// 因为旧容量已经达到了最大的HashMap容量,不可以再扩容了,将阈值变成最大值之后,将原table返回return oldTab;}// 如果原table容量不超过HashMap的最大容量,将原容量*2 赋值给变量newCap,如果newCap不大于HashMap的最大容量,并且原容量大于HashMap的默认容量else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)// 将newThr的值设置为原HashMap的阈值*2newThr = oldThr << 1; // double threshold}// 如果原容量不大于0,即原table为null,则判断旧阈值是否大于0else if (oldThr > 0) // 如果原table为Null且原阈值大于0,说明当前是使用了构造方法指定了容量大小,只是声明了HashMap但是还没有真正的初始化HashMap(创建table数组),只有在向里面插入数据才会触发扩容操作进而进行初始化// 将原阈值作为容量赋值给newCap当做newCap的值。由之前的源码分析可知,此时原阈值存储的大小就是调用构造函数时指定的容量大小,所以直接将原阈值赋值给新容量newCap = oldThr;// 如果原容量不大于0,并且原阈值也不大于0。这种情况说明调用的是无参构造方法,还没有真正初始化HashMap,只有put()数据的时候才会触发扩容操作进而进行初始化else {               // zero initial threshold signifies using defaults// 则以默认容量作为newCap的值newCap = DEFAULT_INITIAL_CAPACITY;// 以初始容量*默认负载因子的结果作为newThr值newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 经过上面的处理过程,如果newThr值为0,说明上面是进入到了原容量不大于0,旧阈值大于0的判断分支。需要单独给newThr进行赋值if (newThr == 0) {// 临时阈值 = 新容量 * 负载因子float ft = (float)newCap * loadFactor;// 设置新的阈值 保证新容量小于最大总量   阈值要小于最大容量,否则阈值就设置为int最大值newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 将新的阈值newThr赋值给threshold,为新初始化的HashMap来使用threshold = newThr;// 初始化一个新的容量大小为newCap的Node数组@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 将新创建的数组赋值给table,完成扩容后的新数组创建table = newTab;// 如果旧table不为null,说明旧HashMap中有值if (oldTab != null) {// 如果原来的HashMap中有值,则遍历oldTab,取出每一个键值对,存入到新tablefor (int j = 0; j < oldCap; ++j) {// 创建一个临时变量e用来指向oldTab中的第j个键值对,Node<K,V> e;// 将oldTab[j]赋值给e并且判断原来table数组中第j个位置是否不为空if ((e = oldTab[j]) != null) {// 如果不为空,则将oldTab[j]置为null,释放内存,方便gcoldTab[j] = null;// 如果e.next = null,说明该位置的数组桶上没有连着额外的数组if (e.next == null)// 此时以e.hash&(newCap-1)的结果作为e在newTab中的位置,将e直接放置在新数组的新位置即可newTab[e.hash & (newCap - 1)] = e;// 否则说明e的后面连接着链表或者红黑树,判断e的类型是TreeNode还是Node,即链表和红黑树判断else if (e instanceof TreeNode)// 如果是红黑树,则进行红黑树的处理。将Node类型的e强制转为TreeNode,之所以能转换是因为TreeNode 是Node的子类// 拆分树,具体源码解析会在后面的TreeNode章节中讲解((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 当前节不是红黑树,不是null,并且还有下一个元素。那么此时为链表else { // preserve order/*这里定义了五个Node变量,其中lo和hi是,lower和higher的缩写,也就是高位和低位,因为我们知道HashMap扩容时,容量会扩到原容量的2倍,也就是放在链表中的Node的位置可能保持不变或位置变成 原位置+oldCap,在原位置基础上又加了一个数,位置变高了,这里的高低位就是这个意思,低位指向的是保持原位置不变的节点,高位指向的是需要更新位置的节点*/// Head指向的是链表的头节点,Tail指向的是链表的尾节点Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;// 指向当前遍历到的节点的下一个节点Node<K,V> next;// 循环遍历链表中的Nodedo {next = e.next;/*如果e.hash & oldCap == 0,注意这里是oldCap,而不是oldCap-1。我们知道oldCap是2的次幂,也就是1、2、4、8、16...转化为二进制之后,都是最高位为1,其它位为0。所以oldCap & e.hash 也是只有e.hash值在oldCap二进制不为0的位对应的位也不为0时,才会得到一个不为0的结果。举个例子,我们知道10010 和00010 与1111的&运算结果都是 0010  ,但是110010和010010与10000的运算结果是不一样的,所以HashMap就是利用这一点,来判断当前在链表中的数据,在扩容时位置是保持不变还是位置移动oldCap。*/// 如果结果为0,即位置保持不变if ((e.hash & oldCap) == 0) {// 如果是第一次遍历if (loTail == null)// 让loHead = e,设置头节点loHead = e;else// 否则,让loTail的next = eloTail.next = e;// 最后让loTail = eloTail = e;}/*其实if 和else 中做的事情是一样的,本质上就是将不需要更新位置的节点加入到loHead为头节点的低位链表中,将需要更新位置的节点加入到hiHead为头结点的高位链表中。我们看到有loHead和loTail两个Node,loHead为头节点,然后loTail是尾节点,在遍历的时候用来维护loHead,即每次循环,更新loHead的next。我们来举个例子,比如原来的链表是A->B->C->D->E。我们这里把->假设成next关系,这五个Node中,只有C的hash & oldCap != 0 ,然后这个代码执行过程就是:第一次循环: 先拿到A,把A赋给loHead,然后loTail也是A第二次循环: 此时e的为B,而且loTail != null,也就是进入上面的else分支,把loTail.next =B,此时loTail中即A->B,同样反应在loHead中也是A->B,然后把loTail = B第三次循环: 此时e = C,由于C不满足 (e.hash & oldCap) == 0,进入到了我们下面的else分支,其实做的事情和当前分支的意思一样,只不过维护的是hiHead和hiTail。第四次循环: 此时e的为D,loTail != null,进入上面的else分支,把loTail.next =D,此时loTail中即B->D,同样反应在loHead中也是A->B->D,然后把loTail = D*/else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 遍历结束,即把table[j]中所有的Node处理完// 如果loTail不为空,也保证了loHead不为空if (loTail != null) {// 此时把loTail的next置空,将低位链表构造完成loTail.next = null;// 把loHead放在newTab数组的第j个位置上,也就是这些节点保持在数组中的原位置不变newTab[j] = loHead;}// 同理,只不过hiHead中节点放的位置是j+oldCapif (hiTail != null) {hiTail.next = null;// hiHead链表中的节点都是需要更新位置的节点newTab[j + oldCap] = hiHead;}}}}}// 最后返回newTabreturn newTab;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/22243.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【NLP】Transformer模型原理(2)

接上文 【NLP】Transformer模型原理(1) 六、零层的transformer 观看涵盖与本节类似内容的视频:0 层理论 在进入更复杂的模型之前,简要考虑一下“零层”变压器很有用。这样的模型获取一个令牌,嵌入它,解嵌它以生成预测下一个令牌的对数: ​

Ext4文件系统介绍 - 实战篇

本文主要通过dd&#xff0c;hexdump和dumpe2fs工具分析ext4的磁盘二进制数据&#xff0c;加深对ext4文件系统的印象&#xff0c;要想理解本建议先阅读下Ext4文件系统介绍 - 理论篇_nginux的博客-CSDN博客。 磁盘超级块数据分析 根据理论篇我们知道ext4 layout中前1024字节是x…

抖音seo源码矩阵系统开发规则开发者分享(一)

抖音SEO矩阵系统源码开发&#xff0c;需要遵循一下步骤 1. 确定需求和功能&#xff1a;明确系统的主要目标和需要实现的功能&#xff0c;包括关键词研究、短视频制作、外链建设、数据分析、账号设置优化等方面。 2. 设计系统架构&#xff1a;根据需求和功能确定系统的架构&am…

数据结构--绪论

这里写目录标题 前言数据结构研究内容基本概念与术语数据元素与数据对象的区别数据结构逻辑结构存储结构 数据类型和抽象数据类型数据类型抽象数据类型定义格式举例 小结研究内容基础概念 抽象数据类型的表示和实现 算法与分析算法的设计要求算法效率事前分析法例子 算法时间的…

【导航地图DB-kiwi地图格式】

背景知识&#xff1a; kiwi趣闻: kiwi是新西兰的一种鸟的名称&#xff0c;Kiwi鸟是尾巴翅膀极短不会飞的鸟&#xff0c;非常珍贵&#xff0c;只在新西兰僻静的丛林里才能见到&#xff0c;所以成为新西兰的国鸟。Kiwi鸟…

【iOS内存管理-内存的几大区域】

前言 iOS内存管理的第一篇章&#xff0c;了解iOS内存的五大分区。 总览 iOS中&#xff0c;内存主要分为五大区域&#xff1a;栈区&#xff0c;堆区&#xff0c;全局区/静态区&#xff0c;常量区和代码区。总览图如下。 如上图所示&#xff0c;代码区是在低地址段存放&#x…

4. CSS用户界面样式

4.1什么是界面样式 所谓的界面样式,就是更改一些用户操作样式,以便提高更好的用户体验。 ●更改用户的鼠标样式 ●表单轮廓 ●防止表单域拖拽 4.2鼠标样式cursor li {cursor: pointer; }设置或检索在对象上移动的鼠标指针采用何种系统预定义的光标形状。 4.3轮廓线outline…

FTP挂载网络磁盘

项目中使用存储阵列或NAS等网络存储作为文件存储地址&#xff0c;服务器与存储之间通过网络进行传输&#xff0c;当我把ftp指向的存储地址修改为网络磁盘时&#xff0c;会出现550等读取不到目录问题&#xff1b;以下为解决方案&#xff1a; 1.在服务器中新增windows用户&#x…

2、JDk、JRE、JVM三者区别和联系

JDK JRE JVM 含义 JDK: Java Develpment Kit java 开发工具 JRE: Java Runtime Environment java 运行时环境 JVM: java Virtual Machine java 虚拟机 一张图来解释&#xff1a; 联系&#xff1a; JVM不能单独搞定class的执行&#xff0c;解释class的时候JVM需要调用解…

什么是CI/CD?让你的项目变得更加敏捷!

在今天这个快速变化的时代&#xff0c;开发者们需要与时俱进&#xff0c;不断提升自己的工作效率。在这篇文章里&#xff0c;将一起探讨如何使用CI/CD和Github Action让你的项目更加高效&#xff0c;快速响应市场变化。 一、什么是CI&#xff1f; CI&#xff08;持续集成&…

【已解决】Flask项目报错TypeError: tuple indices must be integers or slices, not str

文章目录 问题情境报错及分析报错代码分析 解决方案必要的解决方法可能有用的解决方法 问题情境 本解决方案适用情境&#xff1a;在本地可以正常运行的flask项目&#xff0c;放到云服务器报错TypeError: tuple indices must be integers or slices, not str&#xff0c;即代码…

计算机体系结构基础知识介绍之线程级并行性及其利用

线程级并行&#xff08;Thread Level Parallelism&#xff0c;TLP&#xff09;是指在多个处理器或多个核心上同时执行多个线程&#xff0c;从而提高程序的性能和吞吐量。线程级并行的基本原理是利用程序中的数据或任务的并行性&#xff0c;将程序划分为若干个相对独立的子任务&…