Java ArrayList:动态数组

简介: 本文探讨Java中的数组,对比C/C++、JS/PHP/Python等语言的数组特性。文章分析了Java数组的定义、创建方式及其规范,指出其优缺点。Java数组作为引用类型,在堆上分配内存,支持动态大小,避免了C/C++中裸数组的常见问题(如越界访问)。然而,Java数组也存在性能瓶颈和设计缺陷,例如运行时的安全检查影响速度,无法创建超大数组或泛型数组,且多线程场景下缺乏同步机制。作者建议在实际开发中用集合替代数组以规避这些问题。

几乎所有编程语言都支持数组这种数据结构,功能大同小异,本文主要探讨Java中的数组。数组大家都会使用,但是你并不一定真的了解Java数组,本文会和C/C++的数组进行比较,JS/PHP/Python因为这些动态语言虽然可能也叫数组,但是却不是真正意义的“数组”。

先看数组的定义:

体验AI代码助手

代码解读

复制代码

1.所有元素相同类型
2.元素存储在一个连续性的内存块中,可以通过索引访问
3.数组一旦创建,大小不能改变

数组的最有用的性质2,性质2又产生了两个重要应用。
1.内存连续,则基于数组的结构遍历很快(程序局部性原理)
比如Mybatis,List返回默认都是ArrayList,因为这种情况下遍历更频繁。
2.通过索引访问,f(key)->index,所以数组是Hash表在计算机的实现。

先说说C/C++中数组,C/C++中只能创建静态数组(又称裸数组),sizeof可以计算数组的长度(编译期),数组的性能非常好,但是却无处不是坑。内存访问越界和缓存区溢出都和数组有关,还有访问了未初始化数组的元素值,这些可能并不会报错,一旦出问题,只能人肉分析。

正因为这样C++已经不提倡使用裸数组,而是使用stl容器代替。

在C/C++中,数组名实际上是首元素的开始地址,是一个指针常量。

体验AI代码助手

代码解读

复制代码

//创建长度为4的数组
int array[] = {1, 5, 8, 9}
int array1[4]; //此时array1并没有初始化

int array2[i]; //i为变量  error
int a = arr1[2]; //未初始化,元素值未知
int a = arr1[4]; //内存访问越界,元素值未知
arr1[4] = 5; //缓存区溢出

再来看看Java中的数组,数组是Java的内建类型,Java类型分为两大类:原始类型也叫基本类型(primitive type)和引用类型(reference type)。

而引用类型(reference type)又分为三类,分别是类类型(class type)、数组类型(array type)和接口类型(interface type)。

体验AI代码助手

代码解读

复制代码

Java数组的创建
1.int[] array = {1, 5, 7, 9, };
2.int[] array1 = new int[] {1, 5, 7, 9, };
3.int[] array2 = new int[4];
其中1叫做数组初始化器,23成为数组创建表达式

Java可以动态分配数组大小,因为是引用类型,所以是在堆上分配的,这和C语言用malloc分配的相似
int[] array3 = new int[i]; //i为变量

C/C++数组的缺点,Java又是怎么避免的呢,为什么Java数组又饱受诟病?我们详细分析一下。

先来看一下Java规范中的数组。

1.数组中的变量没有名称,只能下标访问。

2.数组一旦被创建,大小就不能改变。

3.数组索引必须是int值(默认转换为int的byte,char,short)。

4.对数组元素的赋值和访问都会进行安全检查。

5.数组创建时元素都会初始化(默认值)。

6.数组的成员:

length(public final int),length>=0

clone() 它覆盖了object类中同名的方法,并且不会抛出任何受检异常。数组类型T[]的clone方法的返回值是T[]

7.数组的直接父类是Object,并实现Cloneable和Serializable接口。

这7条就是Java规范中的数组,从规范中就可以避免C/C++数组的缺点。

体验AI代码助手

代码解读

复制代码

Java安全检查分为两类 1.RangeCheck 2.TypeCheck
String[] strings = {"hello", "world"};
//RangeCheck ArrayIndexOutOfBoundsException
String ele = strings[2];
//error 编译无法通过
strings[0] = 1;
//编译通过,运行期报错(这是数组设计的一个大坑,后面会详细讲)TypeCheck ArrayStoreException
Object[] objects = strings;
objects[1] = 1;
//RangeCheck早于TypeCheck   ArrayIndexOutOfBoundsException
objects[2] = 1;

通过以上及Java规范,可以大致推测JVM数组结构:
class 数组 implements Cloneable,Serializable{
	public final int length;
	public T* array;//C/C++动态分配的内存首地址
	public T[] clone(){};
}
事实上并不是这样,至少Hotspot Vm不是这样的。JVM规范也并没有设置数组的实现。

//在Hotspot上数组declaredFields, declaredMethods,declaredConstructors全是空
//说明数组连默认的构造方法都没有,length属性也不存在,clone方法也没有
int[] array = {1, 5, 7, 9, };
Field[] declaredFields = array.getClass().getDeclaredFields();
Method[] declaredMethods = array.getClass().getDeclaredMethods();
Constructor<?>[] declaredConstructors = array.getClass().getDeclaredConstructors();

实际上,数组和类是区分对待的,创建有着自己的指令 newarray,multianewarray,获取数组的长度使用的是 arraylength指令。这一点我也不懂,因为Java规范中明确规定数组有length域和clone方法。可能因为反射可以修改常量(final)的值,所有Hotspot并没有这样设计,熟悉Hotspot对象模型的都知道数组的长度在其对象模型头的尾部。但是不清楚是不是所有JVM都是这样设计的。

Java数组的类由JVM生成,且类名[ 开头的,数组类是由的内容和维度同时决定的int[]不同于 int[][],类加载器和其元素的加载器一样。

体验AI代码助手

代码解读

复制代码

int[] array = {1, 5, 7, 9, };
System.out.println(array); //[I@685cb137   没有重写toString
System.out.println(array.getClass());//class [I
System.out.println(array.getClass().getClassLoader());//null  和int一样为bootstrap classLoader

//关于clone方法, 此方法为唯一一个继承Object重写的方法,返回值为T[] (不需要再强制装换了)
//但是获取declaredMethods却为空,这是JVM特殊处理吧
int[] array1 = array.clone();

特别注意,数组的克隆,都是值复制。如果是基本类型数组,值复制相当于深克隆了,如果是引用类型数组,值复制相当于复制了引用,两个数组共同持有指向其元素的引用,相当于浅克隆。

还有一点,boolean类型JVM规范并没有给出具体的实现方式,在Hotspot中boolean类型使用int值实现,boolean数组则是使用byte数组实现。

JCF作者Josh Bloch,在《Effective Java》中建议用集合取代数组,Java的数组优点也就length属性和安全检查了,安全检查甚至不一定是优点。我们说说Java数组的缺点和设计失误。

关于规范2,数组长度不能改变这是无论如何都无法解决的,ArrayList,HashMap等集合底层也是创建新的数组实现的扩容。

因为规范3的存在,Java无法创建超大数组,Hotspot中数组最大长度为Integer.MAX_VALUE - 2,所以HashMap和ConcurrentHashMap最大数量为MAXIMUM_CAPACITY = 1 << 30,

体验AI代码助手

代码解读

复制代码

long length = Integer.MAX_VALUE + 1
int[] array2 = new int[length]; //error 无法编译
int[] array3 = new int[Integer.MAX_VALUE] //OutOfMemoryError

关于规范4和5,体现了Java安全大于性能的思想。解决了C/C++裸数组的缺点,但也使Java数组的速度饱受诟病,创建时初始化,添加元素都要RangeCheck和TypeCheck,这些都在运行期而且无法定制。这对于普通用户是非常友好的,但是对一些框架开发者来说,这就是限制了。比如,基于数组的ArrayList,已经是JDK中最快的List,可是有些框架作者还是觉得慢,所以才有hikari的FastList。归更到底如论如何数组的RangeCheck和TypeCheck一定会起作用。加上ArrayList的RangeCheck,相当于两次RangeCheck,FastList则是完全取消了List层面的RangeCheck。有时候数组只是用来从缓冲区获得数据,初始化其实相当于一次无用的操作。

Java数组另一个缺陷是无法创建泛型数组,ArrayList底层是Object数组,所以获取元素都是强制装换的。但是说到底Java整个泛型体系都使用类型擦除(Erasure)实现的。虽然无法创建泛型数组,但是可以创建泛型数组的引用。

体验AI代码助手

代码解读

复制代码

// ArrayList get
transient Object[] elementData;
public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
@SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }


//泛型数组的引用  类型檫除后T[] -> Object[]
private T[] elementData;
this.elementData = (Object[])((Object[])Array.newInstance(clazz, 32));

数组另一个缺陷也是和泛型有关,数组的直接父类是Object,这个规范并不一定准确。Java数组是协变的,当然这个是历史遗留问题,Java5之前还没有泛型,但很多问题需要泛型来解决,比如数组的排序,求最值。如果数组不协变,就不可能有通用的数组方法。所谓协变其实是数学上的术语,当A≤B时有f(A)≤f(B),则关系f是协变的。对应Java数组,因为String extends Object, 所有String[] extends Object[],这样的好处是所有引用类型继承Object,这样所有引用类型数组从现象上继承Object[],这样就可以写出通用的数组处理函数。协变的缺点是什么呢?数组一定会进行TypeCheck,因为协变很多时候编译期的问题无法发现。

基本类型数组的直接父类是Object,并不是Object[],int[]和Integer[]并不能强制装换。

体验AI代码助手

代码解读

复制代码

int[] array = {1, 5, 7, 9, };
//引用类型数组,copy的时候是引用,但是因为Integer是不变类 相当于深克隆
Integer[] integers = {1, 5, 7, 9, }; 
//协变
Object[] objects = {"hello", "world"};
objects[1] = 1;//编译期通过, 运行报错ArrayStoreException

// true 协变
System.out.println(strings instanceof Object[]);
System.out.println(integers instanceof Object[]);
//无法通过编译
System.out.println(array instanceof Object[]);

Java天生支持多线程,不管从高并发到现在并行编程,Java数组这一结构无疑是落寞的。Java层面设计的数组在多线程中只能当一个整体,没有任何API确保数组元素在多线程的同步。Java数组元素无任何final,volatile语义。所以项目中都不会使用数组当共享变量。正是是如此,ConcurrentHashMap即使获取元素锁的情况下也是通过Unsafe putObjectVolatile、getObjectVolatile等API确保数组元素在多线程的同步。而Unsafe是不建议普通开发使用的,这是JVM留的后门,给JDK开发者使用的。

所以,不管是不是多线程使用,普通开发的话还是使用集合代替数组,毕竟集合帮我们屏蔽了数组的缺陷。


转载来源:https://juejin.cn/post/6844904046831075335

相关文章
|
4月前
|
存储 Java 索引
用Java语言实现一个自定义的ArrayList类
自定义MyArrayList类模拟Java ArrayList核心功能,支持泛型、动态扩容(1.5倍)、增删改查及越界检查,底层用Object数组实现,适合学习动态数组原理。
195 4
|
5月前
|
缓存 Java 开发者
Java 开发者必看!ArrayList 和 LinkedList 的性能厮杀:选错一次,代码慢成蜗牛
本文深入解析了 Java 中 ArrayList 和 LinkedList 的性能差异,揭示了它们在不同操作下的表现。通过对比随机访问、插入、删除等操作的效率,指出 ArrayList 在多数场景下更高效,而 LinkedList 仅在特定情况下表现优异。文章强调选择合适容器对程序性能的重要性,并提供了实用的选择法则。
298 3
|
7月前
|
Java 索引
Java ArrayList中的常见删除操作及方法详解。
通过这些方法,Java `ArrayList` 提供了灵活而强大的操作来处理元素的移除,这些方法能够满足不同场景下的需求。
669 30
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
632 4
Java ArrayList扩容的原理
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
安全 Java 程序员
Java集合之战:ArrayList vs LinkedList,谁才是你的最佳选择?
本文介绍了 Java 中常用的两个集合类 ArrayList 和 LinkedList,分析了它们的底层实现、特点及适用场景。ArrayList 基于数组,适合频繁查询;LinkedList 基于链表,适合频繁增删。文章还讨论了如何实现线程安全,推荐使用 CopyOnWriteArrayList 来提升性能。希望帮助读者选择合适的数据结构,写出更高效的代码。
887 3
|
算法 Java 测试技术
数据结构 —— Java自定义代码实现顺序表,包含测试用例以及ArrayList的使用以及相关算法题
文章详细介绍了如何用Java自定义实现一个顺序表类,包括插入、删除、获取数据元素、求数据个数等功能,并对顺序表进行了测试,最后还提及了Java中自带的顺序表实现类ArrayList。
420 0
|
Java
java基础(12)抽象类以及抽象方法abstract以及ArrayList对象使用
本文介绍了Java中抽象类和抽象方法的使用,以及ArrayList的基本操作,包括添加、获取、删除元素和判断列表是否为空。
201 2
java基础(12)抽象类以及抽象方法abstract以及ArrayList对象使用
|
4月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
260 1