List,Set,Map三者的区别与联系
总的来说:
- List-列表(常用来顺序存储): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
- Map-映射表(用Key来快速搜索): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
- Set-集合(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。
Arraylist 与 LinkedList
- 是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全; - 底层数据结构:
Arraylist
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构 (JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别;至于为啥取消了,暂时不得而知,欢迎在下方留言交流)
- 双向链表: 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。
- 双向循环链表: 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。
-
Arraylist
声明数组源码
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
-
LinkedList
部分源码
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
/**
* 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;
......
此处省略部分代码
......
/**
* 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++;
}
/**
* 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++;
}
......
此处省略部分代码
......
//可以看到添加方法往链表最后添加了一个
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Inserts the specified element at the specified position in this list.
* Shifts the element currently at that position (if any) and any
* subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
LinkedList
更适合增加/删除: ①ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候,ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)
。但是如果要在指定位置i
插入和删除元素的话(add(int index, E element))
时间复杂度就为O(n-i)
。因为在进行上述操作的时候集合中第i
和第i
个元素之后的(n-i)
个元素都要执行向后位/向前移一位的操作。 ②LinkedList
采用链表存储,所以插入,删除元素只需要修改前后向节点指针指向的位置, 时间复杂度不受元素位置的影响,都是近似O(1)
而数组为近似O(n)
。很显然LinkedList
更适合增加删除较多的代码场景ArrayList
适合快速随机访问: 通过上面的分析我们知道ArrayList
底层是数组实现,数组的特点就是内存分配在一整块,相邻数据的地址紧挨着,在进行读取访问时可以根据第一个数据的地址位置快速计算得到指定位置的内容. 而LinkedList
则必须采用遍历节点的方式挨个查找下一个内容的位置,直到所需要的深度,不能像数组一样通过元素的序号快速获取元素对象(对应于get(int index)
方法)- 内存空间占用:
ArrayList
的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间(这跟扩容机制有关),而LinkedList
的空间花费则体现在它的每一个元素都需要消耗比ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)。
RandomAccess接口
/**
* Marker interface used by <tt>List</tt> implementations to indicate that
* they support fast (generally constant time) random access. The primary
* purpose of this interface is to allow generic algorithms to alter their
* behavior to provide good performance when applied to either random or
* sequential access lists.
*
* <p>The best algorithms for manipulating random access lists (such as
* <tt>ArrayList</tt>) can produce quadratic behavior when applied to
* sequential access lists (such as <tt>LinkedList</tt>). Generic list
* algorithms are encouraged to check whether the given list is an
* <tt>instanceof</tt> this interface before applying an algorithm that would
* provide poor performance if it were applied to a sequential access list,
* and to alter their behavior if necessary to guarantee acceptable
* performance.
*
* <p>It is recognized that the distinction between random and sequential
* access is often fuzzy. For example, some <tt>List</tt> implementations
* provide asymptotically linear access times if they get huge, but constant
* access times in practice. Such a <tt>List</tt> implementation
* should generally implement this interface. As a rule of thumb, a
* <tt>List</tt> implementation should implement this interface if,
* for typical instances of the class, this loop:
* <pre>
* for (int i=0, n=list.size(); i < n; i++)
* list.get(i);
* </pre>
* runs faster than this loop:
* <pre>
* for (Iterator i=list.iterator(); i.hasNext(); )
* i.next();
* </pre>
*
* <p>This interface is a member of the
* <a href="{@docRoot}/../technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @since 1.4
*/
public interface RandomAccess {
}
看源码我们发现实际上 RandomAccess
接口中什么都没有定义。根据注释说明的, RandomAccess
接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。使用循环时应该判段该列表是否标识支持随机读写(instanceof
),或者通过适当的方式实现了随机读写加上此标识,以获取更好的性能
在 binarySearch()
方法中,它要判断传入的list
是否 RamdomAccess
的实例,如果是,调用indexedBinarySearch()
方法,如果不是,那么调用iteratorBinarySearch()
方法
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
ArrayList
实现了 RandomAccess
接口, 而 LinkedList
没有实现。为什么呢?还是和底层数据结构有关!ArrayList
底层是数组,而 LinkedList
底层是链表。数组天然支持随机访问,时间复杂度为 O(1)
,所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n)
,所以不支持快速随机访问。ArrayList
实现了 RandomAccess
接口,就表明了他具有快速随机访问功能。 RandomAccess
接口只是标识,并不是说 ArrayList
实现 RandomAccess
接口才具有快速随机访问功能的!
总结一下 List
的遍历方式选择:
实现了 RandomAccess
接口的list
,优先选择普通 fori
循环 ,其次 foreach
,
未实现 RandomAccess
接口的list
,优先选择iterator
遍历(foreach
遍历底层也是通过iterator
实现的,后面文文章在分析源码),大size
的数据,千万不要使用普通fori
循环
- 下次接着学习Map