前言
List是java重要的数据结构之一,我们经常接触到的有ArrayList、Vector(由于此集合基本上被ArrayList取代,本文不做讲解)和LinkedList三种。他们的类图如下:
数组可变大小: 大概是把数组复制进新数组、扩展
- Vector:内部是数组数据结构;同步的,线程安全;JDK1.0的时候就只有一种集合结构Vector;几乎不用了;增删查询都很慢。
- ArrayList:内部是数组数据结构;不同步;替代了Vector;查询速度快。
- LinkedList:内部是链表数据结构;不同步;增删元素速度快。
数组和链表
- 数组增删慢是因为,牵一发而动全身,要中间加入或去掉一个元素,后面元素都得动,所以比较慢
- 链表增删快是因为,增加元素就是改动前后元素对下一元素或上一元素的指向,删除元素就是把下一元素的地址交给上一元素
- 数组查询快是因为在一遍连续的空间
- 链表查询慢是因为需要丛头开始查找
List讲解
- 添加 void add(index,ele) void add(index,collection)
- 删除 Object remove(index)
- 查询 Object get(index) int indexOf(object) int lastIndexOf(object) List subList(from,to)
- 修改 Object set(index,ele)
list的基本操作
List list = new ArrayList();
list.add("abc1");
list.add("abc2");
list.add("abc3");
System.out.println(list); //[abc1, abc2, abc3]
list.add(1, "222");
System.out.println(list); //[abc1,222, abc2, abc3]
System.out.println(list.remove(1)); // 222
System.out.println(list.set(1, "333")); // abc2
System.out.println(list.get(1)); // 333
System.out.println(list.subList(1, 2)); // [333]
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
//list特有取值方式
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
简单讲了下List通用的方法后,开始学习在实际开发过程中使用最多的ArrayList.
ArrayList详解
- ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
- 它继承于 AbstractList,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。
- 在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)
- ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
- ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
- ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
- ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector(不建议使用) 或者 CopyOnWriteArrayList。
ArrayList的全部方法
List list = new ArrayList();
//add
list.add("hello");
list.add("world");
//AbstractCollection中定义了toString直接输出数组格式
System.out.println(list); //[hello, world]
//size
System.out.println(list.size()); //2
//empty
System.out.println(list.isEmpty()); //false
//add addAll
List list2 = new ArrayList();
list2.add("hello");
list2.add(123); //自动装箱
list.addAll(list2);
System.out.println(list); //[hello, world, hello, 123]
//remove removeAll
list.remove("hello");
System.out.println(list); //[world, hello, 123]
//list.remove(123); //java.lang.IndexOutOfBoundsException
list.remove(new Integer(123));
System.out.println(list); //[world, hello]
list.remove(0);
System.out.println(list); //[hello]
System.out.println(list2); //[hello, 123]
list.add("world");
list.add(123);
list.removeAll(list2); //删除交集
System.out.println(list);
//retainAll
list.add("hello");
list.add(123);
System.out.println(list);
list.retainAll(list2); //交集
System.out.println(list2);
System.out.println(list); //[hello, 123]
//contains containsAll
list.add("world");
System.out.println(list.contains(123)); //true
System.out.println(list.containsAll(list2)); //true
//clear
list2.clear();
System.out.println(list2); //[]
//get
System.out.println(list); //[hello, 123, world]
System.out.println(list.get(0)); //hello
//set
list.set(1, 456);
System.out.println(list); //[hello, 456, world]
//add
list.add(1,true);
System.out.println(list); //[hello, true, 456, world]
//indexOf
System.out.println(list.indexOf(true)); //1
//foreach
for (Object object : list) {
System.out.println(object);
}
//for
for(int i=0;i<list.size();i++) {
System.out.println(list.get(i));
}
//iterator
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
//listIterator
ListIterator iterator2 = list.listIterator();
while(iterator2.hasNext()) {
System.out.println(iterator2.next());
}
while(iterator2.hasPrevious()) {
System.out.println(iterator2.previous());
}
为了进一步吃透ArrayList。开始进行紧张而又刺激的源码学习
- 首先我们看下ArrayList的构造方法
/**
* Default initial capacity //默认容量.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances. 空对象
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
/**
*默认构造函数,使用初始容量0构造一个空列表(无参数构造)
*/
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的构造方法,其中无参构造方法默认创建一个为空的list.若实现可以判断list的大小,我们可以在创建之初就定义其长度大小。了解了ArrayList的创建,下面学习下list添加元素时的过程吧。
ArrayList的扩容机制
//添加方法 add(Object obj)
public boolean add(E e) {
//检测空间容量是否够用
ensureCapacityInternal(size + 1);
//添加元素
elementData[size]=e;
size++;
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//判断list是否为空,若为空,minCapacity和DEFAULT_CAPACITY取较大的那个
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
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);
}
// 从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。
- 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。
注意
- java 中的 length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.
- java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法.
- java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!
在学习ArrayLsit源码中,发现了方法ensureCapacity();这个方法有什么作用呢?那我们就读下这个方法吧~
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
通过这个段代码的学习,我们可以知道: 输入一个容量值,将list与空list进行对比。若两者相等,则minExpand为10,否则为0;
然后将输入值与minExpand进行对比。判断一次扩展多少。
此方法作用为: 最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数
ArrayList<Object> list = new ArrayList<Object>();
final int N = 10000000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
list.add(i);
}
long endTime = System.currentTimeMillis();
System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); //使用ensureCapacity方法前:2096
list = new ArrayList<Object>();
long startTime1 = System.currentTimeMillis();
list.ensureCapacity(N);
for (int i = 0; i < N; i++) {
list.add(i);
}
long endTime1 = System.currentTimeMillis();
System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); //使用ensureCapacity方法后:435
ArrayList操作机制
//在list的某个位置添加元素
public void add(int index, E element) {
//检查index是否在list的范围内
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//源数组,源数组开始位置,新数组,新数组开始位置,拷贝的元素的个数
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
//获取list中下标为index的数据
public E get(int index) {
rangeCheck(index);
return elementData(index);
//其中elementDate(index)函数为:
return (E) elementData[index];
}
//元素的个数size的大小
public int size() {
return size; //用于记录集合中元素的个数
}
//清空list
public void clear() {
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
LinkList学习
LinkedList是一个实现了List接口和Deque接口的双端链表。LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:
List list=Collections.synchronizedList(new LinkedList(...));
LinkList对象属性
transient int size = 0;
/**
* Pointer to first node.
transient Node<E> first;
/**
* Pointer to last node.
transient Node<E> last;
/**
* Constructs an empty list.
*/
public LinkedList() {
}
/**
* Constructs a list containing the elements of the specified
**/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
通过LinkList对象属性可以发现,其底层为双端链表。
LinkList源码–操作
//链表末尾添加一个元素
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//链表头部添加一个元素
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
Arraylist 与 LinkedList 区别
- 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- 底层数据结构:Arraylist底层使用的是 Object 数组;LinkedList底层使用的是 双向链表 数据结构
- 插入和删除是否受元素位置的影响: ① Arraylist采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候,Arraylist会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度应为o(n)因为需要新创立一个新的链表,复制前i-1个元素并在第i位加入新的元素,最后附上n-i个元素。
- 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 Arraylist支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
- 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
总结一下 list 的遍历方式选择:
实现了 RandomAccess 接口的list,优先选择普通 for 循环 ,其次 foreach,
未实现 RandomAccess接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,),大size的数据,千万不要使用普通for循环