dcsimg
 

Interfaces with inheritance

Thursday Mar 1st 2001

The interface keyword takes the abstract concept one step further. You could think of it as a “pure” abstract class. It allows the creator to establish the form for a class: method names, argument lists and return types, but no method bodies. An interface can also contain data members of primitive types, but these are implicitly static and final . An interface provides only a form, but no implementation.

Bruce Eckel's Thinking in Java Contents | Prev | Next

The interface keyword takes the abstract concept one step further. You could think of it as a “pure” abstract class. It allows the creator to establish the form for a class: method names, argument lists and return types, but no method bodies. An interface can also contain data members of primitive types, but these are implicitly static and final. An interface provides only a form, but no implementation.

An interface says: “This is what all classes that implement this particular interface will look like.” Thus, any code that uses a particular interface knows what methods might be called for that interface, and that’s all. So the interface is used to establish a “protocol” between classes. (Some object-oriented programming languages have a keyword called protocol to do the same thing.)

To create an interface, use the interface keyword instead of the class keyword. Like a class, you can add the public keyword before the interface keyword (but only if that interface is defined in a file of the same name) or leave it off to give “friendly” status.

To make a class that conforms to a particular interface (or group of interfaces) use the implements keyword. You’re saying “The interface is what it looks like and here’s how it works.” Other than that, it bears a strong resemblance to inheritance. The diagram for the instrument example shows this:

Once you’ve implemented an interface, that implementation becomes an ordinary class that can be extended in the regular way.

You can choose to explicitly declare the method declarations in an interface as public. But they are public even if you don’t say it. So when you implement an interface, the methods from the interface must be defined as public. Otherwise they would default to “friendly” and you’d be restricting the accessibility of a method during inheritance, which is not allowed by the Java compiler.

You can see this in the modified version of the Instrument example. Note that every method in the interface is strictly a declaration, which is the only thing the compiler allows. In addition, none of the methods in Instrument5 are declared as public, but they’re automatically public anyway:

//: Music5.java
// Interfaces
import java.util.*;
 
interface Instrument5 {
  // Compile-time constant:
  int i = 5; // static & final
  // Cannot have method definitions:
  void play(); // Automatically public
  String what();
  void adjust();
}
 
class Wind5 implements Instrument5 {
  public void play() {
    System.out.println("Wind5.play()");
  }
  public String what() { return "Wind5"; }
  public void adjust() {}
}
 
class Percussion5 implements Instrument5 {
  public void play() {
    System.out.println("Percussion5.play()");
  }
  public String what() { return "Percussion5"; }
  public void adjust() {}
}
 
class Stringed5 implements Instrument5 {
  public void play() {
    System.out.println("Stringed5.play()");
  }
  public String what() { return "Stringed5"; }
  public void adjust() {}
}
 
class Brass5 extends Wind5 {
  public void play() {
    System.out.println("Brass5.play()");
  }
  public void adjust() { 
    System.out.println("Brass5.adjust()");
  }
}
 
class Woodwind5 extends Wind5 {
  public void play() {
    System.out.println("Woodwind5.play()");
  }
  public String what() { return "Woodwind5"; }
}
 
public class Music5 {
  // Doesn't care about type, so new types
  // added to the system still work right:
  static void tune(Instrument5 i) {
    // ...
    i.play();
  }
  static void tuneAll(Instrument5[] e) {
    for(int i = 0; i < e.length; i++)
      tune(e[i]);
  }
  public static void main(String[] args) {
    Instrument5[] orchestra = new Instrument5[5];
    int i = 0;
    // Upcasting during addition to the array:
    orchestra[i++] = new Wind5();
    orchestra[i++] = new Percussion5();
    orchestra[i++] = new Stringed5();
    orchestra[i++] = new Brass5();
    orchestra[i++] = new Woodwind5();
    tuneAll(orchestra);
  }
} ///:~ 

The rest of the code works the same. It doesn’t matter if you are upcasting to a “regular” class called Instrument5, an abstract class called Instrument5, or to an interface called Instrument5. The behavior is the same. In fact, you can see in the tune( ) method that there isn’t any evidence about whether Instrument5 is a “regular” class, an abstract class or an interface. This is the intent: Each approach gives the programmer different control over the way objects are created and used.

“Multiple inheritance” in Java

The interface isn’t simply a “more pure” form of abstract class. It has a higher purpose than that. Because an interface has no implementation at all – that is, there is no storage associated with an interface there’s nothing to prevent many interfaces from being combined. This is valuable because there are times when you need to say “An x is an a and a b and a c.” In C++, this act of combining multiple class interfaces is called multiple inheritance , and it carries some rather sticky baggage because each class can have an implementation. In Java, you can perform the same act, but only one of the classes can have an implementation, so the problems seen in C++ do not occur with Java when combining multiple interfaces:

In a derived class, you aren’t forced to have a base class that is either an abstract or “concrete” (one with no abstract methods). If you do inherit from a non- interface, you can inherit from only one. All the rest of the base elements must be interfaces. You place all the interface names after the implements keyword and separate them with commas. You can have as many interfaces as you want and each one becomes an independent type that you can upcast to. The following example shows a concrete class combined with several interfaces to produce a new class:

//: Adventure.java
// Multiple interfaces
import java.util.*;
 
interface CanFight {
  void fight();
}
 
interface CanSwim {
  void swim();
}
 
interface CanFly {
  void fly();
}
 
class ActionCharacter {
  public void fight() {}
}
 
class Hero extends ActionCharacter 
    implements CanFight, CanSwim, CanFly {
  public void swim() {}
  public void fly() {}
}
 
public class Adventure {
  static void t(CanFight x) { x.fight(); }
  static void u(CanSwim x) { x.swim(); }
  static void v(CanFly x) { x.fly(); }
  static void w(ActionCharacter x) { x.fight(); }
  public static void main(String[] args) {
    Hero i = new Hero();
    t(i); // Treat it as a CanFight
    u(i); // Treat it as a CanSwim
    v(i); // Treat it as a CanFly
    w(i); // Treat it as an ActionCharacter
  }
} ///:~ 

You can see that Hero combines the concrete class ActionCharacter with the interfaces CanFight, CanSwim, and CanFly. When you combine a concrete class with interfaces this way, the concrete class must come first, then the interfaces. (The compiler gives an error otherwise.)

Note that the signature for fight( ) is the same in the interface CanFight and the class ActionCharacter, and that fight( ) is not provided with a definition in Hero. The rule for an interface is that you can inherit from it (as you will see shortly), but then you’ve got another interface. If you want to create an object of the new type, it must be a class with all definitions provided. Even though Hero does not explicitly provide a definition for fight( ), the definition comes along with ActionCharacter so it is automatically provided and it’s possible to create objects of Hero.

In class Adventure, you can see that there are four methods that take as arguments the various interfaces and the concrete class. When a Hero object is created, it can be passed to any of these methods, which means it is being upcast to each interface in turn. Because of the way interfaces are designed in Java, this works without a hitch and without any particular effort on the part of the programmer.

Keep in mind that the core reason for interfaces is shown in the above example: to be able to upcast to more than one base type. However, a second reason for using interfaces is the same as using an abstract base class: to prevent the client programmer from making an object of this class and to establish that it is only an interface. This brings up a question: Should you use an interface or an abstract class? An interface gives you the benefits of an abstract class and the benefits of an interface, so if it’s possible to create your base class without any method definitions or member variables you should always prefer interfaces to abstract classes. In fact, if you know something is going to be a base class, your first choice should be to make it an interface, and only if you’re forced to have method definitions or member variables should you change to an abstract class.

Extending an interface

with inheritance

You can easily add new method declarations to an interface using inheritance, and you can also combine several interfaces into a new interface with inheritance. In both cases you get a new interface, as seen in this example:

//: HorrorShow.java
// Extending an interface with inheritance
 
interface Monster {
  void menace();
}
 
interface DangerousMonster extends Monster {
  void destroy();
}
 
interface Lethal {
  void kill();
}
 
class DragonZilla implements DangerousMonster {
  public void menace() {}
  public void destroy() {}
}
 
interface Vampire 
    extends DangerousMonster, Lethal {
  void drinkBlood();
}
 
class HorrorShow {
  static void u(Monster b) { b.menace(); }
  static void v(DangerousMonster d) {
    d.menace();
    d.destroy();
  }
  public static void main(String[] args) {
    DragonZilla if2 = new DragonZilla();
    u(if2);
    v(if2);
  }
} ///:~ 

DangerousMonster is a simple extension to Monster that produces a new interface. This is implemented in DragonZilla.

The syntax used in Vampire works only when inheriting interfaces. Normally, you can use extends with only a single class, but since an interface can be made from multiple other interfaces, extends can refer to multiple base interfaces when building a new interface. As you can see, the interface names are simply separated with commas.

Grouping constants

Because any fields you put into an interface are automatically static and final, the interface is a convenient tool for creating groups of constant values, much as you would with an enum in C or C++. For example:

//: Months.java
// Using interfaces to create groups of constants
package c07;
 
public interface Months {
  int
    JANUARY = 1, FEBRUARY = 2, MARCH = 3, 
    APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, 
    AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
    NOVEMBER = 11, DECEMBER = 12;
} ///:~ 

Notice the Java style of using all uppercase letters (with underscores to separate multiple words in a single identifier) for static final primitives that have constant initializers – that is, for compile-time constants.

The fields in an interface are automatically public, so it’s unnecessary to specify that.

Now you can use the constants from outside the package by importing c07.* or c07.Months just as you would with any other package, and referencing the values with expressions like Months.JANUARY. Of course, what you get is just an int so there isn’t the extra type safety that C++’s enum has, but this (commonly-used) technique is certainly an improvement over hard-coding numbers into your programs. (This is often referred to as using “magic numbers” and it produces very difficult-to-maintain code.)

If you do want extra type safety, you can build a class like this: [28]

//: Month2.java
// A more robust enumeration system
package c07;
 
public final class Month2 {
  private String name;
  private Month2(String nm) { name = nm; }
  public String toString() { return name; }
  public final static Month2
    JAN = new Month2("January"), 
    FEB = new Month2("February"),
    MAR = new Month2("March"),
    APR = new Month2("April"),
    MAY = new Month2("May"),
    JUN = new Month2("June"),
    JUL = new Month2("July"),
    AUG = new Month2("August"),
    SEP = new Month2("September"),
    OCT = new Month2("October"),
    NOV = new Month2("November"),
    DEC = new Month2("December");
  public final static Month2[] month =  {
    JAN, JAN, FEB, MAR, APR, MAY, JUN,
    JUL, AUG, SEP, OCT, NOV, DEC
  };
  public static void main(String[] args) {
    Month2 m = Month2.JAN;
    System.out.println(m);
    m = Month2.month[12];
    System.out.println(m);
    System.out.println(m == Month2.DEC);
    System.out.println(m.equals(Month2.DEC));
  }
} ///:~ 

The class is called Month2 since there’s already a Month in the standard Java library. It’s a final class with a private constructor so no one can inherit from it or make any instances of it. The only instances are the final static ones created in the class itself: JAN, FEB, MAR, etc. These objects are also used in the array month, which lets you choose months by number instead of by name. (Notice the extra JAN in the array to provide an offset by one, so that December is month 12.) In main( ) you can see the type safety: m is a Month2 object so it can be assigned only to a Month2. The previous example Months.java provided only int values, so an int variable intended to represent a month could actually be given any integer value, which wasn’t too safe.

This approach also allows you to use == or equals( ) interchangeably, as shown at the end of main( ).

Initializing fields in interfaces

Fields defined in interfaces are automatically static and final. These cannot be “blank finals,” but they can be initialized with non-constant expressions. For example:

//: RandVals.java
// Initializing interface fields with 
// non-constant initializers
import java.util.*;
 
public interface RandVals {
  int rint = (int)(Math.random() * 10);
  long rlong = (long)(Math.random() * 10);
  float rfloat = (float)(Math.random() * 10);
  double rdouble = Math.random() * 10;
} ///:~ 

Since the fields are static, they are initialized when the class is first loaded, upon first access of any of the fields. Here’s a simple test:

//: TestRandVals.java
 
public class TestRandVals {
  public static void main(String[] args) {
    System.out.println(RandVals.rint);
    System.out.println(RandVals.rlong);
    System.out.println(RandVals.rfloat);
    System.out.println(RandVals.rdouble);
  }
} ///:~ 

The fields, of course, are not part of the interface but instead are stored in the static storage area for that interface.


[28] This approach was inspired by an e-mail from Rich Hoffarth.

Contents | Prev | Next
Home
Mobile Site | Full Site
Copyright 2018 © QuinStreet Inc. All Rights Reserved