几乎所有编程语言都支持数组这种数据结构,功能大同小异,本文主要探讨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叫做数组初始化器,2和3成为数组创建表达式
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开发者使用的。
所以,不管是不是多线程使用,普通开发的话还是使用集合代替数组,毕竟集合帮我们屏蔽了数组的缺陷。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有