Saturday 11 March 2017

Interfaces - Java Tutorials

Using the keyword interface, you can fully abstract a class’ interface from its implementation. That is, using interface, you can specify what a class must do, but not how it does it. Interfaces are syntactically similar to classes, but they lack instance variables, and their methods are declared without any body. In practice, this means that you can define interfaces which don’t make assumptions about how they are implemented. Once it is defined, any number of classes can implement an interface. Also, one class can implement any number of interfaces.

To implement an interface, a class must create the complete set of methods defined by the interface. However, each class is free to determine the details of its own implementation. By providing the interface keyword, Java allows you to fully utilize the “one interface, multiple methods” aspect of polymorphism.

Interfaces are designed to support dynamic method resolution at run time. Normally, in order for a method to be called from one class to another, both classes need to be present at compile time so the Java compiler can check to ensure that the method signatures are compatible. This requirement by itself makes for a static and nonextensible classing environment. Inevitably in a system like this, functionality gets pushed up higher and higher in the class hierarchy so that the mechanisms will be available to more and more subclasses. Interfaces are designed to avoid this problem. They disconnect the definition of a method or set of methods from the inheritance hierarchy. Since interfaces are in a different hierarchy from classes, it is possible for classes that are unrelated in terms of the class hierarchy to implement the same interface. This is where the real power of interfaces is realized.

Interfaces add most of the functionality that is required for many applications which would normally resort to using multiple inheritance in a language such as C++.


Defining an Interface

An interface is defined much like a class. This is the general form of an interface:

  access interface name {
    return-type method-name1(parameter-list);
    return-type method-name2(parameter-list);
    type final-varname1 = value;
    type final-varname2 = value;
    // ...
    return-type method-nameN(parameter-list);
    type final-varnameN = value;
  }

Here, access is either public or not used. When no access specifier is included, then default access results, and the interface is only available to other members of the package in which it is declared. When it is declared as public, the interface can be used by any other code. name is the name of the interface, and can be any valid identifier. Notice that the methods which are declared have no bodies. They end with a semicolon after the parameter list. They are, essentially, abstract methods; there can be no default implementation of any method specified within an interface. Each class that includes an interface must implement all of the methods.

Variables can be declared inside of interface declarations. They are implicitly final and static, meaning they cannot be changed by the implementing class. They must also be initialized with a constant value. All methods and variables are implicitly public if the interface, itself, is declared as public.

Here is an example of an interface definition. It declares a simple interface which contains one method called callback( ) that takes a single integer parameter.

  interface Callback {
    void callback(int param);
  }


Implementing Interfaces

Once an interface has been defined, one or more classes can implement that interface. To implement an interface, include the implements clause in a class definition, and then create the methods defined by the interface. The general form of a class that includes the implements clause looks like this:

  access class classname [extends superclass]
                [implements interface [,interface...]] {
    // class-body
  }

Here, access is either public or not used. If a class implements more than one interface, the interfaces are separated with a comma. If a class implements two interfaces that declare the same method, then the same method will be used by clients of either interface. The methods that implement an interface must be declared public. Also, the type signature of the implementing method must match exactly the type signature specified in the interface definition.

Here is a small example class that implements the Callback interface shown earlier.

  class Client implements Callback {
    // Implement Callback's interface
    public void callback(int p) {

      System.out.println("callback called with " + p);
    }
  }

Notice that callback( ) is declared using the public access specifier.

When you implement an interface method, it must be declared as public.

It is both permissible and common for classes that implement interfaces to define additional members of their own. For example, the following version of Client implements callback( ) and adds the method nonIfaceMeth( ):

  class Client implements Callback {
    // Implement Callback's interface
    public void callback(int p) {
      System.out.println("callback called with " + p);
    }

    void nonIfaceMeth() {
      System.out.println("Classes that implement interfaces " +
                         "may also define other members, too.");
    }
  }


Accessing Implementations Through Interface References

You can declare variables as object references that use an interface rather than a class type. Any instance of any class that implements the declared interface can be referred to by such a variable. When you call a method through one of these references, the correct version will be called based on the actual instance of the interface being referred to. This is one of the key features of interfaces. The method to be executed is looked up dynamically at run time, allowing classes to be created later than the code which calls methods on them. The calling code can dispatch through an interface without having to know anything about the “callee.” This process is similar to using a superclass reference to access a subclass object.

Because dynamic lookup of a method at run time incurs a significant overhead when compared with the normal method invocation in Java, you should be careful not to use interfaces casually in performance-critical code.

The following example calls the callback( ) method via an interface reference variable:

  class TestIface {
    public static void main(String args[]) {
      Callback c = new Client();
      c.callback(42);
    }
  }

The output of this program is shown here:

  callback called with 42

Notice that variable c is declared to be of the interface type Callback, yet it was assigned an instance of Client. Although c can be used to access the callback( ) method, it cannot access any other members of the Client class. An interface reference variable only has knowledge of the methods declared by its interface declaration. Thus, c could not be used to access nonIfaceMeth( ) since it is defined by Client but not Callback.

While the preceding example shows, mechanically, how an interface reference variable can access an implementation object, it does not demonstrate the polymorphic power of such a reference. To sample this usage, first create the second implementation of Callback, shown here:

  // Another implementation of Callback.
  class AnotherClient implements Callback {
    // Implement Callback's interface
    public void callback(int p) {
      System.out.println("Another version of callback");
      System.out.println("p squared is " + (p*p));
    }
  }

Now, try the following class:

  class TestIface2 {
    public static void main(String args[]) {
      Callback c = new Client();
      AnotherClient ob = new AnotherClient();

      c.callback(42);

      c = ob; // c now refers to AnotherClient object
      c.callback(42);
    }
  }

The output from this program is shown here:

  callback called with 42
  Another version of callback
  p squared is 1764

As you can see, the version of callback( ) that is called is determined by the type of object that c refers to at run time. While this is a very simple example, you will see another, more practical one shortly.


Partial Implementations

If a class includes an interface but does not fully implement the methods defined by that interface, then that class must be declared as abstract. For example:

  abstract class Incomplete implements Callback {
    int a, b;
    void show() {
      System.out.println(a + " " + b);
    }
    // ...
  }

Here, the class Incomplete does not implement callback( ) and must be declared as abstract. Any class that inherits Incomplete must implement callback( ) or be declared abstract itself.


Applying Interfaces

To understand the power of interfaces, let’s look at a more practical example. In earlier chapters 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.


Variables in Interfaces

You can use interfaces to import shared constants into multiple classes by simply declaring an interface that contains variables which are initialized to the desired values. When you include that interface in a class (that is, when you “implement” the interface), all of those variable names will be in scope as constants. This is similar to using a header file in C/C++ to create a large number of #defined constants or const declarations. If an interface contains no methods, then any class that includes such an interface doesn’t actually implement anything. It is as if that class were importing the constant variables into the class name space as final variables. The next example uses this technique to implement an automated “decision maker”:

  import java.util.Random;

  interface SharedConstants {
    int NO = 0;
    int YES = 1;
    int MAYBE = 2;
    int LATER = 3;
    int SOON = 4;
    int NEVER = 5;
  }

  class Question implements SharedConstants {
    Random rand = new Random();
    int ask() {
      int prob = (int) (100 * rand.nextDouble());
      if (prob < 30)
        return NO;        // 30%
      else if (prob < 60)
        return YES;       // 30%
      else if (prob < 75)
        return LATER;     // 15%
      else if (prob < 98)
        return SOON;      // 13%
      
      else
        return NEVER;     // 2%
    }
  }

  class AskMe implements SharedConstants {
    static void answer(int result) {
      switch(result) {
        case NO:
          System.out.println("No");
          break;
        case YES:
          System.out.println("Yes");
          break;
        case MAYBE:
          System.out.println("Maybe");
          break;
        case LATER:
          System.out.println("Later");
          break;
        case SOON:
          System.out.println("Soon");
          break;
        case NEVER:
          System.out.println("Never");
          break;
      }
    }
    
    public static void main(String args[]) {
      Question q = new Question();
      answer(q.ask());
      answer(q.ask());
      answer(q.ask());
      answer(q.ask());
    }
  }

Notice that this program makes use of one of Java’s standard classes: Random. This class provides pseudorandom numbers. It contains several methods which allow you to obtain random numbers in the form required by your program. In this example, the method nextDouble( ) is used. It returns random numbers in the range 0.0 to 1.0.

In this sample program, the two classes, Question and AskMe, both implement the SharedConstants interface where NO, YES, MAYBE, SOON, LATER, and NEVER are defined. Inside each class, the code refers to these constants as if each class had defined or inherited them directly. Here is the output of a sample run of this program. Note that the results are different each time it is run.

  Later
  Soon
  No
  Yes


Interfaces Can Be Extended

One interface can inherit another by use of the keyword extends. The syntax is the same as for inheriting classes. When a class implements an interface that inherits another interface, it must provide implementations for all methods defined within the interface inheritance chain. Following is an example:

  // One interface can extend another.
  interface A {
    void meth1();
    void meth2();
  }

  // B now includes meth1() and meth2() -- it adds meth3().
  interface B extends A {
    void meth3();
  }

  // This class must implement all of A and B
  class MyClass implements B {
    public void meth1() {
      System.out.println("Implement meth1().");
    }

    public void meth2() {
      System.out.println("Implement meth2().");
    }

    public void meth3() {
      System.out.println("Implement meth3().");
    }

  }

  class IFExtend {
    public static void main(String arg[]) {
      MyClass ob = new MyClass();
      ob.meth1();
      ob.meth2();
      ob.meth3();
    }
  }

As an experiment you might want to try removing the implementation for meth1( ) in MyClass. This will cause a compile-time error. As stated earlier, any class that implements an interface must implement all methods defined by that interface, including any that are inherited from other interfaces.

Although the examples we’ve included in this book do not make frequent use of packages or interfaces, both of these tools are an important part of the Java programming environment. Virtually all real programs and applets that you write in Java will be contained within packages. A number will probably implement interfaces as well. It is important, therefore, that you be comfortable with their usage.

No comments:

Post a Comment