说到栈,或许不陌生,先进后出,后进先出等等的特点,在日常的编程中也经常用到,但是Java中如何去使用栈,通过这篇文章来详细了解一下


文章目录

  • Java中如何使用栈
  • 自己实现一个栈
  • 数组栈
  • 链式栈
  • 总结


Java中如何使用栈

      栈是一种数据结构,可以分为链式栈数组栈,顾名思义,链式栈的底层是用链表实现的,数组栈的底层是用数组实现的,下面就来谈一下JDK中的栈。

      很多人都说Java中官方实现了专门的栈类Stack,该类位于java.util包下,一起来看一下继承结构:

java 异常堆栈格式化 java堆栈数据结构_数组


      该类继承了Vector类,Vector类是Java中已经淘汰的一个类,该类底层是数组实现,并且是线程安全的,Stack类继承了该类后就具有了这两个特性,这两个特性也是Stack最大的缺点:

  1. 底层使用数组,具体的来说就是动态数组,扩容方式需要重新申请大的内存空间 ,而且要把原数组中的值拷贝,效率是很低的
  2. 该类线程安全,从另一方面来说,确保了线程安全就需要一定的开销,那么在很多场景下是不需要线程安全的,所以这个功能显得有些赘余,降低效率。

既然JDK提供的Stack类有这两个缺点,那么在实际的使用中应该如何去使用栈?

      在Java中提供了一个类LinkedList,该类相信有些人不会陌生,链表啊,但是该类其实不光是链表,其实还是链式栈链式队列,该类提供了一些方法来满足栈和队列的功能,而且该栈的底层是链表实现的,所以扩容就不用担心了,不存在数组动态扩容的低效问题,并且非线程安全,所以完美的解决了上面的两个缺点。一起来看一下常用方法:

push : 入栈
pop : 出栈
peek : 返回栈顶元素
empty : 是否为空
size : 返回栈中元素的个数

用法非常简单,要想更加深入的了解栈,一起来实现一下:

自己实现一个栈
数组栈
class MyStack<T> {
    private T[] elemData;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;
    public MyStack() {
        elemData = (T[])new Object[10];
        size = 0;
    }
    //判断栈满
    public boolean isFull() {
        return size >= elemData.length;
    }
    //判断栈空
    public boolean isEmpty() {
        return size <= 0;
    }
    //扩容
    private void grow() {
        elemData = Arrays.copyOf(elemData,elemData.length*2+2);
    }
    //入栈
    public boolean push(T data) {
        if(isFull()) {
            grow();
        }
        elemData[size++] = data;
        return true;
    }
    //出栈
    public T pop() {
        if(isEmpty()) {
            try {
                throw new IllegalAccessException("栈空,不可出栈");
            } catch (IllegalAccessException e) {
                e.printStackTrace();;
            }
        }
        T temp = elemData[--size];
        //防止内存泄漏
        elemData[size] = null;
        return temp;
    }
    //获得栈顶元素
    public T peek() {
        if(isEmpty()) {
            try {
                throw new IllegalAccessException("栈空,无栈顶元素");
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return elemData[size-1];
    }
}

      下面来解释一下这段代码,这是自己实现的一个栈,,使用泛型进行编写,内部是使用数组进行实现的,需要注意的有几点:

  1. 因为底层是使用数组进行编写,所以为了防止开辟的大小不够,改进为动态数组,也就是每次入栈元素前先进行判断是否需要扩容,如果需要扩容则进行2倍扩容,开辟新的数组把原数组的值拷贝过来
  2. 在出栈的时候要注意栈空
  3. 为了防止内存泄漏,在出栈的时候记得把不用的对象置为空
链式栈
class MiNiStack<T> {
    //定义top指针
    private Entry<T> top;

    public MiNiStack() {
        this.top = new Entry<>();
    }

    /**
     * 入栈,相当于链表的头插过程
     * @param data
     */
    public void push(T data) {
        Entry<T> entry = new Entry<>(data, top.next);
        top.next = entry;
    }

    /**
     * 出栈,删除第一个节点
     * @return
     */
    public T pop() {
        if(isEmpty()) {
            throw new IllegalArgumentException("栈空,出栈失败");
        }
        T var = top.next.data;
        top.next = top.next.next;
        return var;
    }

    /**
     * 返回栈中元素的个数
     * @return
     */
    public int size() {
        int size = 0;
        Entry<T> cur = top.next;
        while (cur != null) {
            size++;
            cur = cur.next;
        }
        return size;
    }

    /**
     * 判断栈是否为空
     * @return
     */
    private boolean isEmpty() {
        return top.next == null;
    }

    //定义节点Entry
    class Entry<T>{
        private T data;
        private Entry<T> next;

        public Entry() {
            this(null,null);
        }

        public Entry(T data, Entry<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}
  1. 链式栈的实现底层使用了链表,这样就不用担心扩容的问题,一定程度上提高了效率
  2. 在实现上是把链表的头部当做栈顶,入栈和出栈都是从链表的头部进行操作
  3. 入栈其实就是链表的头插操作
  4. 出栈直接删除链表的第一个数据节点
总结

      综上,我们是不推荐使用jdk提供的Stack,除非是并发情况下。可以使用LinkedList或者自己去实现一个泛型的链式栈,把功能实现的完备一点,方便自己的经常使用。