简单说明
Java中的集合类包括Collection和Map两大分支,这篇文章简单的描述下我们在写代码过程中常用的Collection。
从上图可以知道,Collection派生了三大子分支,分别是Set、List、Queue。这三个分支的区别如下:
1、List:必须保持元素特定的顺序
2、Set:不能有重复元素
3、Queue:保持一个队列(先进先出)的顺序
层次关系
<b>Iterable</b>
由上图可以知道,Collection的父类是一个迭代器,所以Collection的实现类都能进行foreach遍历(具有可遍历性),Iterable唯一实现的接口是iterator(),当然我们可以通过查看Iterable源码可发现有两个以实现的方法:forEach()/spliterator(),Java为何在一个接口中具有两个实现的方法,我们留在以后讲解。
<b>Collection</b>
上面也说了Collection派上了三个分支(List/Set/Queue),这三个分支也有不同的使用场合。
<b>Collection之Set</b>
Set集合类似于一个罐子,"丢进"Set集合里的多个对象之间没有明显的顺序。Set继承自Collection接口,不能包含有重复元素(记住,这是整个Set类层次的共有属性)。
Set判断两个对象相同不是使用"=="运算符,而是根据equals方法。也就是说,我们在加入一个新元素的时候,如果这个新元素对象和Set中已有对象进行注意equals比较都返回false,则Set就会接受这个新元素对象,否则拒绝。因为Set的这个制约,
在使用Set集合的时候,应该注意两点:
- 为Set集合里的元素的实现类实现一个有效的equals(Object)方法。
- 对Set的构造函数,传入的Collection参数不能包含重复的元素。
下面我们通过源码分析以下HashSet,它其实通过一个HashMap(Hash算法存储)的key来控制的一个数据结构。
public HashSet() {
map = new HashMap<>();
}
通过他的构造方法来创建一个map值
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
他的add方法就是对map对象的赋值,改map的key为e,value为一个固定的值PRESENT,该值就是一个普通的Object对象
private static final Object PRESENT = new Object();
他的remove/clear方法亦是如此。
下面我们看看他实现的迭代器的iterator()方法
public Iterator<E> iterator() {
return map.keySet().iterator();
}
关于HashMap的内容我们下一个章节详细讲述下。
<b>Collection之List</b>
List是编程过程中最常用的数据结构,它必须按照指定的顺序规则来进行,例如ArrayList是按照数据存储的位置来存储的(有序的),而LinkedList是一种链表,按照指针索引来提取数据的(无序的)
<b>下面简单的说一说ArrayList和LinkedList</b>
<b>1、ArrayList</b>
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);
}
}
transient Object[] elementData;
通过ArrayList的构造方法,我们可以发现,在ArrayList初始化的过程中,他创建了一个有指定大小的不参与序列化(transient)的elementData数组或者大小的0的空数组。
通过他的add方法一步步的走下去我们可以看到他的grow方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
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);
}
我们可以发现grow其实就是给elementData对象扩容的方法。他的remove方法就是add的逆过程,将指定位置(index)或者指定的Object移除。
<b>1、LinkedList</b>
LinkedList类是双向列表,列表中的每个节点都包含了对前一个和后一个元素的引用,是一个无序的数据结构
LinkedList 他有两个重要的Node对象,通过他们来实现LinkedList作为List的基本操作。
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
下面是他的add方法
public void addLast(E e) {
linkLast(e);
}
public void addFirst(E e) {
linkFirst(e);
}
/**
* Links e as last element.
*/
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++;
}
/**
* Links e as first element.
*/
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++;
}
通过上面代码可以发现linkFirst 和linkLast分别对first/last两个节点进行了操作.
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> 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;
}
}
上面代码是获取指定位置对象的方法,有node方法可以看出,双向链表可以快速的查询到结果,当index小于总大小一半的时候,采用first节点查找,反之采用last查找。
说了这么多ArrayList和LinkedList,那么他们到底有什么区别?
1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
3.LinkedList不支持高效的随机元素访问。
4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了
<b>Collection之Queue</b>
Queue在java中的使用率可能没有前面两种那么高,但是作为Collection一份子,它也有他存在的价值。
Queue,顾名思义就是队列(先进先出的数据结构),队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。
因为LinkedList也实现了Queue,那么我们依然从LinkedList来说说Queue。首先Queue自身除了继承了Collection方法外,另外也有自己方法。
offer: 一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。
poll:和Collectio的remove方法相似,但是remove方法抛出异常, poll() 方法在用空集合调用时不是抛出异常,只是返回 nul
peek/element:element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。
public boolean offer(E e) {
return add(e);
}
这是linkedList的offer方法,可以看出他就是调用了add方法。
public E poll() {
final Node<E> f = first;
return (f == null) ? null : 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;
}
从上面可以,poll()方法把first的第一条记录提取出来(最先进去的)并返回。
另外Queue还有两个重量级的实现类,这两个留着大家自己看看吧:
BlockingQueue (阻塞算法)
ConcurrentLinkedQueue(非阻塞算法)
好吧,Collection暂时说到这些,更加深度的留在以后再细说
网友评论