集合的分类
首先来看一下集合的整体分类。
Collection和Set中文都可以翻译成集合。但是从Java编程角度,Collection应该被翻译成容器,Set翻译成集合。
Collection和Set中文都可以翻译成集合。但是从Java编程角度,Collection应该被翻译成容器,Set翻译成集合。
List | Set | |
---|---|---|
顺序性 | 有序 | 无序 |
重复性 | 可重复 | 不可重复 |
索引 | 可通过索引操作元素,即可使用普通for循环遍历 | 没有索引,即不可使用普通for循环遍历。 |
1.ArrayList
基本原理及优缺点
数组的长度是固定的,比如为100,当元素数量超过了100以后会扩容,把以前的数组拷贝到新的数组里。
缺点是:
- 数组扩容+元素拷贝的过程,会慢一些,所以不要频繁插入数据。
- 要是往数组的中间加一个元素,新增元素后面的全部元素需要往后面挪动一位,性能较差。
优点是:
- 因为基于数组来实现,可以通过内存地址来定位某个元素,所以在随机获取数组里的某个元素的时候,性能很高。
适用场景:
- ArrayList的元素按照插入的顺序来排列。
- 适用于查多插入频率低的场景。
源码分析
构造函数
默认的构造函数,会将内部的数组做成一个默认的空数组。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
默认的初始化数组的大小是10,但是不是在初始化的时候进行赋值的,详看 List 数组扩容
,是在第一次add中进行赋值的。
默认的值太小了,所以构造ArrayList一般会给一个大小,比如100个数据,避免数组太小。
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);
}
}
add()方法的源码
会导致数组元素的移动。
每次往ArrayList中添加数据,都会判断当前数组的元素是满了。
如果满了,就会扩容数组,然后将老数组中的元素拷贝到新数组中。
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
set()方法的源码
public E set(int index, E element) {
// 检查是否越界
rangeCheck(index);
// 获取原本的值
E oldValue = elementData(index);
// 设置新值
elementData[index] = element;
// 返回原本的值
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
transient Object[] elementData;
@SuppressWarnings("unchecked")
E elementData(int index) {
// 获取到i位置原本的值,李四
return (E) elementData[index];
}
add(index, element)方法的源码
public void add(int index, E element) {
// 判断越界
rangeCheckForAdd(index);
// 确保数组能添加这个元素,直接+1
ensureCapacityInternal(size + 1);
// 进行数据拷贝,看注释
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 上面相当于是数据向后移动一格,当前index进行赋值
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
// 将elementData从第1位开始,拷贝到第二位,总共拷贝两个元素
// System.arraycopy(elementData, 1, elementData, 2, 2);
get()方法的源码
从数组中直接取出元素,是优点所在。
public E get(int index) {
// 检查数组越界
rangeCheck(index);
// 直接返回
return elementData(index);
}
remove()方法的源码
会导致数组元素的移动。
public class ArrayList<E> {
public E remove(int index) {
// 检查越界
rangeCheck(index);
// list长度变化的次数:增、删
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 相当于把index+1位置的元素,全部往前移动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 末尾那位设置为null,让垃圾回收去回收对象
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
}
public abstract class AbstractList<E> {
protected transient int modCount = 0;
}
数组扩容以及元素拷贝
默认情况,第一次add(),会给数组默认赋值10。
从add()
方法中进入。
假设一个数组的大小是10,且已经添加了10个元素,此时数组的size = 10,capacity = 10。
此时调用add()方法插入一个元素,无法插入第11个元素。
默认是扩大1.5倍。
transient Object[] elementData;
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 final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// elementData已经填充了10个元素了,minCapacity = 11
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果是空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 跟默认的10去比
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 表示是要扩大数组
// elementData.length默认就是10
if (minCapacity - elementData.length > 0)
// 进行扩容
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// oldCapacity >> 1 = oldCapacity / 2 = 5
// newCapacity = 15
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 还是太小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 新数组太大
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 得到一个超大的值
newCapacity = hugeCapacity(minCapacity);
// 完成老数组到新数组的拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
2.LinkedList
基本原理以及优缺点
LinkedList,底层是基于双向链表数据结构来实现的。
优点:
- 大量的插入时不需要像ArrayList那样去扩容,不断的把新的节点挂到链表上就可以了。所以适合频繁的插入和删除操作。
- LinkedList可以当做队列来用,先进先出,在list尾部怼进去一个元素,从头部拿出来一个元素。
缺点:
- 不适合获取元素,因为需要遍历整个链表,直到找到index = 10的这个元素。
插入元素的原理
add()
在双向链表的尾部插入一个元素。
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
// 原本的尾节点
final Node<E> l = last;
// 封装一个新的node 具体代表的含义如下类所示
final Node<E> newNode = new Node<>(l, e, null);
// 进行覆盖
last = newNode;
// 表示原本就是空List
if (l == null)
// 重新给头结点赋值
first = newNode;
else
// 改变原本l的下一个指向
l.next = newNode;
size++;
// list长度是有变化的
modCount++;
}
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
add(index, element)
是在队列的中间插入一个元素。
public void add(int index, E element) {
// 保证>=0 <=size,直接点进去看,很简单
checkPositionIndex(index);
// 插在队尾
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
// 获取index位置的node
Node<E> node(int index) {
// 插入位置在list的前半部分
if (index < (size >> 1)) {
// 从头部开始遍历,得到index位置的节点
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 插入的位置在队列的后半部分
else {
// 从尾部开始逆着遍历,得到index位置的节点
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// index 位置的 Node,前一位 Node 需要指向 值为e的node
final Node<E> pred = succ.prev;
// 新增的节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 重新指向,如上
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
addFirst()
在队列的头部插入一个元素。
public void addFirst(E e) {
linkFirst(e);
}
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++;
}
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
addLast()
跟add()方法是一样的,也是在尾部插入一个元素。
public void addLast(E e) {
linkLast(e);
}
获取元素的原理
poll(),从队列头部出队
peek(),获取队列头部的元素,但是头部的元素不出队
getFirst()
getFirst() == peek()
获取头部的元素,直接返回first指针,指向的Node里的数据。
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
transient Node<E> first;
getLast()
获取尾部的元素。
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
get(int index)
这个方法性能差,使用node(index)
这个方法(),要进行链表的遍历。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 获取index位置的 node
Node<E> node(int index) {
// 插入位置在list的前半部分
if (index < (size >> 1)) {
// 从头部开始遍历,得到index位置的节点
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
// 插入的位置在队列的后半部分
else {
// 从尾部开始逆着遍历,得到index位置的节点
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
删除元素的原理
removeLast()
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
// 让jvm的垃圾回收自动去收
l.item = null;
l.prev = null; // help GC
// 链表的重新指向
last = prev;
// 原本只有一个元素
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
removeFirst()
removeFirst() == poll()
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
// 双向链表的重新指向
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
remove(int index)
public E remove(int index) {
// 不越界
checkElementIndex(index);
return unlink(node(index));
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
// 前节点的指向
prev.next = next;
// 要删除节点本身的前后指向,也要指向null,让垃圾回收回收掉
x.prev = null;
}
if (next == null) {
last = prev;
} else {
// 后节点的指向
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
注
可能一篇笔记,找了很多篇文章才解决一个问题,如有引用未贴的来源,请告知补上,水平有限,如有错误欢迎评论。