您的位置:首页 > 编程语言 > Java开发

[置顶] java常用集合类及其区别、源码分析(一)

2018-03-02 12:30 489 查看
在Java中集合是最常用到的工具类,在各种各样的业务中集合用来存储数据、查找数据等,常用的集合有ArraryList、Vector、LinkedList、HashMap、HashTable、HashSet等,但这些集合在源码上是怎么实现的,最底层是使用什么样的数据结构实现的。

常用的集合可以分为两大类:Collection和Map,这两大类是分别单独实现和定义接口的。

下图列出了常见集合的继承关系和归类。



连接线,实现为类实现,点线为接口继承,段线为抽象类。

Collection

Collection是集合类的根接口,他定义了集合应该具备的基本方法,例如添加、删除数据等。

Collection集合根据功能和数据结构又分为三类:

1.List(有序、可重复)

List里存放的对象是有序的,同时也是可以重复的,List关注的是索引index,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。

2.Set(无序、不能重复)

不允许数据重复的集合。

Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。

3.Queue()

先进后出的队列。

1.List

List的实现有ArraylList、Vector、LinkedList,这几个有什么区别呢。

1.ArrayList

存储数据的实体是数组[],线程不安全,查找速度快,插入速度慢。

2.Vector

和ArrayList有点类似,只是在get、add、remove等方法加了synchronized线程安全关键字,所以它是线程安全的。

3.LinkedList

数据存储的对象是带有前一个元素和后一个元素内存地址的Node对象集合。线程不安全。插入删除速度快。查找速度慢。

1.1ArrayList

ArrayList是List接口的可变数组非同步实现,并允许包括null在内的所有元素。

底层使用数组实现

该集合是可变长度数组,数组扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。

采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险

remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC

先看下ArrayList的关键源代码看下ArrayList为什么查找速度块、插入慢、线程不安全。

下面分别对ArrayList的构造方法、add、remove、get等方法源代码进行分析。

首先看下构造方法和基本属性

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;

/**
* 默认初始化的数组长度。
*/
private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//存储数据的对象是一个数组
transient Object[] elementData; // non-private to simplify nested class access

//当前List元素的数量
private int size;

//初始化指定数组长度的List对象
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);
}
}

//初始化一个默认的List对象
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//用原有的集合来初始化对象
public ArrayList(Collection<? extends E> c) {
//调用toArray()方法获取数组对象
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
//使用Arrays.copyOf方法将原来的数据拷贝到对象的数组中。
//Arrays.copyOf方法底层调用的System.arrayCopy()方法,这个方法是native,由jvm实现。
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}


由上面方法可以看出ArrayList实际上是一个对数组封装的对象,并且提供对数组操作的各种方法。

看下add方法如何实现,add方法分为向尾部添加元素和向索引位置添加元素。

//向尾部添加元素
public boolean add(E e) {
//判断数组长度是否可以容纳新的元素,下面说这个方法。
ensureCapacityInternal(size + 1);
//在数组指定位置赋值。
elementData[size++] = e;
return true;
}

//判断数组长度是否可以容纳新的元素
private void ensureExplicitCapacity(int minCapacity) {
//这里的modCount是记录修改对象的次数,用来判断线程是否安全,当多个线程操作list的时候就会造成modcount的值和预期的值不相等的时候就会抛出ConcurrentModificationException()异常,说明多个线程操作list,造成数据有误。
modCount++;
//如果插入后元素的数量小于数组的长度就不用扩展数组长度
//反之,就扩展数组的长度,看下grow方法如何扩展数组长度。
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//当前数组长度
int oldCapacity = elementData.length;
//新的数组长度=当前数组长度+当前数组长度向右移一位
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//使用System.arrayCopy()方法将原来的数据拷贝到新的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

//下面看看另外一个add方法。向指定索引位置添加元素
public void add(int index, E element) {
//检查索引是否超过数组大小
rangeCheckForAdd(index);
//和上面方法类似,判断是否扩展数组。
ensureCapacityInternal(size + 1);  // Increments modCount!!
//从索引位置开始到结束位置拷贝原始数据,拷贝到原始数据索引+1的位置。
//相当于从索引位置开始,所有数据都向后移位。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//在指定索引位置赋值
elementData[index] = element;
size++;
}


由上面源代码可以看出,ArrayList的add方法是向数组添加元素时

1.首先判断数组长度是否够用,是否需要扩展数组长度。扩展数组长度实际是申请一块新的内存,把原来的数据拷贝到新的数组上面。

2.在指定索引位置赋值时,使用System.arrayCopy()方法将索引位置后面的数据向后移一位

3.在数组的指定索引位置赋值。

4.向尾部添加元素,只需给数组赋值、或者申请一块新的内存把原来的值复制过来再赋值。

5.向中间添加元素,需要申请一块新的内存把索引位置后面所有的数据向后移位。

由此得出ArrayList为什么插入速度慢

get方法是如何实现的

@SuppressWarnings("unchecked")
E elementData(int index) {
//GET方法比较简单,获取指定索引位置的内存即可
return (E) elementData[index];
}

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

return elementData(index);
}


ArrayList数据内存都连续的,所以获取数据非常快。

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;
}


remove的方法和add方法类似,只不过反过来了,remove方法是向索引位置左边移位。同样ArrayList的删除开销也很大。

由上面ArrayList常用的几个方法看出ArrayList是一个线程非安全、插入删除慢、查询速度快的数组封装对象。

1.2Vector

Vector和ArrayList实现思路相似,只不过Vector在很多方法上都加上了synchronized线程安全,所以Vector是线程安全的集合类。

另外,vector数组扩张时是固定大小,且可以指定扩张的大小,而ArrayList每次扩张原来数组长度一半即1.5倍的大小。

1.3Stack

stack翻译就是栈的意思,大学学过算法导论的都知道,栈是后进后出的数据结构。

java里面的Stack类继承自Vector,他只允许从尾部添加数据,从尾部删除数据。

//继承自Vector
public
class Stack<E> extends Vector<E> {
/**
* Creates an empty Stack.
*/
public Stack() {
}

//从尾部添加数据
public E push(E item) {
addElement(item);

return item;
}

//线程安全,从尾部删除数据
public synchronized E pop() {
E       obj;
int     len = size();

obj = peek();
removeElementAt(len - 1);

return obj;
}


1.4LinkedList

LinkedList在实现思路上是一个链表形式的结合。他的每一个元素都标记了他前面元素和后面元素的引用地址。

LinkedList是List接口的双向链表非同步实现,并允许包括null在内的所有元素。

底层的数据结构是基于双向链表的,该数据结构我们称为节点

双向链表节点对应的类Node的实例,Node中包含成员变量:prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值。

它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找,这样最多只要遍历链表的一半节点即可找到

另外,LinkedList虽然查找速度慢,但是从头尾取数据还是很快的,所以它实现了Dequeue队列接口,可以像队列一样从两头取数据。

看下LinkedList基本属性和构造方法。

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;

//第一个元素
transient Node<E> first;

//最后一个元素
transient Node<E> last;

//构造方法并没有执行任何内容。
public LinkedList() {
}

//看下LinkedList存储元素的对象Node
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;
}
}


可见LinkedList是标记前后元素引用的集合对象,元素在内存中排列并不连续。

get方法

public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
//下面根据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;
}
}


如果LinkedList很大,且索引位置在中间,则取到指定索引位置的值是非常慢的。

add方法,这里只说典型的add方法,其它add方法实现大致相同。

public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
//向结尾添加元素
linkLast(element);
else
//向中间位置添加元素
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//获取索引位置前一个元素
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++;
}


删除方法和插入方法类似,LinkedList在插入删除时只需要标记插入元素前一个和后一个元素的引用地址就可以,所以插入速度非常快,整个集合里面的元素并不连续,所以查找起来特别慢。

2.Queue

队列是一种特殊的线性表,它只允许在表的前段(front)进行删除操作,只允许在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。

对于一个队列来说,每个元素总是从队列的尾部进入队列,然后等待该元素之前的所有元素出队之后,当前元素才能出对,遵循先进先出(FIFO)原则。

2.1LinkedList

LinkedList是最常见的Queue实现,但是他是一个两头进、两头出的队列。

LinkedList是链表形式的队列,可以无限添加元素,单单对于队列来讲,他添加元素很快、删除数据也很快(只从头尾删除数据),但是线程非安全。

//向头部添加数据
public boolean offer(E e) {
return add(e);
}

//下面两个方法类似,返回第一个元素并从队列中删除。
//如果队列为空时会抛出NoSuchElementException的异常
public E remove() {
return removeFirst();
}

public E pop() {
return removeFirst();
}

//这个方法和上面两个方法相近,但返回的元素为null时,会返回null
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}


2.2BlockingQueue

BlockingQueue是线程安全的阻塞队列,常见的有ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedBlockingQueue等

阻塞队列常用在线程池中,给线程池提供任务存源和任务存储。

由于篇幅较大,后面专门用一篇来讲。

3.Set

set是不允许集合重复的集合。

常见的set集合有HashSet、LinkedHashSet、TreeSet等。这些类基本都是基于Map实现类进行实现的。这里只介绍到他是用什么map类实现的,真正的原理看后面的map集合实现过程。

3.1HashSet

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;

private transient HashMap<E,Object> map;

// Dummy value to associate with an Object in the backing Map
//哑值,一个固定值,用来存给HashMap的value
private static final Object PRESENT = new Object();

/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
//构造函数会初始化一个HashMap
public HashSet() {
map = new HashMap<>();
}
//添加的元素会作为key添加到HashMap里面
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//删除元素会删除指定key的HashMap
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}


3.2LinkedHashSet

用链表形式的MapLinkedHashMap实现的set集合。

//默认构造方法,真正的实现在HashSet一个封闭的构造函数里面
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
//会初始化一个LinkedHashMap
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//删除和添加方法和上面hashset一样,都是利用map的key、value来实现。


3.3TreeSet

TreeSet由TreeMap实现

public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
/**
* The backing map.
*/
private transient NavigableMap<E,Object> m;

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

/**
* Constructs a set backed by the specified navigable map.
*/
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}

//会初始化一个TreeMap
public TreeSet() {
this(new TreeMap<E,Object>());
}


以上就是Collection接口常见的类实现的原理和数据结构,下一篇介绍map接口常见的类实现原理。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: