源码阅读(5):Java中主要的List结构——Stack集合

================
(接上文《源码阅读(4):Java中主要的List结构——ArrayList集合(下)》)

5.java.util.Stack结构解析

所谓“栈”结构,就是能使集合中的元素具有后进先出(LIFO)操作特性的集合结构,如下图所示:
在这里插入图片描述
从最初的的JDK版本开始,就使用java.util.Stack类在程序中实现“栈”结构的操作。下图是java.util.Stack类的主要继承结构,从下图可以看出java.util.Stack类就是继承于java.util.Vector类。也就是说Stack容器除了具有Vector容器的所有操作特性外,还具有作为“栈”结构能够进行的操作功能。例如Stack容器同样有线程安全操作特性(虽然性能不是最好的);Stack容器的扩容同样也是扩容成当前容量的一倍;Stack容器同样也没有对容器的序列化和反序列化做特殊优化……
超级简单的继承结构
请注意java.util.Stack类在JDK 1.6+版本后就不再推荐使用的,本文也只是出于学习JDK设计思路、演进思路的目的,才会花篇幅介绍java.util.Stack这个类。在实际工作中,如果需要在无需保证线程安全型的场景下使用“栈”数据结构,那么官方推荐使用的是java.util.ArrayDeque这个类;如果需要在保证线程安全的场景下使用“栈”数据结构,则推荐使用java.util.concurrent.LinkedBlockingDeque这个类,关于这些类本专题在后文都会进行详细说明。

由于Stack类继承Vector类的原因,Stack类的代码真心不多。除去注释外,JDK1.8中Stack类的全部代码如下所示:

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;
  }
  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) {
    int i = lastIndexOf(o);
    if (i >= 0) {
	  return size() - i;
    }
    return -1;
  }
  /** use serialVersionUID from JDK 1.0.2 for interoperability */
  private static final long serialVersionUID = 1224463164541339165L;
}

下面本文就对以上这些扩充的重要方法进行详细介绍

5.1. pop方法和peek方法

pop方法将从“栈”结构的顶部移除元素,并将这个元素返回给调用者。peek方法同样会将“栈”结构顶部元素返回给调用者,但并不会从“栈”结构顶部移除这个元素。以下代码片段示意:

/**
 * Removes the object at the top of this stack and returns that object as the value of this function.
 * @return  The object at the top of this stack (the last item of the <tt>Vector</tt> object).
 * @throws  EmptyStackException  if this stack is empty.
 */
public synchronized E pop() {
  E obj;
  int len = size();
  obj = peek();
  // 这句话很关键,从数组的尾部移除元数
  removeElementAt(len - 1);
  return obj;
}
/**
 * Looks at the object at the top of this stack without removing it from the stack.
 * @return  the object at the top of this stack (the last item of the <tt>Vector</tt> object).
 * @throws  EmptyStackException  if this stack is empty.
 */
public synchronized E peek() {
  int len = size();
  if (len == 0)
    throw new EmptyStackException();
  return elementAt(len - 1);
}

从以上代码片段可以看出,Stack容器内部结构依然是一个数组,程序使用数组的尾部模拟“栈”结构的栈顶,使用数组的头部模拟“栈”结构的栈底。下图是数组和“栈”结构的转换示意图:

在这里插入图片描述

5.2. push方法

/**
 * Pushes an item onto the top of this stack. This has exactly the same effect as: addElement(item)
 * @param item the item to be pushed onto this stack.
 * @return the <code>item</code> argument.
 * @see java.util.Vector#addElement
 */
public E push(E item) {
  addElement(item);
  return item;
}

push方法将传入的元素放置在“栈”结构的顶部,使其作为新的“栈顶”元素。注意:由于Stack类使用elementData数组的elementCount索引位模拟“栈顶”位置,所以这个方法的实际操作就是调用Vector类中的addElement(item)方法,在elementCount索引位代表的数组“尾部”添加一个新元素

5.3. 不建议在实际工作中使用Stack、Vector

Stack和Vector这两个集合存在继承关系,他们在很早的JDK版本中就存在了,其核心设计思想、实现的功能、稳定性都没有任何问题,但本文并不建议读者在实际工作中使用它们。这个主要原因是Stack、Vector的所有主要功能特性都已经被更好的选择所替换了

  • Stack、Vector虽然是线程安全的,但是它们在线程安全场景下的操作性能又不是最好的——核心原因是因为synchronized关键字总体来说是Java中一种悲观锁的实现(后文会着重讲)思路。同样的数据结构特性下、同样的线程安全性场景下可以由更好的实现类LinkedBlockingDeque、CopyOnWriteArrayList进行替换

  • Stack、Vector也并没有针对序列化/反序列化进行任何的优化。为什么要进行特定优化呢?这个原因在前文已经做了解释——对elementData数组中elementCount索引位以后的空元素进行序列化/反序列操作没有任何意义,只会徒劳消耗性能。而对应的ArrayDeque、ArrayList这两个都对序列化/反序列化操作进行了优化。

  • Stack、Vector本质结构都是一个数组,是数组就存在容量达到上限情况下的扩容操作,但是扩容规则又不是最灵活的——每次将当前容量增大一倍的扩容方式,当容量较小时这种扩容方式的适应性还好;但如果在本身容量基数就较大的情况下(例如5千万),一次性扩充过大的容量容易造成不必要的浪费,JVM也必将耗费更多资源为新的、更大的数组寻找连续的存储空间。而后者所描述的较大容量基数的场景下,ArrayList提供的按照50%(实际上的 50% + 1)的标准进行扩容的方式显得更加灵活。

====================
(接下文《源码阅读(6):Java中主要的List结构——LinkedList集合》)

posted @ 2019-06-28 21:04  点点爱梦  阅读(130)  评论(0编辑  收藏  举报