Java集合源码学习

Java集合工具包位于java.util包下,包含了很多数据结构。如数组、链表、栈、队列、集合、哈希表等。

Java集合框架大致分为5个部分:List列表、Set集合、Map映射、迭代器(Iterator, Enumeration)、工具类(Arrays, Collections)

Java集合类的整体框架如下:
在这里插入图片描述
集合类可分为Collection和Map两大类。

Collection又分为:
1)List(有序可重复):数组、链表、栈、队列。
实现类常用的有ArrayList, LinkedList, 不常用的有Vector。
由于LinkedList还实现了Queue接口,因此也可以作为队列使用。

2)Set(无序不可重复)
常用实现有HashSet(基于HashMap实现), TreeSet(基于TreeMap实现)
TreeSet还实现了SortedMap接口,因此是有序的集合.

抽象类AbstractCollection、AbstractList、AbstractSet分别实现了Collection、List、Set接口,用这些抽象类去实现接口,在抽象类中实现接口中的若干或全部方法,这样在下面的一些类,只需要直接继承该抽象类,并实现自己需要的方法即可,而不用实现接口中的全部抽象方法。

AbstractMap实现了Map接口的大部分方法,TreeMap, HashMap, WeakHashMap等类都继承自AbstractMap。而HashTable就直接实现了Map接口。

Iterator是遍历集合的迭代器(不能遍历Map, 只能遍历Collection),Collection的实现类都现实了iterator()方法, 该方法返回一个Iterator对象,用来遍历集合。

Arrays和Collections是用来操作数组、集合的两个工具类,例如ArrayList和Vector中大量调用了Arrays.copyOf()方法;而Collections中有很多静态方法可以返回各集合类的synchronized版本。

一。ArrayList

  1. 概述
    ArrayList以数组实现,这样节约空间,但由于数组的长度有限, 超出时会增加50%的容量,用System.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。

按照数组下标访问元素:get(i), set(i)的性能非常高,这是数组的优势。

直接在数组末尾加入元素add(e)性能也很高。但如果按下标插入、删除元素,如add(i, e), remove(i), remove(e), 则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是数组的基本劣势。

数组是简单的数据结构,最重要的一点事它的扩容。如下面的代码:
ArrayList list = new ArrayList();
list.add(“语文: 99”);
list.add(“数学: 98”);
list.add(“英语: 100”);
list.remove(0);

在执行这四句是,内存中的数据这么变化的:
在这里插入图片描述
2. add方法

add方法会将元素放到数组的末尾,具体源码如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

其中,ensureCapacityInternal方法是实现自动扩容的方法,其具体实现如下:

private void ensureCapacityInternal(int minCapacity) {
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(minCapacity);

}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 扩展为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩为1.5倍还不满足需求,直接扩为需求值
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已经不满足需求时,那么就扩容到原来的1.5倍,之后的操作就是把老的数组copy到新的数组中。例如,数组初始大小是10,在加入第11个元素时,会触发扩容:
在这里插入图片描述
3. set和get方法:先做index检查,再赋值

public E set(int index, E element) {
rangeCheck(index);

E oldValue = elementData(index);
elementData[index] = element;
return oldValue;

}

public E get(int index) {
rangeCheck(index);

return elementData(index);

}

  1. remove方法

public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
    // 把后面的往前移
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
// 把最后的置null
elementData[--size] = null; // clear to let GC do its work

return oldValue;

}

二。LinkedList

  1. 概述

LinkedList以双向链表实现。链表无容量限制,但双向链表使用了更多空间,也需要额外的链表指针操作。

按下标访问元素如get(i), set(i, e)要悲剧地遍历链表,将指针移动到位(使用双向链表是为了,如果i > 数组大小的一半,会从末尾向前查找)

在插入、删除元素时,只需要修改节点的指针即可,但还是要遍历部分链表的指针才能移动到下标所指的位置,只有在链表两头的操作如add(), addFirst(), removeLast(), 或用iterator()上的remove()能省掉指针的移动。

LinkedList是采用双向链表的简单的数据结构,如:
LinkedList list = new LinkedList();
list.add(“语文: 1”);
list.add(“数学: 2”);
list.add("英语: 3”);

在这里插入图片描述
2. set和get方法

public E set(int index, E element) {
checkElementIndex(index);
Node x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

这两个函数都调用了node方法,该函数以O(n/2)的性能去获取一个节点,具体实现代码如下:
Node node(int index) {
// assert isElementIndex(index);

if (index < (size >> 1)) {
    Node<E> x = first;
    for (int i = 0; i < index; i++)
        x = x.next;
    return x;
} else {
    Node<E> x = last;
    for (int i = size - 1; i > index; i--)
        x = x.prev;
    return x;
}

}

即判断index在前半区还是后半区,如果在前半区就从前往后查找,如果在后半区就从后往前查找。这样,将节点的访问的复杂度由O(n)变为O(n/2)

  1. Stack:继承自Vector, 数组实现,线程安全,后进先出。

猜你喜欢

转载自blog.csdn.net/shijinghan1126/article/details/87950337