数组的本质

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语言规范》中,数组具有以下几点特点:

  1. 在Java编程语言中,数组是动态创建的对象,可以被赋值给Object类型的变量。Object类的所有方法都可以在数组上调用。
  2. 数组对象包含大量的变量。
  3. 数组的所有元素都具有相同的类型,称为数组的元素类型。如果数组的元素类型为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中数组的继承关系

上面已经验证了,数组是对象,也就是说可以以操作对象的方式来操作数组。并且数组在虚拟机中有它特别的类型。既然是对象,遵循Java语言中的规则---顶层父类是Object。这就说明数组对象可以向上直接转型到Object,也可以向下强制类型转换,也可以使用instanceof关键字做类型判定。这一切都和普通对象一样。

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){
    //如果不适用数组而是依次传递参数,会使参数列表变得冗长,难以阅读
}
posted @ 2018-10-31 13:42  九曜  阅读(612)  评论(0编辑  收藏  举报