一. 数组在内存中是如何存储的?
数组是一种线性数据结构,用来存储 相同数据类型 的一组数据。当创建一个数组的时候必须确定它的大小,系统会在内存中开辟一块连续的内存空间来保存数组,因此数组的容量固定且无法动态改变(空间效率不高)。如果只是定义一个数组变量(引用类型)如 int[] arr,系统仅在栈内存中定义了一个空引用 arr,这个引用并未指向任何有效的内存(内存指的是堆内存中的数据)。数组初始化后 arr = new int[6]; 系统会在堆内存为其分配一块连续的内存空间,此时数组每个元素的默认值都是0,因为例子中定义的是int类型的数组。栈中存储的数组名存的是堆中数组第一个元素的首地址,与 arr[0] 等价。
数组可以存储基本类型数据和引用类型数据。存储基本数据类型时,每个数组元素里存储的是基本类型数据;存储引用类型时,每个数组元素里存储的还是引用,它指向另一块内存。
扩:在方法中定义的一些基本类型的遍历和对象的引用变量都在方法的栈内存中分配空间存储。
堆内存用来存放 new 出来的对象和数组,在堆内存中分配的内存由 java 虚拟机的自动垃圾回收器来管理。
二. 链表
链表在内存中不是连续存储的,可以充分利用内存的碎片空间。
三. ArrayList
3.1 ArrayList 底层是用数组实现的,相当于动态数组,但是扩容操作性能消耗比较大。(当数组容量不够用时,创建一个比原数组容量大的新数组,将原数组的元素“搬到”新数组,再把新添加的元素也放入新数组,最后将新数组赋给原数组)
3.2 ArrayList 查询效率高,增删效率低。
原因:
ArrayList 插入和删除元素时,除非插入和删除的位置都在末尾,否则代码的开销会很大。加入要在下标为 m 的位置插入一个元素,需要先将从下标为 m 开始至末尾的所有元素依次向后移动一位,然后再在下标为 m 的位置插入新的元素。删除同理,删除下标为 m 的元素后,m 之后的所有元素需要向前移动一位。而查询:因为数组在内存上占有一块连续的内存空间,只需要通过下标即可直接拿到想要的元素。故ArrayList 查询效率高,增删效率低。
3.3 ArrayList 效率高,但是线程不安全,允许元素为 null
从哪里看出来 ArrayList 是线程不安全的呢?从源码可以看出。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!判断是否需要扩容,如需要则扩容
elementData[size++] = e;
return true;
}
① 造成下标溢出
假设数组 arr 的长度为10,目前 arr 已经有了 9 个元素,此时线程 A 和线程 B 同时向数组 arr 中增加一个元素,
线程 A 进入 add 方法,此时 size == 9,ensureCapacityInternal 方法判断容联足够,不需要扩容 ;
线程 B 得到同样的结论;
线程 A 成功向数组添加一个元素,而线程 B 添加元素时则发生下标溢出异常(B 在调用 ensureCapacityInternal 方法时应该扩容而未扩容造成的)
② 一个线程添加的值覆盖另一个线程添加的值
elementDate[size++] = e; 其实是分两步执行的,如下:
elementDate[size] = e;
size++;
当 size == 0 时,线程 A 和线程 B 同时向 animal 添加元素,
线程 A 把新的元素 dog 放到 animal[0],此时还没有执行 size++;
线程 B 又把新的元素 cat 放到 animal[0],就覆盖了 线程 A 的添加的值;
线程 A 和 B 都执行完之后,animal[0]=cat , animal[1]=null,size=2
解决 ArrayList 的线程不安全的办法是:借助 Collections 里面的 synchronizedList 方法,
List list = Collections.synchronizedList(new ArrayList());
但是非常消耗性能。如果想用线程安全的可以选择 Vector。
除了线程不安全之外,ArrayList 基本等同于 Vector。
四. LinkedList
Linkedlist 增删效率高,查询效率低