Java提高——常见Java集合实现细节(3)
Map和List
map的values方法
map集合是一个关联数组,它包含两组值:一组是key组成的集合,因为map集合的key不允许重复,且map不会保存key加入的顺序,因此这些key可以组成一个Set集合;另一组是value组成的集合,因为value完全可以重复,且map可以根据key来获取对应的value,所以这些value可以组成一个List集合。
HashMap的values方法的源码:
public Collection<V> values() { //获取values实例变量 Collection<V> vs = values; //如果vs == null,将返回new values()对象 return (vs != null ? vs : (values = new Values())); }
private final class Values extends AbstractCollection<V> { public Iterator<V> iterator() { return newValueIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsValue(o); } public void clear() { HashMap.this.clear(); } }
Iterator<V> newValueIterator() { return new ValueIterator(); }
private final class ValueIterator extends HashIterator<V> { public V next() { return nextEntry().value; } }综上HashMap的values()方法表面上返回了一个Values对象,但这个对象不能添加元素。它的主要功能是用于遍历HashMap里的所有value。而遍历主要依赖于HashIterator的nextEntry()方法实现。每个Entry都持有一个引用变量指向下一个Entry.
TreeMap的 values方法的源码:
public Collection<V> values() { Collection<V> vs = values; return (vs != null) ? vs : (values = new Values()); }
class Values extends AbstractCollection<V> { public Iterator<V> iterator() { //以TreeMap中最小节点创建一个ValueIterator return new ValueIterator(getFirstEntry()); } public int size() { //调用外部类的size()实例方法的返回值作为返回值 return TreeMap.this.size(); } public boolean contains(Object o) { //调用外部类的containsValue(o)实例方法的返回值作为返回值 return TreeMap.this.containsValue(o); } public boolean remove(Object o) { //从TreeMap中最小的节点开始搜索,不断搜索下一个节点 for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e)) { if (valEquals(e.getValue(), o)) {//如果找到指定节点 deleteEntry(e);//执行删除 return true; } } return false; } public void clear() { //调用外部类的clear()实例方法来清空该集合 TreeMap.this.clear(); } }
getFirstEntry:获取TreeMap底层“红黑树”最左边的“叶子节点”,也就是“红黑树”中最小的节点,即TreeMap中第一个节点。
final Entry<K,V> getFirstEntry() { Entry<K,V> p = root; if (p != null) //不断搜索左子节点,直到p成为最左子树的叶子节点 while (p.left != null) p = p.left; return p; }
successor:获取TreeMap中指定Entry(t)的下一个节点,也就是红黑树中大于t节点的最小节点。
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { if (t == null) return null; //如果其右子树存在,搜索右子树中最小的节点(也就是右子树最左的叶子节点) else if (t.right != null) { //先获取其右子节点 Entry<K,V> p = t.right; //不断搜索左子节点,直到找到最左的叶子节点 while (p.left != null) p = p.left; return p; //如果右子树不存在 } else { Entry<K,V> p = t.parent; Entry<K,V> ch = t; //只要父节点存在,且ch是父节点的右节点 //表明ch大于其父节点,循环一直继续 //直到父节点为null,或者ch变成父节点的子节点——此时父节点大于被搜索的节点 while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } }归纳:不管是HashMap还是TreeMap,他们的values()方法都可以返回其所有value组成的Collection集合——通常理解,这个Collection集合应该是一个List集合,因为map的多个value允许重复。但这两个map对象values()方法返回的是一个不存储元素的Collection集合。当程序遍历Collection集合时,实际上就是遍历Map对象的value
Map和List的关系
从底层上看,Set和Map很相似;从用法上看,Map和List也有很大的相似。
1)Map接口提供了get(K key)方法允许Map对象根据key来获得value
2)List接口提供了get(int index)方法允许List对象根据元素索引来取得value
可以说List相当于所有key都是int类型的Map。Map和List只是用法上有些许相似之处,在底层实现上并没有太大的相似之处。、
ArrayList和LinkedList
List的实现主要有三个类:ArrayList,Vector,LinkedList
其中Vector有一个子类Stack,这个Stack子类仅在Vector父类的基础上增加了5个方法,这5个方法将Vector扩展成一个Stack,本质上,Stack依然是一个Vector。
Stack源码:
public class Stack<E> extends Vector<E> { /** * 无参构造器 */ 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; } /** * 取出最后一个元素,但不会弹出栈 */ public synchronized E peek() { int len = size(); //如果不包含任何元素,直接抛出异常 if (len == 0) throw new EmptyStackException(); return elementAt(len - 1); } //集合不包含任何元素就是空栈 public boolean empty() { return size() == 0; } public synchronized int search(Object o) { //获取o在集合中的位置 int i = lastIndexOf(o); if (i >= 0) { //用集合长度减去o在集合中的位置,就得到该元素到栈顶的距离。 return size() - i; } return -1; } private static final long serialVersionUID = 1224463164541339165L; }
从源码可以看出Stack新增的5个方法中有3个使用了synchronized修饰——那些需要操作集合元素的方法都被添加了synchronized修饰,也就是说Stack是一个线程安全的类,这也是为了让Stack和Vector保持一致——Vector也是一个线程线圈类。
如今不在推荐使用Stack类,而是使用Deque实现类。在无需保证线程安全的情况下,完全可以使用ArrayDeque代替Stack。
Deque接口代表双端队列这种数据结构,即具有队列先进先出的性质,也具有栈的性质。即是队列也是栈。
ArrayList和ArrayDeque底层都是基于Java数组实现的,只是提供的方法不同而已。
Vector和ArrayList
Vector和ArrayList都实现了List接口,底层都是基于Java数组存储集合元素。
ArrayList源码:
//采用elementData数组保存集合元素 private transient Object[] elementData;
Vector源码:
//采用elementData元素保存集合 protected Object[] elementData;
ArrayList使用了transient修饰,保证了系统序列化ArrayList对象不会直接序列化elementData数组,而是通过writeObject和readObject实现定制序列化。从序列化的角度看,ArrayList的实现比Vector安全。
除此之外,Vector其实就是ArrayList的线程安全版,ArrayList和Vector大部分实现方法都相同,只是Vector方法增加了synchronized修饰。
下面看看两者的一些源码:
ArrayList的add方法:
public void add(int index, E element) { rangeCheckForAdd(index); //保证ArrayList底层数组可以保存所有集合元素 ensureCapacityInternal(size + 1); // Increments modCount!! //将elementData数组中index位置之后的所有元素向后移动一位 //也就是将elementData数组中的index位置空出来 System.arraycopy(elementData, index, elementData, index + 1, size - index); //将新元素放进elementData数组的index位置 elementData[index] = element; size++; }
//如果添加位置大于集合长度或小于0,抛出异常 private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
Vector的add方法:
public void add(int index, E element) { insertElementAt(element, index); }
public synchronized void insertElementAt(E obj, int index) { modCount++;//增加集合的修改次数 //如果添加位置大于集合长度抛出异常 if (index > elementCount) { throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount); } //保证ArrayList底层数组可以保存所有集合 ensureCapacityHelper(elementCount + 1);
//将elementData数组中index位置之后的所有元素向后移动一位
//也就是将elementData数组中的index位置空出来
System.arraycopy(elementData, index, elementData, index + 1, elementCount - index); elementData[index] = obj;//将新元素放入elementData数组的index位置 elementCount++;}发现只是Vector的方法多了synchronized方法修饰。
ArrayList序列化实现比Vector序列化实现更加安全,因此Vector基本被ArrayList代替。Vector的唯一好处就是他是线程安全的。但是ArrayList也可以被包装成线程安全的。
ArrayList和LinkedList实现差异
List代表一种线性表的数据结构,
ArrayList则是一种顺序存储的线性表,底层采用数组保存每个集合元素,
LinkedList则是一种链式存储的线性表,本质是一个双向链表,但是它不仅实现了List接口还实现了Deque接口,也就是说LinkedList既可以当成双向链表来使用,也可以当成队列来使用,还可以当成栈来使用。(Deque代表双端队列,同时具有队列和栈的特性)。
从上可知:ArrayList底层采用数组保存集合元素,则ArrayList插入时需要完成以下两件事:
1)底层数组长度大于集合元素个数
2)插入位置之后的元素“整体搬家”向后一格
当删除ArrayList集合中指定位置元素时,程序也要进行“整体搬家”,而且还要将被删除的数组元素赋为null。
public E remove(int index) { //如果index大于或等于size,抛出异常 rangeCheck(index); modCount++; //保证index索引处的元素 E oldValue = elementData(index); //计算需要“整体搬家”的元素个数 int numMoved = size - index - 1; //当numMoved大于0时,开始搬家 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // 释放被删除的元素,以免GC回收该元素 return oldValue; }
对于ArrayList而言,添加、删除都需要”整体搬家“,因此性能十分差
public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); }
E elementData(int index) { return (E) elementData[index]; }当时获取元素的性能和数组几乎相同,非常快。
LinkedList本质上是一个双向链表:
添加节点的方式:
使用如下内部类来保存每个集合元素:
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; } }
在指定位置插入新节点:
public void add(int index, E element) { checkPositionIndex(index); //如果index == size直接在header之前插入新节点 if (index == size) linkLast(element); else//否则在index索引处的节点之前插入新节点 linkBefore(element, node(index)); }
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++; }
在指定节点(succ)前添加一个新的节点 void linkBefore(E e, Node<E> succ) { // assert succ != null; //创建一个新节点,新节点的下一个节点指向succ,上一个节点指向succ的上一个节点 final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); //让succ的上一个节点向后指向新节点 succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
获取指定索引处的节点 Node<E> node(int index) { // assert isElementIndex(index); //如果index<size/2 if (index < (size >> 1)) { Node<E> x = first;//从链表的头部开始搜索 for (int i = 0; i < index; i++) x = x.next; return x; } else {//如果index>size/2 Node<E> x = last;//从链表的尾部开始搜索 for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
LinkedList的get方法只是对上面的node方法进行封装:
public E get(int index) { checkElementIndex(index); return node(index).item; }
无论如何LinkedList为了获取指定索引处的元素都是比较麻烦的,系统开销也会比较大。
但是简单的插入操作就比较简单,只要修改了几个节点里的prev,next引用的值就可以了。
删除节点也必须先通过node方法找到索引处的节点,然后修改前一个节点的next引用和后一个节点的prev引用:
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); }
E unlink(Node<E> x) { // assert x != null; final E element = x.item;//先保存x节点的元素 final Node<E> next = x.next; final Node<E> prev = x.prev; //将被删除的元素的两个引用、元素都赋值为null,以便垃圾回收 if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
ArrayList和LinkedList性能分析
ArrayList性能总体上优于LinkedList。
当程序需要通过get(int index)方法获取List集合指定索引处的元素时,ArrayList性能大大优于LinkedList,因为ArrayList底层是数组来保存集合元素,所以调用get方法获取指定索引处的元素时,底层实际上调用elementData[index]来返回该元素,因此性能非常好。
当程序调用add(int index,Object obj)向List集合中添加元素时,ArrayList必须对底层数组进行“整体搬家”。如果数组不够长,还要重新创建一个长度为原来1.5倍的数组,再由垃圾回收机制回收原来的数组,因此系统开销比较大;对于LinkedList而言,它的主要系统开销集中在node(int index )方法上,必须一个一个的搜索过去,直到找到index元素,再在此元素之前插入新元素。即便如此,执行该方法时LinkedList性能依然优于ArrayList
Iterator迭代器
Iterator迭代器是一个接口,专门用于迭代各种Collection集合,包括Set和List
List和Set在实现Iterator的差异——>导致删除集合元素的不同表现