10
\$\begingroup\$

As an exercise, I wanted to make an array implementation of Stack, including most of the methods from the native Java Stack.

I'd like to know the following:

  • Does the code follow good programming principles?
  • Is the code readable/understandable?
  • Are there any redundant fields/operations?
  • Is there a way to further improve the code?
  • Should I reduce the capacity of the stack at some point? Testing the native Java Stack, it doesn't seem to do any size reduction, except with trim().

Note: I know Javadoc is missing, but I am only just beginning to read into that.

import java.util.Arrays;
import java.util.EmptyStackException;
import java.util.Random;

@SuppressWarnings("unchecked")
public class ArrayStack<T> {
    private T[] data;
    private int numElements;
    private static final int INITIAL_CAPACITY = 10;

    public ArrayStack() {
        data = (T[]) new Object[INITIAL_CAPACITY];
    }

    private void enlarge() {
        T[] temp = Arrays.copyOf(data, data.length);
        data = (T[]) new Object[data.length * 2];

        for (int i = 0; i < temp.length; i++) {
            data[i] = temp[i];
        }
    }

    public void push(T element) {
        if (numElements == data.length) {
            enlarge();
        }

        data[numElements] = element;
        numElements++;
    }

    public T peek() {
        if (isEmpty()) throw new EmptyStackException();
        return data[numElements - 1];
    }

    public T pop() {
        if (isEmpty()) throw new EmptyStackException();
        T element = data[--numElements];
        data[numElements] = null;
        return element;
    }

    public boolean isEmpty() {
        return (numElements == 0);
    }

    public int capacity() {
        return data.length;
    }

    public int size() {
        return numElements;
    }

    public void clear() {
        for (int i = 0; i < numElements; i++) {
            data[i] = null;
        }

        numElements = 0;
    }
}
\$\endgroup\$
11
\$\begingroup\$

Suppressing warnings

It's hard to defend suppressing warnings at the class level. The problematic lines in your code are these:

    data = (T[]) new Object[INITIAL_CAPACITY];

Which appears in two places. I suggest to move the array allocation to one place, and suppress the warning only for that line, like this:

public ArrayStack() {
    data = newArray(INITIAL_CAPACITY);
}

private final T[] newArray(int capacity) {
    @SuppressWarnings("unchecked")
    T[] tmp = (T[]) new Object[capacity];
    return tmp;
}

private void enlarge() {
    T[] temp = Arrays.copyOf(data, data.length);
    data = newArray(data.length * 2);

    System.arraycopy(temp, 0, data, 0, temp.length);
}

This hits two birds with one stone:

  • The ugly array creation is now at once place instead of two
  • Warnings are suppressed for the single problematic line, not for the entire class

In case you were wondering, the local tmp variable is necessary in newArray, otherwise Java doesn't allow suppressing the warning on a return statement or regular assignment. It has to be declaration + assignment.

Another important note here is the private final modifier of newArray. The private modifier is necessary to prevent sub-classes from overriding the behavior and potentially breaking the constructor. The final modifier is necessary to prevent sub-classes from shadowing the method by another method with the same name, causing confusion.

Manual array copy

Instead of this:

for (int i = 0; i < temp.length; i++) {
    data[i] = temp[i];
}

It's better to use System.arraycopy instead:

System.arraycopy(temp, 0, data, 0, temp.length);

(Also seen in the previous section.)

Other minor things

In clear, I'm wondering why you manually null out the elements, rather than simply creating a fresh new array. You could let the garbage collector take care of the stale array.

Random is unused, so remove the import.

\$\endgroup\$
  • \$\begingroup\$ I wasn't sure what is better, and I thought that creating a new array was not necessary \$\endgroup\$ – Stefan Rendevski Nov 26 '15 at 19:09
  • \$\begingroup\$ I don't know which is uglier... (T[])new Object[], or the alternative, which is using an Object array and casting to T every time you get something out of the array \$\endgroup\$ – immibis Nov 27 '15 at 5:10
  • 2
    \$\begingroup\$ Isolating the ugliness to one place, protecting the rest of the code from it is better \$\endgroup\$ – janos Nov 27 '15 at 6:05
7
\$\begingroup\$

Manual clearing array

In clear you manually null out the elements. You could re-initialize the array (which will throw work at the GC) or you could use Arrays.fill(data, null) or use the fact that larger indexes are already null and use Arrays.fill(data, 0, numElements-1, null) with adding a check for empty stack, all this with java.util.Arrays.

Single line if statements

if (isEmpty()) throw new EmptyStackException();

This has 2 code smells:

  • Uses if without braces. Semantically correct, but error prone during subsequent modifications. A lot of style guides plainly disallow this
  • Has code in the same line as if statement. Sometimes seen as less readable.

Seeing as what it does, I'd have

private ensureNonEmpty() {
    if (isEmpty()) {
        throw new EmptyStackException();
    }
}

and use it in both places. Would be also easier to change (and harder to skip one) if you later want to swap this exception for a different one.

Order of declarations

Convention is to have static members before non-static ones. So it's

private static final int INITIAL_CAPACITY = 10;
private T[] data;
private int numElements;

Or even better

private static final int INITIAL_CAPACITY = 10;

private T[] data;
private int numElements;

With some separation between static and non-static.

Shrinking the array

Should I reduce the capacity of the stack at some point? Testing the native Java Stack, it doesn't seem to do any size reduction, except with trim().

Currently your code has one important characteristics: Amortized constant time push cost. While a single push can be not constant time, the average of all push operations (no matter how many pop/clear you do between) stays constant. Add an explicit trim and you loose it, although in a controlled manner. Add an implicit trim (say when the array is one-third full) and you loose this completely. I'd rather not do it or explicitly warn about it in the JavaDoc (And I'm not complaining about lack of one, as you've admitted yourself).

\$\endgroup\$
5
\$\begingroup\$

Unnecessary Copy

Currently you make a copy of the original array before you enlarge it:

T[] temp = Arrays.copyOf(data, data.length);

But you don't need to make a copy of that array. You could keep a reference to it like this:

 T[] temp = data;

The rest of the code will work the same using the reference to the old array instead of a copy of it.

\$\endgroup\$
  • \$\begingroup\$ Wow, nice, wouldn't have remembered that \$\endgroup\$ – Stefan Rendevski Nov 26 '15 at 21:29
3
\$\begingroup\$

Simple enlarge

The enlarge function can be simplified to

private void enlarge() {
    data = Arrays.copyOf(data, data.length * 2);
}

because the Arrays.copyOf function will automatically truncate or pad given an array size different to the original.

This allows you to completely avoid some of the issues that other users have mentioned, like the unnecessary temp copy and the warning on the cast.

\$\endgroup\$
  • \$\begingroup\$ Neat, I didn't know Arrays.copyOf() can pad. \$\endgroup\$ – Stefan Rendevski Nov 27 '15 at 0:23
  • \$\begingroup\$ @StefanRendevski checking documentation even for simple functions is a good idea, as well designed APIs often have sensible/useful behaviour for common edge cases. \$\endgroup\$ – Edward Peek Nov 27 '15 at 0:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.