• JDK源码分析(2)之 Array 相关


    在深入了解 Array 之前,一直以为 Array 比较简单,但是深入了解后才发现其实挺复杂的。所以我把重要的写在最前面,但凡遇到和语言本身相关的问题,都可以查阅 Java Language and Virtual Machine Specifications

    一、Array 是一个是对象吗?

    首先可以肯定的是,数组是一个对象;但是在推导的过程中还是有些难以理解的问题,比如对于任意一个引用对象A,

    • 数组是协变的,所以Object[]A[]的父类,即Object[] o = A[];
    • 数组是一个对象,所以数组的父类是Object,即Object oo = o;
    • 那么A[]Object[]Object是什么关系呢?

    是这样吗?
    数组继承关系

    我们可以通过反射来观察一下:

    private static void test05() {
      Object[] o = new String[2];
      System.out.println(o.getClass().getName());
      System.out.println(o.getClass().getSuperclass().getName());
      String[] s = (String[]) o;
      System.out.println(s.getClass().getSuperclass().getName());
      Object oo = s; 
    }
    
    打印:
    [Ljava.lang.String;
    java.lang.Object
    java.lang.Object
    

    可以看见A[]Object[]的直接父类都是Object,所以他们之间的关系也一定不是上图中的多继承关系,那么数组协变产生的关系一定不同于extends关键字产生的关系;

    extends关键字产生的继承关系是怎么定义呢?
    这里我们可以从《Virtual Machine Specifications》中找到答案:

    // ClassFile 结构
    ClassFile {
      u4                magic;
      u2                minor_version;
      u2                major_version;
      u2                constant_pool_count;
      cp_info           constant_pool[constant_pool_count-1];
      u2                access_flags;
      u2                this_class;
      u2                super_class;
      u2                interfaces_count;
      u2                interfaces[interfaces_count];
      u2                fields_count;
      field_info        fields[fields_count];
      u2                methods_count;
      method_info       methods[methods_count];
      u2                attribute_count;
      attributes_info;  attributes[attributes_count]; 
    }
    

    可以看到extends关键字产生的继承关系是记录在class文件中的super_class里面的。我这里还没有在 JDK 源码里面找到数组协变关系的产生,但是可以猜想这个应该是后来加的类似语法关系的结构。这里先留着以后看源码的时候确认吧。

    二、Array 的 length 域相关

    在准备看Array源码的时候,我直接就点开了java.lang.reflect.Array,后来才知道这根本不是Array的源码,看包名就知道,这是使用反射操作数组的一些方法。Array的class是在运行过程中动态生成的。
    那么在Array的class中到底包含了什么呢?在很多的资料中都写了,Array中有类似public final int length的成员变量。但是在《Java Language Specifications》10.1. Array Types中明确写了,length不是类型的一部分;

    • An array's length is not part of its type.
    private static void test06() {
      String[] s = new String[2];
      System.out.println(s.length);
      System.out.println(s.getClass().getDeclaredFields().length);
      try {
      System.out.println(s.getClass().getDeclaredField("length"));
      } catch (NoSuchFieldException e) {
      System.out.println(e.toString());
      } 
    }
    
    打印:
    2
    0
    java.lang.NoSuchFieldException: length
    

    可以看到length并不是Array的成员变量,那么length是从哪里来的呢?
    同样我们可以从ClassFile结构中找打答案;

    数组class结构

    可以看到Arraylength信息是记录在对象头中的,而读取length信息的时候,是使用的arraylength字节码指令来读取的。

    三、Array 的创建流程

    // 数组创建的几种形式
    String[] s = {"a", "b", "c"};  // 初始化器
    String[] s1 = new String[3];   // 有维度表达式
    String[] s2 = (String[]) Array.newInstance(String.class, 3); // 有维度表达式
    

    数组创建流程

    是否有维度表达式:
      无:
        创建的时候每个元素递归深入初始化,失败则退出
        变量类型检查 -> 与数组类型不兼容 -> 编译错误
        不是可具化类型(如:null) -> 编译错误 
        空间不足 -> OutOfMemoryError
      有:
        创建的时候,从左向右地计算,任意维度表达式计算失败则退出
        检查所有维度值,有小于0 -> NegativeArraySizeException 
        分配空间,若空间不足 -> OutOfMemoryError
        只有一个维度表达式,创建一维数组,每个元素初始化化为初始值
        有n个维度表达式,执行深度为n-1的循环
    

    四、协变数组

    1. 逆变与协变

    逆变与协变用来描述类型转换(type transformation)后的继承关系
    如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)

    • f(⋅)是逆变的,当A≤B时有f(B)≤f(A)成立;
    • f(⋅)是协变的,当A≤B时有f(A)≤f(B)成立;
    • f(⋅)是不变的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

    正因为数组是协变的,所以Object[] o = new A[];

    2. 为什么要设计为协变数组

    有种看法认为这是在泛型产生之前的妥协产物,比如在 JDK5 之前还没有泛型,但是很多地方需要用泛型来解决,比如:

    // java.util.Arrays
    public static boolean equals(Object[] a, Object[] a2) {
      if (a==a2)
        return true;
        
      if (a==null || a2==null)
        return false;
        
      int length = a.length;
      if (a2.length != length)
        return false;
        
      for (int i=0; i<length; i++) {
        Object o1 = a[i];
        Object o2 = a2[i];
        if (!(o1==null ? o2==null : o1.equals(o2)))
          return false;
      }
      return true; 
    }
    

    最后调用的是Object.equals()方法,但是不想全部都重写equals,这里最简单的就是让数组实现协变的特性;

    3. 为什么不能使用泛型数组

    这里简单的讲是因为泛型是不变的,而数组是协变的,所以不能使用泛型数组;

    // 如果泛型也是协变的
    private static void test07() {
      List<Object> list = new ArrayList<String>();  // 原本会编译出错
      list.add(123);
      List<String> list1 = list;
      String s = list1.get(0);    // 类型错误
    }
    

    可以看到如果泛型也是协变的,那么Collection 在存取数据的时候,就会产生类型转换错误;

    4. 为什么数组可以是协变的

    private static void test07() {
      Object[] o = new String[2];
      o[0] = 123;
    }
    
    运行时:
    Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
    

    可以看到数组,在存数据的时候,还会检查数据类型是否兼容,所以数组可以是协变的。

    五、数组在 java 和 c++ 中的区别

    • C++ 中的数组只是一个指针,java 中的数组是一个对象
    • java 中访问数组会有额外的范围检查
    • java 中会确保数组被初始化

    六、Array 和 ArrayList的效率对比

    private static final int SIZE = 50000;
    private static final Random RANDOM = new Random();
    
    private static void test_array() {
      System.out.println("Array:");
      long start = System.currentTimeMillis();
      String[] s = new String[SIZE];
      for (int i = 0; i < SIZE; i++) {
        s[i] = i + "";
      }
      
      System.out.println("insert:" + (System.currentTimeMillis() - start));  
      start = System.currentTimeMillis();
      for (int i = 0, len = SIZE * 10; i < len; i++) {
        String ss = s[RANDOM.nextInt(SIZE)];
      }
      
      System.out.println("get:" + (System.currentTimeMillis() - start));
    }
      
    private static void test_list() {
      System.out.println("ArrayList:");
      long start = System.currentTimeMillis();
      List<String> list = new ArrayList<>(SIZE);
      for (int i = 0; i < SIZE; i++) {
        list.add(i + "");
      }
      
      System.out.println("insert:" + (System.currentTimeMillis() - start));
      start = System.currentTimeMillis();
      for (int i = 0, len = SIZE * 10; i < len; i++) {
        String s = list.get(RANDOM.nextInt(SIZE));
      }
      System.out.println("get:" + (System.currentTimeMillis() - start));
    }
    
    打印:
    Array:
    insert:13
    get:10
    ArrayList:
    insert:7
    get:22
    

    对比可以看到,数组的插入和随机访问效率都要比ArrayList高,但是一般建议优先使用列表,只有在优先考虑效率的时候才考虑使用数组,因为

    • 数组是协变的不能使用泛型
    • 数组是具体化的,只有在运行时才知道元素的类型

    七、总结

    在看数组的时候,因为class是动态创建的,所以看了很久,但是根据数组的特性,基本可以认为数组的域和方法,类似于:

    class A<T> implements Cloneable, java.io.Serializable {
      public final int length = X;
      
      public T[] clone() {
      try {
        return (T[]) super.clone();
      } catch (CloneNotSupportedException e) {
        throw new InternalError(e.getMessage());
      }
     }
    }
    
  • 相关阅读:
    【PS技巧】常用概念和功能操作
    【存储】RAID磁盘阵列选择
    【Python 01】Python一种面向对象、解释型计算机程序设计语言
    【PS技巧】如何校正倾斜的图片
    【阿里巴巴大数据实践笔记】第14章:存储和成本管理
    【阿里巴巴大数据实践笔记】第13章:计算管理
    【阿里巴巴大数据实践笔记】第9章:阿里巴巴数据整合及管理体系
    今晚直播丨抢鲜体验-openGauss入门
    详述一则数据库死锁故障的分析过程
    前端学习笔记(一)HTML入门
  • 原文地址:https://www.cnblogs.com/sanzao/p/10098974.html
Copyright © 2020-2023  润新知