Java中为什么要有ArrayList这个集合类?
ArrayList底层的数据结构是采用数组来实现的,我们都知道数组这种结构的优点:访问数组中的元素所耗费的时间都是常数时间。但是还有一个大的缺点就是数组是定长的,也就是我们一旦创建了数组,就必须指定它的存储空间大小,而它的存储空间大小一旦指定就不会再发生改变,显然,这个缺点制约了数组在很多场合下的应用,因此,在保证常数时间访问的同时,又要实现可变长的数组,就出现了ArrayList这个集合类。
ArrayList是怎么实现数组优点的同时,又解决了数组的缺点?
聪明的你一定能想到,利用数组作为存储集合数据的成员变量,所有的集合操作都是围绕存储集合数据的数组来进行的,显而易见,实现了利用数组的优点,但如果只限于此,还是大小不可变的数组。故而,通过数组复制并重新赋值数组对象的方式来实现可变长的数组。简而言之,ArrayList是采用数组复制并重新赋值到数组对象的方式来实现的。
引申一点,利用数组作为ArrayList的成员变量,为什么要加transient修饰?
要说明这个问题首先就得说一下为什么ArrayList需要序列化?ArrayList被广泛应用于数据传输或者数据存储等,这些操作都需要将ArrayList的实例对象转换成对象输出输入流来完成。而实现Serializable接口就表示该集合对象可以转成输入输出流进行操作。
ArrayList只有两个成员变量elementData和size,实现Serializable接口在序列化时只会将该类的非静态和非transient修饰的fields进行序列化。ArrayList的设计者将elementData用transient修饰标识不将它进行自动序列化,因为传输或者存储的关注点在于数据,但是在传输或者存储的过程中,还是要以数据原来存在的结构状态(对于ArrayList就是有序的),所i以为了保证序列化前和反序列化后对象状态的一致,设计者对elementData进行手动序列化(通过writeObject和readObject方式)。看源代码也知道,只是将集合中的元素和集合大小进行了序列化。还要序列化集合大小是因为要考虑到反序列化时利用集合大小创建数组以存放序列化的元素。
总结,我们在考虑是否用transient修饰时考虑两个场景1)业务需要2)性能因素,比如前端传输了5个变量,后端用一个实体类全部接收,但返回的时候只返回这5个变量中的4个,就想到用transient,同时要对另一个进行手动序列化readObject和writeObject,这样避免新建另一个实体类。
引申一点:jdk1.8相比1.7,在ArrayList多了几个静态成员变量为什么?
jdk1.7在设计ArrayList有一个不合理之处,试想这样的一种情况:1)1.7创建ArrayList时不管用不用默认就是开辟10个空间,不合理 2)有一个公共方法trimToSize(),1.7中只有一个判断,试想当如果最后ArrayList的size大小为0时候,ArrayList的结构其实就是创建时的默认结构(10个空间)由于没有考虑为0的情况,当进行数组复制的时候,不会创建出10个空间的数组。因此,1.8对于这些情况都做了完善的考虑:新增了几个数组成员变量EMPTY_ELEMENTDATA,DEFAULTCAPACITY_EMPTY_ELEMENTDATA。那么它们为什么是静态类型的?为什么不是transient?第一问:我们知道静态的和transient修饰的都不会参与到序列化。第二问:可能基于这两个成员变量是类属性而不是实例属性。从1.7就可以看出ArrayList其实只需要两个实例属性elementData和size。最重要的一点应该是它们的初始化时机都要在实例属性之前。
ArrayList的含Collection参数的构造方法中为什么要判断toArray()方法返回的是不是Object类型的?
Collection接口中声明的toArray()方法返回的是Object数组,但如果有一个类继承了ArrayList并重写了toArray()方法返回为String类型的数组,当把这个继承了ArrayList的类作为参数传入到ArrayList(Collection c)的构造方法中,调用c.toArray()方法返回的是String类型的数组,而不是Object数组。这里涉及到方法调用:包含解析调用和分派调用。而JVM规定:只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本。符合这个条件的有:静态方法,私有方法,实力构造器和父类方法这4类以及被final修饰的方法,它们在类加载的时候就会把符号引用解析为该方法的直接引用。
引申一点:除了解析调用,jvm还有分派调用。