我是靠谱客的博主 迷你羽毛,最近开发中收集的这篇文章主要介绍java容器---ArrayList学习笔记一、概述二、底层数据结构三、初始化四、扩容机制五、主要方法五、线程安全,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

一、概述

  ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。

 

二、底层数据结构

           ArrayList的底层是一个object数组,并且由trasient修饰


transient Object[] elementData;

在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1),所以在ArrayList中查询速度快,在增删操作效率低于链表。

使用trasient关键字修饰表示数组不支持序列化,但是ArrayList本身是支持序列化的,但是不是直接序列化数组,而是遍历每个数据分别进行io流实现存储容器中对象的序列化。这样做的原因就是容器的存储空间在扩容后会产生很大的闲置空间,序列化会将空的对象空间也进行序列化,而真正存储的空间只有size,造成空间的浪费,效率低下

 private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

 

三、初始化

构造函数 

在ArrayList中,提供了三个构造函数


public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

在第一个是一个带初始容量的构造函数,在实例化是明确指定容量大小,当指定的容量大小为0时,会将一个初始化的空数组EMPTY_ELEMENTDATA赋值给elementData,而使用默认的无参构造函数,会将默认大小空实例的共享空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData。将它从EMPTY_ELEMENTDATA数组中分离出来为了知道在添加第一个元素时扩容是使用默认容量10还是1.5倍扩容。

在ArrayList中存在一个包含指定集合元素列表的构造函数,注意点就是在集合转换为数组的时候进行了一下判断

   if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);

 

为什么需要这个类型判断?

源码的解释就是参数集合c.toArray()返回的可能不是一个Object数组。那究竟是个啥意思,看下一个例子


List list = Arrays.asList(1, 2, 3);
System.out.println(list.getClass());
//class java.util.Arrays$ArrayList
Object[] objects = list.toArray();
System.out.println(objects.getClass());
//class [Ljava.lang.Integer;
// objects[0]=new Object();
运行时会抛出异常java.lang.ArrayStoreException
List<Integer> integers = new ArrayList<>();
System.out.println(integers.getClass());
//class java.util.ArrayList
integers.add(1);
Object[] objects1 = integers.toArray();
System.out.println(objects1.getClass());
//class [Ljava.lang.Object;
objects1[0]=new Object();
//操作可以完成

由上可知,通过Arrays转换的集合,返回的实际类型为java.util.Arrays$ArrayList,而再讲集合转换为数组讲会保留原来的数组类型,,所以不能讲Object对象存入数组。而创建的集合转换为数组为Object类型,所以源码进行判断,如果类型不为Object,使用Arrays.copyOf(elementData, size, Object[].class)来创建1个Object[]数组。

 

四、扩容机制

初始化的容量

之前就学习到了ArrayList底层是一个可以动态扩容的数组,那么究竟是如何实现扩容。

在对ArrayList进行实例化时,如果所未指定容量大小,或者指定容量或集合长度为0时,实例化的容器存储容量为0,

add扩容

那么容器的初始化容量不是默认的10吗?在构造函数中,并没有使用到默认初始化容量DEFAULT_CAPACITY = 10,


public boolean add(E e) {
ensureCapacityInternal(size + 1);
// Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

由源码可知,ArrayList的扩容发生在add元素时,在添加元素前,首先就会检查容器的容量大小是否够用,ensureCapacityInternal方法中传入的参数minCapacity便是最小的容量大小,

在calculateCapacity方法中判断,如果采用的默认构造方法,也就是说在实例化时使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组,便会采用默认初始化容量10. return Math.max(DEFAULT_CAPACITY, minCapacity);返回默认初始化容量10和minCapacity最大值,这样做因为addAll中的扩容可是调用该方法,通过AddAll中传入的最小容量可能大于初始化容量。

真正扩容的方法grow

如果容器的容量大小小于最小需求容量便会调用扩容方法,扩容的方式是通过位运算扩容原容量的1.5倍。再检查新容量是否超出了ArrayList所定义的最大容量,若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,

那么在这里我们为甚选择1.5倍扩容呢?

因为我1.5倍的扩容是最好的倍数。因为一次性扩容太大(例如2.5倍)可能会浪费更多的内存(1.5倍最多浪费33%,而2.5被最多会浪费60%……)。但是一次性扩容太小,需要多次对数组重新分配内存,对性能消耗比较严重。所以1.5倍刚刚好,既能满足性能需求,也不会造成很大的内存消耗。所以如一开始知道容量大小最好直接声明,以减轻性能消耗

 

五、主要方法

 1 添加元素

 源码中你存在四个add方法,可以直接添加,可以在指定索引出添加,如果在指定出添加,首先检查索引是否可用,会调用arraycopy()方法让数组自己     复制自己实现让index开始之后的所有成员后移一个位置:

2删除元素

删除元素时,同样判断索引是否和法,删除的方式是把被删除元素右边的元素左移,方法同样是使用System.arraycopy进行拷贝。

3修改元素

修改元素时,只需要检查下标即可进行修改操作。

       

五、线程安全

   ArrayList是线程不安全的。在其迭代器iteator中,如果有多线程操作导致modcount改变,会执行fastfail。抛出异常。

 public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

最后

以上就是迷你羽毛为你收集整理的java容器---ArrayList学习笔记一、概述二、底层数据结构三、初始化四、扩容机制五、主要方法五、线程安全的全部内容,希望文章能够帮你解决java容器---ArrayList学习笔记一、概述二、底层数据结构三、初始化四、扩容机制五、主要方法五、线程安全所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部