Java集合概况
Java集合一直理解的都是片面的,整理一下,将知识组织成面,更便于理解。上图来自Java 集合系列01之 总体框架 - 如果天空不死 - 博客园,虽然博主是基于java1.6整理的,但也不碍于我们学习。理解了上图,对于学习java集合会事半功倍,为了便于理解,我要再复述一下,哈哈。
Java集合框架主要包含三部分,Collection、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)。
Collection是一个接口,包含了集合的基本操作和属性。为了方便编码,又抽象出了AbstractCollection抽象类,我们可以看到List和Set实现类都间接继承自AbstractCollection。Collection主要包含List和Set接口,List和Set的区别在于,List是基于数组,元素有序可重复;Set的实现类是基于Map,元素无序不可重复。
Map是一个映射接口,主要实现类包括HashMap、TreeMap、HashTable等。
Iterator是遍历集合的工具,Collection实现了iterator()函数,所以Collection依赖于Iterator。Enumcration也是遍历集合的工具,只不过仅用于HashTable、Vector、Stack。
Arrays和Collections。它们是操作数组、集合的两个工具类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
ArrayList
可以参考这个博客:Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例 - 如果天空不死 - 博客园;下边理理ArrayList扩容。
ArrayList扩容机制
ArrayList包含了两个重要的对象:elementData 和 size。
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
* 存储ArrayList元素的数组缓冲区,也就是数组的大小,是一个动态数据,如果用无参
* 构造函数创建,list的大小为默认值10;如果用有参构造函数,可以定义list大小
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
* 动态数组的实际大小
* @serial
*/
private int size;
再看下ArrayList的三种创建方式
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
*默认构造函数,使用初始容量10构造一个空列表(无参数构造)
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//初始容量等于0
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
*构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
*如果指定的集合为null,throws NullPointerException。
*/
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;
}
}
由上可知, 以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。
接着看扩容源码
/**
* 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) {
// 判断是否扩容,如果原来的元素个数是size,那么增加一个元素之后的元素个数为size + 1,所以需要的最小容量就为size + 1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 获取数组最小容量,如果elementData为空,且minCapacity <= 10,都会以DEFAULT_CAPACITY作为最小容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果minCapacity大于elementData的长度,使用grow方法进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 扩容方法
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// 原有数组容量
int oldCapacity = elementData.length;
// 新的数组容量,下面位运算相当于newCapacity = oldCapacity * 1.5 向下取整
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新的数组容量小于需要的最小容量,即假设新的数组容量是15,最小需要16的容量,则会将16赋予newCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
/*变量MAX_ARRAY_SIZE = 2147483639 [0x7ffffff7],如果扩容后的新容量大于这个值则会使用hugeCapacity方法
* 判断最小容量minCapacity是否大于MAX_ARRAY_SIZE,如果需要最小容量的也大于MAX_ARRAY_SIZE,则会以
* Integer.MAX_VALUE = 2147483647 [0x7fffffff]的值最为数组的最大容量,如果没有则会以MAX_ARRAY_SIZE最为最大容量
* MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,为什么用MAX_ARRAY_SIZE ,源码的中的说法是一些虚拟机中会对数组保留一些标题字段
* 使用Integer.MAX_VALUE会造成内存溢出错误
* */
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 确定数组最终的容量newCapacity之后,将原有ArrayList的元素全部拷贝到一个新的ArrayList中
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// MAX_ARRAY_SIZE 或者 Integer.MAX_VALUE作为最大长度,而多余的元素就会被舍弃掉
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
ArrayList的扩容机制总结成一句话,就是在第一个添加元素时,创建一个长度为10的数组,之后随着元素的增加,以1.5倍原数组的长度创建一个新数组,即10, 15, 22, 33,。。这样序列建立,将原来的元素拷贝到新数组之中,如果数组长度达到上限,则会以MAX_ARRAY_SIZE 或者 Integer.MAX_VALUE作为最大长度,而多余的元素就会被舍弃掉。
ArrayList的两个remove方法
//参数是Object包装类型,本质上是寻找集合中是否有该元素,有则删除。返回值是布尔类型,有该对象返回true并删除,没有则返回false
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//参数是基本类型int,实现是直接删除下标,返回泛型类型,有则返回该对象,因为是根据下标删除,所以不存在没有的情况,除非下标越界
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
LinkedList
可以参考:Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例 - 如果天空不死 - 博客园、
双向链接实现。LinkedList 同时实现了 List 接口和 Deque 接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList 简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用 LinkedList,一方面是因为 Java 官方已经声明不建议使用 Stack 类,更遗憾的是,Java 里根本没有一个叫做 Queue 的类(它是个接口名字)。关于栈或队列,现在的首选是 ArrayDeque,它有着比 LinkedList(当作栈或队列使用时)有着更好的性能。
ArrayList和LinkedList
这两的对比,我们常常可以总结成一句话:
- ArrayList底层是数组,查询快,增删慢;
- LinkedList底层是链表,查询慢,增删快;
但是,任何东西好坏都是没有绝对的,ArrayList和LinkedList的快慢也是要看场景的,ArrayList查询快,也只是对于索引查询……LinkedList增删快,但是必须要先找到元素。
查询比较
ArrayList底层是数组,存储空间是连续的,根据索引查询,时间复杂度是O(1);但是如果通过元素去查,需要遍历,时间复杂度是O(n);
LinkedList底层是链表,存储空间是不连续的,查询时,需要遍历列表,不断跳转新的地址,所以时间复杂度是O(n);
增删比较
ArrayList增删慢,一是因为ArrayList可以扩容,一旦扩容就会有数组拷贝,扩容和拷贝都是耗时的,如果初始分配恰当容量,扩容也是可以避免的;二是因为如果不是头插或尾插,而是把元素插入到中间,这样就会涉及到元素的移动,也是耗时的;
LinkedList增删快,重在说明的是,元素插入的时候,不涉及元素的移动,只是指针的改变;但是进行增删前,有个前提是需要找到对应的位置,而查找是需要遍历的。所以综合考虑的话,LinkedList增删也不一定快,只是在理想的情况下确实是O(1),理想情况就是头插和头删。
两者哪个更占空间呢?
LinkedList每个Node中包含了三个成员,分别是存储数据的item
,指向前一个存储单元的点 prev
和指向后一个存储单元的节点 next,这么一琢磨,如果存储相同的数据,似乎就是LinkdeList更占空间。。但这只是表面因素。
ArrayList虽然每个节点没有多余信息,但是ArrayList是可以扩容的,
如果刚好数据量超过ArrayList默认的临时值时,那么会有将近原来一半大小的数组空间被浪费了,ArrayList占用的空间也是不小的。不过,因为ArrayList的数组变量是用transient关键字修饰的,如果集合本身需要做序列化操作的话,ArrayList这部分多余的空间不会被序列化。