Tuesday, November 11, 2008

Applying Interfaces

Applying Interfaces:

 

To understand the power of interfaces, let's look at a more practical example. In earlier Posts you developed a class called Stack that implemented a simple fixed-size stack. However, there are many ways to implement a stack. For example, the stack can be of a fixed size or it can be "growable." The stack can also be held in an array, a linked list, a binary tree, and so on. No matter how the stack is implemented, the interface to the stack remains the same. That is, the methods push( ) and pop( ) define the interface to the stack independently of the details of the implementation. Because the interface to a stack is separate from its implementation, it is easy to define a stack interface, leaving it to each implementation to define the specifics.

 

Let's look at two examples:

 

First, here is the interface that defines an integer stack. Put this in a file called IntStack.java. This interface will be used by both stack implementations.

 

// Define an integer stack interface.

interface IntStack {

void push(int item); // store an item

int pop(); // retrieve an item

}

 

The following program creates a class called FixedStack that implements a fixed-length version of an integer stack:

 

// An implementation of IntStack that uses fixed storage.

class FixedStack implements IntStack {

private int stck[];

private int tos;

// allocate and initialize stack

FixedStack(int size) {

stck = new int[size];

tos = -1;

}

// Push an item onto the stack

public void push(int item) {

if(tos==stck.length-1) // use length member

System.out.println("Stack is full.");

else

stck[++tos] = item;

}

// Pop an item from the stack

public int pop() {

if(tos < 0) {

System.out.println("Stack underflow.");

return 0;

}

else

return stck[tos--];

}

}

class IFTest {

public static void main(String args[]) {

FixedStack mystack1 = new FixedStack(5);

FixedStack mystack2 = new FixedStack(8);

// push some numbers onto the stack

for(int i=0; i<5; i++) mystack1.push(i);

for(int i=0; i<8; i++) mystack2.push(i);

// pop those numbers off the stack

System.out.println("Stack in mystack1:");

for(int i=0; i<5; i++)

System.out.println(mystack1.pop());

System.out.println("Stack in mystack2:");

for(int i=0; i<8; i++)

System.out.println(mystack2.pop());

}

}

 

Following is another implementation of IntStack that creates a dynamic stack by use of the same interface definition. In this implementation, each stack is constructed with an initial length. If this initial length is exceeded, then the stack is increased in size. Each time more room is needed, the size of the stack is doubled.

 

// Implement a "growable" stack.

class DynStack implements IntStack {

private int stck[];

private int tos;

// allocate and initialize stack

DynStack(int size) {

stck = new int[size];

tos = -1;

}

// Push an item onto the stack

public void push(int item) {

// if stack is full, allocate a larger stack

if(tos==stck.length-1) {

int temp[] = new int[stck.length * 2]; // double size

for(int i=0; i<stck.length; i++) temp[i] = stck[i];

stck = temp;

stck[++tos] = item;

}

else

stck[++tos] = item;

}

// Pop an item from the stack

public int pop() {

if(tos < 0) {

System.out.println("Stack underflow.");

return 0;

}

else

return stck[tos--];

}

}

class IFTest2 {

public static void main(String args[]) {

DynStack mystack1 = new DynStack(5);

DynStack mystack2 = new DynStack(8);

// these loops cause each stack to grow

for(int i=0; i<12; i++) mystack1.push(i);

for(int i=0; i<20; i++) mystack2.push(i);

System.out.println("Stack in mystack1:");

for(int i=0; i<12; i++)

System.out.println(mystack1.pop());

System.out.println("Stack in mystack2:");

for(int i=0; i<20; i++)

System.out.println(mystack2.pop());

}

}

 

The following class uses both the FixedStack and DynStack implementations. It does so through an interface reference. This means that calls to push( ) and pop( ) are resolved at run time rather than at compile time.

 

/* Create an interface variable and

access stacks through it.

*/

class IFTest3 {

public static void main(String args[]) {

IntStack mystack; // create an interface reference variable

DynStack ds = new DynStack(5);

FixedStack fs = new FixedStack(8);

mystack = ds; // load dynamic stack

// push some numbers onto the stack

for(int i=0; i<12; i++) mystack.push(i);

mystack = fs; // load fixed stack

for(int i=0; i<8; i++) mystack.push(i);

mystack = ds;

System.out.println("Values in dynamic stack:");

for(int i=0; i<12; i++)

System.out.println(mystack.pop());

mystack = fs;

System.out.println("Values in fixed stack:");

for(int i=0; i<8; i++)

System.out.println(mystack.pop());

}

}

 

In this program, mystack is a reference to the IntStack interface. Thus, when it refers to ds, it uses the versions of push( ) and pop( ) defined by the DynStack implementation.

 

When it refers to fs, it uses the versions of push( ) and pop( ) defined by FixedStack. As explained, these determinations are made at run time. Accessing multiple implementations of an interface through an interface reference variable is the most powerful way that Java achieves run-time polymorphism.

No comments: