Implementing Interfaces in Java
A practical guide to implementing ADTs specified by Java interfaces
A common task in Data Structures is the following: given an ADT and associated Java interface, write your own implementation of the interface. In this note, we will give a walk-through of how you might approach this task systematically. Through your experience programming, you will develop your own programming methods and workflows tailored to your own taste. The goal of presenting the systematic approach shown here is to give some advice to help avoid common pitfalls in programming. Of course, your mileage may vary!
Outline of the Process
- Pick data representation and procedures; design program on paper
- Create a minimal compiling program
- Add instance variables/fields/inner classes to represent the data structure you are implementing
- Write a simple testing program, and check that it compiles
- Write a
toString
method for your class, or other methods to display the contents of your data structure - Implement required methods one at a time, compiling and testing each method as you go using the testing program you wrote
The Process as Applied to a Stack
Here, we will detail the process described above using the example of implementing a stack—specifically, the SimpleStack
:
- Download:
SimpleStack.java
A stack is an ADT that stores a collection of elements, where only the most recently added element can be accessed. See List-like ADTs for details. Below, we will demonstrate the process of writing a program, ArraySimpleStack.java
, that provides an implementation of the SimpleStack
interface and stores the stack contents in an array.
Step 1: Pick Representation and Design
Before doing any programming, it is a good idea to come up with a plan for your program on paper. The plan should include how you intend to represent the data in your program (i.e., what data structure you intend to use). It may be helpful to include diagrams and examples demonstrating how the operations required by the interface will be performed in your representation.
For the ArraySimpleStack
this is the sketch I wrote for myself indicating how I intend to implement the basic operations of push
, pop
, and peek
:
Important note. Sketching out your program design is a crucial step of the design process. You cannot program a computer to perform any task that you cannot (in principle) perform by hand. Thus, if it is not clear to you how to perform some operation by hand at this stage of the design process, it is important to figure out how before starting to write code.
Step 2: Minimal Compiling Program
Once you have a sketch of how you intend to implement your program, you should start writing code! Before implementing anything, I recommend writing a minimal program that will compile. First, you’ll need to give your class a name, and indicate that implements the desired interface(s). For our stack, I’d start with:
1
2
3
public class ArraySimpleStack<E> implements SimpleStack<E> {
}
Note that if I try to compile at this point, I’ll get the following error:
1
2
3
4
5
6
ArraySimpleStack.java:1: error: ArraySimpleStack is not abstract and does not override abstract method peek() in SimpleStack
public class ArraySimpleStack<E> implements SimpleStack<E> {
^
where E is a type-variable:
E extends Object declared in class ArraySimpleStack
1 error
This is telling me that I have not yet implemented methods (specifically peek()
) that are required by the interface. To get something that will compile, you need to add all of the methods required by the interface. One way you might do that is to open the interface (in this case SimpleStack.java
) and copy all of the methods and comments to the body of the new class you will be implementing (you can remove the comments later, but it might be helpful to see them as you start programming). The interface just gives declarations of the required methods, but you can provide minimal definitions for these methods as well. In particular, for each method, you will need to change its access modifier to public
and add a return
statement if the method should return a value. Doing this for my ArraySimpleStack
, I get the following program:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ArraySimpleStack<E> implements SimpleStack<E> {
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void push(E x) {
}
@Override
public E pop() {
return null;
}
@Override
public E peek() {
return null;
}
}
I also added the annotation @Override
before each method to indicate to the compiler that these are methods I plan to implements that were declared in the interface. At this point the program already compiles. This is progress! The program does not yet do anything useful, but we will already get some feedback from the compiler that we don’t have typos or anything.
Note. If you compile above and get errors, there is still something amiss with the skeleton of the program you wrote. Common errors include typos in method definitions, forgetting to set the access modifier of each method to public
, or forgetting to return an appropriate (default) value.
Step 3: Adding Data Representation
As yet, our program doesn’t do anything useful, or even store any data. A good next step is to add fields for the data instances of your class will store. As indicated in Step 1 above, I plan to represent my stack using an array, and I will keep track of the index corresponding to the top value of the stack, as well as the stack’s size. I’ll make fields for each of these, and write a constructor to describe how they should be initialized. Here’s the result:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ArraySimpleStack<E> implements SimpleStack<E> {
public static final int DEFAULT_SIZE = 8;
private Object[] contents;
private int size;
private int top;
public ArraySimpleStack() {
contents = new Object[DEFAULT_SIZE];
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void push(E x) {
}
@Override
public E pop() {
return null;
}
@Override
public E peek() {
return null;
}
}
Once again, I checked that the program compiles at this point.
Step 4: Write a Tester
Even though my ArraySimpleStack
doesn’t yet do anything useful, I can still write a simple program to test it. For example, I wrote a simple program that performs the three basic stack operations for a stack of Integers
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StackTester {
public static void main(String[] args) {
System.out.println("Creating a new stack of Integers");
SimpleStack<Integer> stk = new ArraySimpleStack<Integer>();
System.out.println("Pushing values 1, 2, 3");
stk.push(1);
stk.push(2);
stk.push(3);
System.out.println("contents = " + stk);
System.out.println("Peeking at top value");
int top = stk.peek();
System.out.println("top value = " + top);
System.out.println("Popping top value");
top = stk.pop();
System.out.println("popped value " + top);
System.out.println("contents = " + stk);
}
}
This program already compiles, but when I run it, I get the following output and error:
1
2
3
4
5
6
7
java StackTester
Creating a new stack of Integers
Pushing values 1, 2, 3
contents = ArraySimpleStack@2db0f6b2
Peeking at top value
Exception in thread "main" java.lang.NullPointerException
at StackTester.main(StackTester.java:14)
The exception is because right now, the peek()
method just returns null
. So for now, I’ll comment out the tests for the peek
and pop
methods until I’ve given proper implementation for these methods.
In the program above, I used the line
1
System.out.println("contents = " + stk);
to print the contents of the stack. The output of this line when I run the program is contents = ArraySimpleStack@2db0f6b2
, which is quite unhelpful. In order to get more meaningful output, we can override the toString()
method for our ArraySimpleStack
so that it returns (a String
representation of) the actual stack contents. Note that the println
method automatically calls the toString()
method on objects, so the line above is equivalent to
1
System.out.println("contents = " + stk.toString());
Step 5: Displaying Contents
At this point, it is a good idea to write methods to help us understand the state of our implementation. As suggested above, a good place to start is writing a toString
method for your class. I added the following toString
method to ArraySimpleStack
, that displays the contents of the stack stored in the array contents
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("[");
for (int i = 0; i < this.size; ++i) {
str.append(contents[i].toString());
if (i < this.size - 1) {
str.append(", ");
}
}
str.append("]");
return str.toString();
}
Now when I run StackTester
(with the peek
/pop
tests commented out), I get the following output:
1
2
3
4
java StackTester
Creating a new stack of Integers
Pushing values 1, 2, 3
contents = []
Since I haven’t yet implemented the push
method, contents
after the calls to push
is still unmodified. Thus, I can see that my push
method is not doing what it should. Next, I will actually implement the method!
Step 6: Implement and Test Methods
At this point we have an ArraySimpleStack
that compiles and produces a String
representation of its contents, and a program StackTester
that creates, modifies, and displays the state of a SimpleStack
. Everything compiles and we have all feedback we would need to check that our implementation is operating as expected. All that is needed to implement and test the SimpleStack
methods that we have so far neglected. For example, here is my first implementation of the push
method:
1
2
3
4
5
6
7
8
9
10
@Override
public void push(E x) {
// add x to the index after top; this index becomes the new top
contents[top+1] = x;
top++;
// pushing increases the size of the stack
size++;
}
Now, I can compile and run ArraySimpleStack
and StackTester
. When I do, StackTester
gives the following error:
1
2
3
4
5
6
Creating a new stack of Integers
Pushing values 1, 2, 3
Exception in thread "main" java.lang.NullPointerException
at ArraySimpleStack.toString(ArraySimpleStack.java:49)
at java.base/java.lang.String.valueOf(String.java:2951)
at StackTester.main(StackTester.java:11)
The problem arises in my toString
method, but it isn’t obvious looking at my code what exactly the problem is! To diagnose it further, I modify my tester program to print the stack contents before and after every call to push
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class StackTester {
public static void main(String[] args) {
System.out.println("Creating a new stack of Integers");
SimpleStack<Integer> stk = new ArraySimpleStack<Integer>();
System.out.println("contents = " + stk);
System.out.println("Pushing value 1");
stk.push(1);
System.out.println("contents = " + stk);
System.out.println("Pushing value 2");
stk.push(2);
System.out.println("contents = " + stk);
System.out.println("Pushing value 2");
stk.push(3);
System.out.println("contents = " + stk);
// System.out.println("Peeking at top value");
// int top = stk.peek();
// System.out.println("top value = " + top);
// System.out.println("Popping top value");
// top = stk.pop();
// System.out.println("popped value " + top);
// System.out.println("contents = " + stk);
}
}
Running the program, I again encounter the same error, immediately after calling the add
method for the first time:
1
2
3
4
5
6
7
8
java StackTester
Creating a new stack of Integers
contents = []
Pushing value 1
Exception in thread "main" java.lang.NullPointerException
at ArraySimpleStack.toString(ArraySimpleStack.java:49)
at java.base/java.lang.String.valueOf(String.java:2951)
at StackTester.main(StackTester.java:10)
Since the problem only occurred after calling push
, I look at the push
method for the source of the error. Again, I don’t immediately see any problems with my logic, so I add some print statements to my push
method to help diagnose what is going on:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void push(E x) {
System.out.println("called push(" + x + "), top = " + top);
// add x to the index after top; this index becomes the new top
contents[top+1] = x;
top++;
System.out.println("added " + x + ", top = " + top);
// pushing increases the size of the stack
size++;
}
Running StackTester
now produces:
1
2
3
4
5
6
7
8
9
10
java StackTester
Creating a new stack of Integers
contents = []
Pushing value 1
called push(1), top = 0
added 1, top = 1
Exception in thread "main" java.lang.NullPointerException
at ArraySimpleStack.toString(ArraySimpleStack.java:56)
at java.base/java.lang.String.valueOf(String.java:2951)
at StackTester.main(StackTester.java:10)
Now I can compare the code and output to what I expected to happen from my description and notes from Step 1. Looking carefully, I see an issue! After I pushed the value 1
, we have top = 1
. But from my description, the first element should be stored at index 0
, not index 1
! Since top
was initialized to 0
(this is the default initial value of int
), and push
sets contents[top+1]
, the first element is added at index 1
instead of 0
. Since contents[0]
was never initialized, toString()
gives the error when calling contents[0].toString()
!
At this point, I can think about how to fix the issue. One way is to initialize the value of top
to -1
, with the interpretation that top == -1
when the stack is empty. This is consistent with my choice of top
as being the index of the element at the top of the stack, if any. Note that with this change, the field size
becomes redundant, as we should always have size == top + 1
. So maybe I will go back an remove the size
field entirely (though I don’t need to do this). Updating the ArraySimpleStack
constructor to initialize top = -1
,
1
2
3
4
public ArraySimpleStack() {
top = -1;
contents = new Object[DEFAULT_SIZE];
}
I can run the StackTester
again. Now I get:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java StackTester
Creating a new stack of Integers
contents = []
Pushing value 1
called push(1), top = -1
added 1, top = 0
contents = [1]
Pushing value 2
called push(2), top = 0
added 2, top = 1
contents = [1, 2]
Pushing value 2
called push(3), top = 1
added 3, top = 2
contents = [1, 2, 3]
Now everything seems to be working as expected, so I can comment out the extra print statements in the push
method. I won’t delete them yet, in case I need to print output from the push
method again for debugging purposes. Now I can go on to implementing and testing the other methods.
Eventually, I will want my testing program to test larger instances (i.e., more pushes, etc). At this point, I will find out that I forgot a crucial detail: that I need to increase the capcity of the contents
array in order for the program to function properly. Thus, I will need to design another feature that wasn’t part of my original plan.
Final Thoughts
When planning a program, we will often overlook details in the design that only become apparent through coding and testing. It is good to catch these potential problems early on in the coding process. By continually compiling, running, and testing your program before it is “finished,” you can often identify problems more quickly. Thus, you may avoid the need to change many parts of your program at once.
The example above demonstrates that my initial plan overlooked two imporant details: the initialization of the top
index, and resizing the array. The first issue caused problems as soon as tried to implement the push
method. I only detected that there was a problem because I tested the program when I had only implemented the push
method. The actual error (NullPointerException
) was thrown in the toString
method, even though there was no problem with that method! Finding logical errors in code can be incredibly challenging and time consuming. Testing the code as you write it helps narrow down where to search for the problem, and will often save a lot of time in the long-run.
Finally, the process of programming takes time. Even if we work systematically, there will inevitably be unexpected setbacks. Thus it is crucial to allow onseself sufficient time to work through a problem by starting the process well in advance of any deadlines.