Java中的数组是对象吗
要判断数组是不是对象,那么首先明确什么是对象,也就是对象的定义。在较高的层面上,对象是根据某个类创建出来的一个实例,表示某类事物中一个具体的个体。对象具有各种属性,并且具有一些特定的行为。而在较低的层面上,站在计算机的角度,对象就是内存中的一个内存块,在这个内存块封装了一些数据,也就是类中定义的各个属性,所以,对象是用来封装数据的。以下为一个Person对象在内存中的表示:
那么在Java中,数组满足以上的条件吗?在较高的层面上,数组不是某类事物中的一个具体的个体,而是多个个体的集合。那么它应该不是对象。而在计算机的角度,数组也是一个内存块,也封装了一些数据,这样的话也可以称之为对象。以下是一个数组在内存中的表示:
这样的话, 数组既可以是对象,也可以不是对象。至于到底是不是把数组当做对象,全凭Java的设计者决定。数组到底是不是对象, 通过代码验证:
int[] a=new int[4];
//数组可以访问属性
int len = a.length;
//数组可以调用方法
a.clone();
a.toString();
Java中的数组具有其他对象的一些特点:封装了一些数据,可以访问属性,也可以调用方法,所以Java中的数组是对象。再来对比C++ 中的数组:
int main(){
int a[] = {1, 2, 3, 4};
int* pa = a;
//无法访问属性,也不能调用方法。
return 0;
}
C++ 中的数组虽然封装了数据,的那还是数组名只是一个指针,指向数组中的首个元素,既没有属性也没有方法可以调用,所以C++ 中的数组不是对象,只是一个数据的集合。
Java中数组的类型
int[] arr1 = {1, 2, 3, 4};
System.out.println(arr1.getClass().getName());
//打印出的数组类的名字为[I
String[] arr2 = new String[2];
System.out.println(arr2.getClass().getName());
//打印出的数组类的名字为 [Ljava.lang.String;
String[][] arr3 = new String[2][3];
System.out.println(arr3.getClass().getName());
//打印出的数组类的名字为 [[Ljava.lang.String;
数组也是有类型的。只是这个类型显得比较奇怪。你可以说arr1的类型是int[],这也无可厚非。但是我们没有自己创建这个类,也没有在Java的标准库中找到这个类。也就是说不管是我们自己的代码,还是在JDK中,都没有如下定义:
public class int[] {
// ...
// ...
// ...
}
数组类型的创建
在《Java语言规范》中,数组具有以下几点特点:
- 在Java编程语言中,数组是动态创建的对象,可以被赋值给Object类型的变量。Object类的所有方法都可以在数组上调用。
- 数组对象包含大量的变量。
- 数组的所有元素都具有相同的类型,称为数组的元素类型。如果数组的元素类型为T,那么数组自身的类型就写作T[ ]。
int[] arr1 = {1,2};//隐式地创建一个新的数组对象
int[] arr2 = new int[2];//显式地创建一个数组对象
Object o = arr2;//数组对象可以赋值给Object类型的变量
o = arr1;
通过上面的代码片段,再联系“数组是动态创建的对象”这句话,我们可以猜测:数组的类型很可能是运行时通过反射动态创建的,并且其类型是Object的子类。
数组类型的成员
- public final 属性 length,它包含了数组的元素数量。length可以是正数或0。
- public 方法 clone,它覆盖了Object类中的同名的方法,并且不会抛出任何受检异常。
多维数组的克隆是浅复制,即它只创建单个新数组,子数组是共享的。 - 所有从Object类继承而来的成员。
下面的例子说明多维数组克隆后共享子数组:
int ia[][] = { { 1, 2 }, null };
int ja[][] = ia.clone();
System.out.print((ia == ja) + " ");
System.out.println(ia[0] == ja[0] && ia[1] == ja[1]);
数组的Class对象
每个数组都与一个Class对象关联,并与其它具有相同元素类型的数组共享该对象。尽管数组类型不是类,但是每一个数组的Class对象起到的作用看起来都像是。
class A {
}
class B extends A implements Comparable<B> {
@Override
public int compareTo(B o) {
return 0;
}
}
public class Test {
public static void main(String[] args) {
B[] bArr1 = new B[2];
B b = new B();
B[] bArr2 = new B[2];
//测试数组的Class对象是共享的
System.out.println(bArr1 == bArr2);
//输出:false
System.out.println(bArr1.getClass() == bArr2.getClass());
//输出:true
//测试数组bArr1和b的Class对象是否一样
System.out.println(bArr1.getClass() + " | " + b.getClass());
//输出:class [Lcom.ninety.B; | class com.ninety.B
//测试数组bArr2和b的父类是否一样
System.out.println(bArr1.getClass().getSuperclass() + " | " + b.getClass().getSuperclass());
//输出:class java.lang.Object | class com.ninety.A
//测试数组bArr1和b实现的接口分别是什么
for(Class<?>c : bArr1.getClass().getInterfaces()){
System.out.println("Superinterfaces: " + c);
}
//输出:
//Superinterfaces: interface java.lang.Cloneable
//Superinterfaces: interface java.io.Serializable
for(Class<?>c : b.getClass().getInterfaces()){
System.out.println("Superinterfaces: " + c);
}
//输出:Superinterfaces: interface java.lang.Comparable
}
}
其中,字符串“[Lcom.ninety.B”是“元素类型为com.ninety.B的数组”的Class对象的运行时类型签名。
根据上面的输出结果,可得出以下总结:
- 数组的Class对象是共享的。
- 虽然每个数组都与一个 Class 对象关联,但数组的Class对象并不等于数组元素的Class对象。
从上面这个输出可以看出:class [Lcom.ninety.B; | class com.ninety.B - 数组的类型是Object类的子类,并且数组的类型和数组元素的类型不一样。
如上面的输出中, B[] 的父类是Object,而B的父类是A。B[]类型实现的是Cloneable 和 Serializable 接口,而B类实现的是Comparable接口。
Java中数组的继承关系
int[] arr1=new int[2];
//将arr1向上转型为Object
Object o=arr1;
int[] arr2=(int[])o;
//使用instanceof关键字判定
if(o instanceof int[]){
System.out.println("o的真实类型是int[]");
}
由上文的验证可以得知数组类型的顶层父类一定是Object,那么下面的代码很容易让我们疑惑:
String[] s = new String[5];
//可以用Object[]的引用来接收String[]的对象
Object[] obja = s;
Object[]类型的引用可以指向String[]类型的数组对象?数组类型的顶层父类一定是Object,那么上面代码中s的直接父类是谁呢?难道说String[]继承自Object[],而Object[]又继承自Object? 让我们通过反射的方式来验证这个问题:
System.out.println(s.getClass().getSuperclass().getName());
//打印结果为java.lang.Object,说明String[] 的直接父类是 Object而不是Object[]
由代码可知,String[]的直接父类就是Object而不是Object[]。可是Object[]的引用明明可以指向String[]类型的对象。除非String[]不可能即继承Object,又继承Object[]。这样的话就违背了Java单继承的原则。那么只能这样解释:数组类直接继承了Object,关于Object[]类型的引用能够指向String[]类型的对象,这种情况只能是Java语法之中的一个特例,并不是严格意义上的继承。也就是说,String[]不继承自Object[],但是我可以允许你向上转型到Object[],这种特性是赋予你的一项特权。
其实这种关系可以这样表述:如果有两个类A和B,如果B继承(extends)了A,那么A[]类型的引用就可以指向B[]类型的对象。如下代码所示:
class A {
}
class B extends A {
}
public class Test {
public static void main(String[] args) {
B[] bArr = new B[2];
A[] aArr = bArr;
System.out.println(bArr.getClass().getSuperclass().getName());
//class java.lang.Object,说明B[]的直接父类是Object
}
}
上面的结论可以扩展到二维数组和多维数组:
B[][] bArr = new B[2][4];
A[][] aArr = bArr;
上面的代码可以这样理解:
将A[][]数组看成是一维数组,这是个数组中的元素为A[],将B[][]数组看成是一维数组,这是个数组中的元素为B[],因为A[]类型的引用可以指向B[]类型的对象,所以,根据上面的结论,A[][]的引用可以指向B[][]类型的对象。
数组的这种用法不能作用于基本类型数据:
int[] aArr = new int[4];
//错误的,不能通过编译
//Object[] oArr = aArr;
这是错误的, 因为int不是引用类型,Object不是int的父类,在这里自动装箱不起作用。但是这种方式是可以的:
Object[] objss = {"aaa", 1, 2.5};
这种情况下自动装箱可以工作,也就是说,Object数组中可以存放任何值,包括基本数据类型。这种特性主要是用于方法中参数的传递。如果不传递数组,而是依次传递各个值,会使方法参数列表变得冗长。如果使用具体的数组类型,如String[],那么就限定了类型,失去了灵活性。所以传递数组类型是一种比较好的方式。但是如果没有上面的数组特性(如果有两个类A和B,如果B继承(extends)了A,那么A[]类型的引用就可以指向B[]类型的对象),那么数组类型就只能通过Object类型接收,这样就无法在方法内部访问或遍历数组中的各个元素。如下代码:
private static void test() {
String[] a = new String[3];
doArray(a);
}
private static void doArray(Object[] objs){
}
private static void doArray1(Object obj){
//不能用Object接收数组,因为这样无法对数组的元素进行访问
// obj[1] //错误
//如果在方法内部对obj转型到数组,存在类型转换异常的风险
// Object[] objs = (Object[]) obj;
}
private static void doArray2(String[] strs){
//如果适用特定类型的数组,就限制了类型,失去灵活性和通用性
}
private static void doArray3(String name, int age, String id, float account){
//如果不适用数组而是依次传递参数,会使参数列表变得冗长,难以阅读
}