Java实现动态数组【数据结构与算法】
1、数组
类型固定、长度固定
连续的内存空间
顺序存储、随机读取
查询快、新增删除慢。最好初始化的时候就指定数组大小。这样就可以避免一定的数组扩容出现的内存消耗。
import java.util.Arrays;
import java.util.Iterator;
/**
* @author Administrator
* @date 2022-09-11 16:56
* 实现一个数组
*/
public class MyArray<E> implements Iterable<E> {
private Object[] elementData; // Object存放数据
public MyArray(int capacity) // 构造方法 初始化容量大小
{
// 指定长度 初始化数组 new 出一块空间
elementData = new Object[capacity];
}
/**
* 直接添加新元素
* @param element
* @return
*/
public boolean add (E element)
{
int size = elementData.length; // 获取当前数组大小
int newCapacity = size+1; // 扩容+1
// 此处发生性能消耗,新增数据时,需要扩容,整体数据需要复制迁移,实际上arraylist是1.5扩容!
elementData = Arrays.copyOf(elementData,newCapacity); // 把旧的空间复制一份到新的空间并+1
elementData[size]=element;
return true;
}
/**
* set 方法 根据索引位置新增元素
* @param index
* @param element
* @return
*/
public E set (int index ,E element)
{
E oldValue = (E) elementData[index]; // 获取旧位置的元素值
elementData[index] = element; // 新值覆盖旧值
return oldValue; // 返回旧值
}
public E get (int index)
{
return (E) elementData[index]; // 返回对应索引位置的值
}
@Override
public Iterator<E> iterator(){
return new MyIterator();
}
class MyIterator implements Iterator<E>{
int index = 0;
@Override
public boolean hasNext() {
return index != elementData.length;
}
@Override
public E next() {
return (E) elementData[index++]; // 返回下一个元素值并+1
}
@Override
public void remove() {
//
}
}
public static void main(String[] args) {
MyArray<String> myArray= new MyArray<String>(10); // 初始化一个容量为10的数组
myArray.set(0,"q");
myArray.set(2,"w");
myArray.add("新增");
Iterator<String> iterator = myArray.iterator(); // 使用迭代器
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

1.1、关于arraylist初始容量和扩容
ArrayList 新增元素的方法有两种,一种是直接将元素加到数组的末尾,另外一种是添加元素到任意位置。
arraylist默认构造器,在不指定大小的时候默认容量为 10。
在超出容量之后,每次扩容为当前容量大小的1.5倍+1。
1.2、关于迭代器
集合的顶层接口Collection继承Iterable接口。在Iterable接口中有一个Iterator方法,它返回一个Itertator对象。
public interface Iterable<T> {
/**
* Returns an iterator over elements of type {@code T}.
*
* @return an Iterator.
*/
Iterator<T> iterator();
}
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
迭代器遍历中调用集合revome()方法触发异常 java.util.ConcurrentModificationException 集合中并发修改的异常.
因为迭代器只负责遍历,它使用的仍然是集合本身的数据,在List集合实现的时候数组的长度size会因为remove发生变化的,同时元素的索引值也会因为remove( )方法的调用而发生变化。那么在遍历的时候的remove就需要对这个点进行复刻,而且如果在迭代器里使用了List原生的remove方法,那么就会引起数值不同步的问题。
在ArrayList集合的iterator()方法中,是通过返回Itr对象来获得迭代器的。Itr是ArrayList的一个内部类,它实现了Iterator接口,代码如下:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
注意以下的三个属性:
| cursor | 索引下标,表示下一个可以访问的元素的索引,默认值为 0 |
|---|---|
| lastRet | 索引下标,表示上一个元素的索引,默认值为 -1 |
| expectedModCount | 对集合修改的次数,初始值为 0 |
我们知道:List的add和remove调用会增加modCount的值。也就是这两个操作会被计入对集合的修改次数。
在迭代器的源码中,有一个方法是用来判断 modCount 和 expectModCount 的值是否相等的,其中modCount的值来自List,expectModCount 是迭代器内定义的变量。那为什么要这么设计呢?
因为arraylist是线程不安全的。
结合iterrator的next方法,我们可以看到,如果没有这个校验,某个线程删除了list的一个元素,此时next方法不知道size变更了,依然去取数组里的数据,会导致数据为null或ArrayIndexOutOfBoundsException异常等问题。
ConcurrentModificationException发生在Iterator( )和next( )方法实现中,每次调用都会检查容器的结构是否发生变化,目的是为了避免共享资源而引发的潜在问题。
观察HashMap和ArrayList底层Iterator#next(), 可以看到fast-fail只会增加或者删除(非Iterator#remove())抛出异常;改变容器中元素的内容不存在这个问题(主要是modCount没发生变化)。
在单线程中使用迭代器,对非线程安全的容器,但是只能用Iterator和remove;否则会抛出异常。
在多线程中使用迭代器,可以使用线程安全的容器来避免异常。
使用普通的for循环遍历,效率虽然比较低下,但是不存在ConcurrentModificationException异常问题,用的也比较少。
所以说如果在使用迭代器的时候,用到了List自带的remove方法,那么modCount改变了,但是迭代器内定义的变量expectedCount却没有改变,这样就会被抛出异常。
综上:我们在使用迭代器的时候,不要混用List本身的remove方法。
Iterator接口有四个方法,hasNext、next、remove和forEachRemaining
其中forEachRemaining是java1.8新增的
这个方法是针对集合中剩余元素的操作
剩余的含义是没有被iterator.next()遍历过的元素
1.3、为什么迭代器在调用remove之前要先调用next
当使用Iterator迭代访问Collection集合元素时,Collection集合里的元素不能被改变,只有通过Iterator的remove()方法删除上一次next()方法返回的集合元素才可以;否则会引发java.util.ConcurrentModificationException异常。
查看next方法的源码可以看到 return (E) elementData[lastRet = i];这样一行代码,这行代码表示next方法在让数组下标cursor向后移动一位的同时,还会把lastRet的值变成当前返回的元素下标,这样remove方法就可以根据这个下标完成对元素的删除。
Java实现动态数组【数据结构与算法】的更多相关文章
- 数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解
数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解 对数组有不了解的可以先看看我的另一篇文章,那篇文章对数组有很多详细的解析,而本篇文章则着重讲动态数组,另一篇文章链接 ...
- Java 内功修炼 之 数据结构与算法(一)
一.基本认识 1.数据结构与算法的关系? (1)数据结构(data structure): 数据结构指的是 数据与数据 之间的结构关系.比如:数组.队列.哈希.树 等结构. (2)算法: 算法指的是 ...
- Java 内功修炼 之 数据结构与算法(二)
一.二叉树补充.多叉树 1.二叉树(非递归实现遍历) (1)前提 前面一篇介绍了 二叉树.顺序二叉树.线索二叉树.哈夫曼树等树结构. 可参考:https://www.cnblogs.com/l-y-h ...
- [java笔记]动态数组
private int count;//计数器 private int ary[] = new int [3]; if(count >= ary.length){ //数组动态扩展 int ne ...
- 数据结构与算法——基数排序简单Java实现
基数排序(radix sort)又称“桶子法”,在对多个正整数进行排序时可以使用.它的灵感来自于队列(Queue),它最独特的地方在于利用了数字的有穷性(阿拉伯数字只有0到9的10个). 基数排序使用 ...
- 数据结构和算法(Golang实现)(13)常见数据结构-可变长数组
可变长数组 因为数组大小是固定的,当数据元素特别多时,固定的数组无法储存这么多的值,所以可变长数组出现了,这也是一种数据结构.在Golang语言中,可变长数组被内置在语言里面:切片slice. sli ...
- C与Java中的动态数组
1. 引言 在实际的编程中,往往会发生这种情况,即所需的内存空间取决于实际输入的数据,而无法预先确定.对于这种问题,用静态数组的办法很难解决. 动态数组,是相对于静态数组而言.静态数组的长度是预先定义 ...
- ArrayList类源码解析——ArrayList动态数组的实现细节(基于JDK8)
一.基本概念 ArrayList是一个可以添加对象元素,并进行元素的修改查询删除等操作的容器类.ArrayList底层是由数组实现的,所以和数组一样可以根据索引对容器对象所包含的元素进行快速随机的查询 ...
- 数据结构和算法(Golang实现)(25)排序算法-快速排序
快速排序 快速排序是一种分治策略的排序算法,是由英国计算机科学家Tony Hoare发明的, 该算法被发布在1961年的Communications of the ACM 国际计算机学会月刊. 注:A ...
- 数据结构和算法(Golang实现)(1)简单入门Golang-前言
数据结构和算法在计算机科学里,有非常重要的地位.此系列文章尝试使用 Golang 编程语言来实现各种数据结构和算法,并且适当进行算法分析. 我们会先简单学习一下Golang,然后进入计算机程序世界的第 ...
随机推荐
- 谁说docker-compose不能水平扩展容器、服务多实例?
❝ 虽说我已经从docker-compose走上了docker swarm的邪门歪道,目前被迫走在k8s这条康庄大道, 但是我还是喜欢docker-compose简洁有效的部署方式. ❞ 曾其何时 d ...
- 我的小程序之旅八:基于weixin-java-mp实现微信公众号被动回复消息
在微信里有这样一个公众号[华为运动健康],当点击最新排行的时候,公众号就会发送今天最新的运动步数给你.如下图: 这里有两种格式的消息 1.有头像框,有聊天框--普通消息 2.消息有样式.颜色等--模板 ...
- Jenkins流水线使用@Grab 导入Maven库
有个需求需要在pipeline中调用Java的SDK去执行业务 使用 @Grab 注解可以在Maven中导入Java 库, @Grab('org.apache.commons:commons-math ...
- docker安装mysql服务
拉取镜像 docker pull mysql:5.7.3 运行镜像 docker run -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7.32 # -e MYSQL_ ...
- 【系统选型】OA需求分析,OA系统选型及各供应商对比。
去年公司内部做OA信息化升级,需要更新换代一下OA系统,当时OA选型整理下来的资料分享一下. 需求调研整理后如下: 一共四个模块需要更新&升级 : OA模块(包括行政) + 合同模块 + 费 ...
- jstack查看JVM堆栈信息
目录 介绍 线程状态 Monitor 调用修饰 线程动作 命令格式 常用参数说明 使用实例 jstack pid jstack 查看线程具体在做什么,可看出哪些线程在长时间占用CPU,尽快定位问题和解 ...
- 【认知服务 Azure Cognitive Service】使用Azure Search中Create an Demo的示例时,出现空白页面的问题
问题描述 在根据Azure 认知服务的Search功能文档创建示例时(快速入门:在门户中创建演示应用(Azure 认知搜索)).完全相同的步骤,在中国区创建后下载Demo,查询结果一片空白:如下: 而 ...
- 1、mysql-索引简介
1.1 MySQL官方对索引的定义为: 索引(index)是帮助MySQL高效获取数据的数据结构(有序).在数据之外,数据库系统还维护者满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数 ...
- 第12章_MySQL数据类型精讲
第12章_MySQL数据类型精讲 讲师:尚硅谷-宋红康(江湖人称:康师傅) 官网:http://www.atguigu.com 1. MySQL中的数据类型 类型 类型举例 整数类型 TINYINT. ...
- 摆脱鼠标操作 - vscode - vim - 官方说明文档 github上的,防止打不开,这里发一个
Key - command done - command done with VS Code specific customization ️ - some variations of the com ...