Published: April 10, 2002
By Richard G. Baldwin
Java Programming Notes # 1618
The first lesson in the group was entitled The Essence of OOP Using Java, Objects, and Encapsulation. That lesson, and each of the lessons following that one, has provided explanations of certain aspects of the essence of Object-Oriented Programming using Java. The previous lesson was entitled The Essence of OOP using Java, Polymorphism and Interfaces, Part 1.
Necessary and significant aspects
This miniseries will describe and discuss the necessary and significant aspects of OOP using Java.
If you have a general understanding of computer programming, you should be able to read and understand the lessons in this miniseries, even if you don't have a strong background in the Java programming language.
Viewing tip
You may find it useful to open another copy of this lesson in a separate browser window. That will make it easier for you to scroll back and forth among the different listings while you are reading about them.
Supplementary material
I recommend that you also study the other lessons in my extensive collection of online Java tutorials. You will find those lessons published at Gamelan.com. However, as of the date of this writing, Gamelan doesn't maintain a consolidated index of my Java tutorial lessons, and sometimes they are difficult to locate there. You will find a consolidated index at Baldwin's Java Programming Tutorials.
What is OOP?
OOP is the common abbreviation for Object-Oriented Programming.
What is polymorphism?
The meaning of the word polymorphism is something like one name, many forms.
How does Java implement polymorphism?
Polymorphism manifests itself in Java in the form of multiple methods having the same name.
In some cases, multiple methods have the same name, but different formal argument lists (overloaded methods, which were discussed in a previous lesson).
In other cases, multiple methods have the same name, same return type, and same formal argument list (overridden methods).
Three distinct forms of polymorphism
From a practical programming viewpoint, polymorphism manifests itself in three distinct forms in Java:
I covered method overloading as one form of polymorphism (compile-time polymorphism) in a previous lesson.
Method overriding and class inheritance
I discussed runtime polymorphism implemented through method overriding and class inheritance in more than one previous lesson.
Using the Java interface
In this and the previous lesson, I am explaining runtime polymorphism as implemented using method overriding and the Java interface.
A very important concept
In my opinion, this is one of the most important concepts in Java OOP, and the one that seems to give students the greatest amount of difficulty. Therefore, I am trying to take it slow and easy. As usual, I am illustrating the concept using sample programs.
A skeleton program
In the previous lesson, I presented a simple skeleton program that illustrated many of the important aspects of polymorphic behavior based on the Java interface.
Multiple inheritance and the cardinal rule
I explained how the implementation of interfaces in Java is similar to multiple inheritance.
I explained the cardinal rule of interface implementation.
A new relationship
I explained that objects instantiated from classes that implement the same interface have a new relationship that goes beyond the relationship imposed by the standard class hierarchy.
One object, many types
I explained that due to the combination of the class hierarchy and the fact that a class can implement many different interfaces, a single object in Java can be treated as many different types. However, for any given type, there are restrictions on the methods that can be invoked on the object.
Many classes, one type
I explained that because different classes can implement the same interface, objects instantiated from different classes can be treated as a common interface type.
Interfaces are critical to Java programming
I suggested that there is little if anything useful that can be done in Java without understanding and using interfaces.
In support of this suggestion, I discussed several real-world examples of the use of the Java interface, including the Delegation Event Model and Remote Method Invocation.
Another sample program
In this lesson, I will present another sample program that will take you deeper into the world of polymorphism as implemented using the Java interface.
The sample program that I will discuss in this lesson will illustrate (in a very basic form) some of the things that you can do with interfaces, along with some of the things that you cannot do with interfaces. In order to write programs that do something worthwhile, you will need to extend the concepts illustrated by this sample program into real-world requirements.
This program is designed to be very simple, while still illustrating runtime polymorphism using interfaces, class inheritance, and overridden methods.
You can view a complete listing of the program in Listing 15 near the end of the lesson.
Same structure as before
Note that this program has the same structure as Poly05 discussed in the previous lesson. (I strongly recommend that you study the previous lesson before continuing with this lesson.) However, unlike the program in the previous lesson, the methods in this version of the program are not empty. When a method is invoked in this version, something happens. (Admittedly not much happens. Text is displayed on the computer screen, but that is something.)
The interface definitions
Listing 1 shows the definition of the two interfaces named I1
and I2.
interface I1{ public void p(); }//end interface I1 //===================================// interface I2 extends I1{ public void q(); }//end interface I2 Listing 1 |
Since the methods declared in an interface are not allowed to have a body, these interface definitions are identical to those shown in the program from the previous lesson.
The class named A
Similarly, Listing 2 shows the definition of the class named A
along with the definition of the method named x, and the overridden
method named toString.
class A extends Object{ public String toString(){ return "toString in A"; }//end toString() //---------------------------------// public String x(){ return "x in A"; }//end x() //---------------------------------// }//end class A Listing 2 |
These two methods were also fully defined in the program from the previous lesson, so there is no change here either.
The method named B
Listing 3 defines the class named B, which extends A,
and implements I2.
class B extends A implements I2{ public void p(){ System.out.println("p in B"); }//end p() //---------------------------------// public void q(){ System.out.println("q in B"); }//end q(); //---------------------------------// }//end class B Listing 3 |
Actually implements two interfaces
Although it isn't obvious from an examination of Listing 3 alone, the class named B actually implements both I2 and I1. This is because the interface named I2 extends I1. Thus, the class named B implements I2 directly, and implements I1 through interface inheritance.
The cardinal rule
In case you have forgotten it, the cardinal rule for implementing interfaces is:
If a class implements an interface, it must provide a concrete definition for all the methods declared by that interface, and all the methods inherited by that interface. Otherwise, the class must be declared abstract and the definitions must be provided by a class that extends the abstract class.Must define two methods
As a result, the class named B must provide concrete definitions for the methods p and q. (The method named p is declared in interface I1 and the method named q is declared in interface I2.)
As you can see from Listing 3, the behavior of each of these methods is to display a message indicating that it has been executed. This will be useful later to tell us exactly which method is executed when we exercise the objects in the main method of the driver class.
The class named C
Listing 4 shows the upgraded version of the class named C.
class C extends Object implements I2{ public void p(){ System.out.println("p in C"); }//end p() //---------------------------------// public void q(){ System.out.println("q in C"); }//end q(); //---------------------------------// }//end class B Listing 4 |
In this upgraded version, the methods named p and q each display a message indicating that they have been executed. Again, this will be useful later to let us know exactly which version of the methods named p and q get executed when we exercise the objects.
The driver class
Listing 5 shows the beginning of the class named Poly06.
The main method in this class instantiates objects of the classes
named B and C, and exercises them to illustrate what can,
and what cannot be done with them.
public class Poly06{ public static void main( String[] args){ I1 var1 = new B(); var1.p();//OK Listing 5 |
A new data type
As explained in the previous lesson, when you define a new interface, you create a new data type.
You can store the reference to any object instantiated from any class that implements the interface in a reference variable of that type.
A new object of the class B
The code shown in Listing 5 instantiates a new object of the class B.
Important: stored as type I1
It is important to note that the code in Listing 5 stores the object's reference in a reference variable of the interface type I1 (not as the class type B).
Invoke an interface method
Following this, the code in Listing 5 successfully invokes the method named p on the reference, producing the following output on the computer screen:
p in B
Why is this allowed?
This is allowable because the method named p is declared in the interface named I1.
Which version of the method was executed?
It is also important to note, (by observing the output), that the version of the method defined in the class named B (and not the version defined in the class named C) was actually executed.
Attempt unsuccessfully to invoke q
Next, the code in Listing 6 attempts, unsuccessfully, to invoke the
method named q on the same reference variable of type I1.
var1.q();//won't compile Listing 6 |
Why did it fail?
Even though the class named B, from which the object was instantiated, defines the method named q, that method is neither declared nor inherited into the interface named I1.
Therefore, a reference of type I1 cannot be used to invoke the method named q.
The solution is a type conversion
Listing 7 shows the solution to the problem presented by Listing 6.
((I2)var1).q();//OK Listing 7 |
As in the case of polymorphism involving class inheritance, the solution is to change the type of the reference to a type that either declares or inherits the method named q.
In this case, this takes the form of using a cast operator to convert the type of the reference from type I1, to type I2, and then invoking the method named q on that reference of a new type.
This produces the following output:
q in B
Using type I2 directly
Listing 8 instantiates a new object of the class B and stores
the object's reference in a reference variable of the interface type I2.
I2 var2 = new B(); var2.p();//OK var2.q();//OK Listing 8 |
Invoke both methods successfully
Then the code successfully invokes both the methods p and q on that reference, producing the following output:
p in B
q in B
So, why does this work?
This works because:
Following this, the code in Listing 9 attempts, unsuccessfully, to invoke
the method named x on the reference variable named var2 of
type I2. This code produces a compiler error.
String var3 = var2.x(); Listing 9 |
The object of class B has a method named x
At this point, the reference variable named var2 contains a reference to an object instantiated from the class named B.
Furthermore, the class named B inherits the method named x from the class named A.
Necessary, but not sufficient
However, the fact that the object contains the method is not sufficient to make it executable in this case.
Same song, different verse
The interface named I2 neither declares nor inherits the method named x.
Therefore, the method named x cannot be invoked using the reference stored in the variable named var2 unless the reference is converted either to type A (where the method named x is defined) or type B (where the method named x is inherited).
Do the type conversion
The required type conversion is accomplished in Listing 10 where the
reference is temporarily converted to type A using a cast operator.
(It
would also work to cast it to type B.)
String var3 = ((A)var2).x();//OK System.out.println(var3); Listing 10 |
The String produced by the first statement in Listing 10 is passed to the println method causing the following text to be displayed on the computer screen:
x in A
Get ready for a surprise
If you have now caught onto the general scheme of things, the next thing that I am going to show you may result in a little surprise.
Successfully invoke the toString method on var2
The first statement in Listing 11 successfully invokes the toString
method on the object of the class B whose reference is stored as
type I2.
var3 = var2.toString();//OK System.out.println(var3); Listing 11 |
How can this work?
How can this work when the interface named I2 neither declares nor inherits a method named toString.
A subtle difference in behavior
I am unable to point you to any Sun documentation to verify the following (I also admit that I haven't spent a large amount of time searching for such documentation).
With respect to the eleven methods declared in the Object class (listed in an earlier lesson), a reference of an interface type acts like it is also of type Object.
And the end result is ...
This allows the methods declared in the Object class to be invoked on references held as interface types without a requirement to cast the references to type Object. (Later, I will show you that the reverse is not true.)
The output
Therefore, the two statements shown in Listing 11 cause the following to be displayed on the computer screen:
toString in A
Polymorphism applies
Note that the object whose reference is held in var2 was instantiated from the class named B, which extends the class named A.
Due to polymorphism, the toString method that was actually executed in Listing 11 was the overridden version defined in class A, and not the default version defined in the Object class. The overridden version in class A was inherited into class B.
The reverse is not true
While a reference of an interface type also acts like type Object, a reference of type Object does not act like an interface type.
Store a reference as type Object
The code in Listing 12 instantiates a new object of type B and stores it in a reference of type Object.
Attempt unsuccessfully to invoke p
Then it attempts, unsuccessfully, to invoke the method named p
on the reference.
Object var4 = new B(); var4.p();//won't compile Listing 12 |
Same song, an even different verse
The code in Listing 12 won't compile, because the Object class neither defines nor inherits the method named p.
In order to invoke the method named p on the reference of type Object, the type of the reference must be changed to either:
The code in Listing 13 uses a cast operator to convert the reference
from type Object to type I1, and invokes the method named
p
on the converted reference.
((I1)var4).p();//OK Listing 13 |
The output
The code in Listing 13 compiles and executed successfully, producing the following text on the computer screen:
p in B
A walk in the park
If you understand all of the above, understanding the code in Listing
14 should be like a walk in the park on a sunny day.
var2 = new C(); var2.p();//OK var2.q();//OK Listing 14 |
Class C implements I2
Recall that the class named C also implements the interface named I2.
The code in Listing 14 instantiates a new object of the class named C, and stores the object's reference in the existing reference variable named var2 of type I2.
Then it invokes the methods named p and q on that reference, causing the following text to be displayed on the computer screen:
p in C
q in C
Which methods were executed?
This confirms that the methods that were actually executed were the versions defined in the class named C (and not the versions defined in the class named B).
Same method name, different behavior
It is important to note that the behavior of the methods named p and q, as defined in the class named C, is different from the behavior of the methods having the same signatures defined in the class named B. Therein lies much of the power of the Java interface.
The power of the Java interface
Using interface types, it is possible to collect many objects instantiated from many different classes (provided all the classes implement a common interface), and store each of the references in some kind of collection as the interface type.
Appropriate behavior
Then it is possible to invoke any of the interface methods on any of the objects whose references are stored in the collection.
To use the current jargon, when a given interface method is invoked on a given reference, the behavior that results will be appropriate to the class from which that particular object was instantiated.
This is runtime polymorphism based on interfaces and overridden methods.
If you don't understand interfaces, you don't understand Java, and it is highly unlikely that you will be successful as a Java programmer.
Interfaces are indispensable in Java
Beyond writing "Hello World" programs, there is little if anything that can be accomplished using Java without understanding and using interfaces.
What can you do with interfaces?
The sample program that I discussed in this lesson has illustrated (in a very basic form) some of the things that you can do with interfaces, along with some of the things that you cannot do with interfaces. In order to write programs that do something worthwhile, you will need to extend the concepts illustrated by this sample program into real-world requirements.
While static members can be useful in some situations, the existence of static members tends to complicate the overall object-oriented structure of Java.
Furthermore, the overuse of static members can lead to problems similar to those experienced in languages like C and C++ that support global variables and global functions.
The use of static members will be discussed in the next lesson.
/*File Poly06.java Copyright 2002, R.G.Baldwin This program illustrates polymorphic behavior using interfaces in addition to class inheritance. The program output is: p in B q in B p in B q in B x in A toString in A p in B p in C q in C **************************************/ interface I1{ public void p(); }//end interface I1 //===================================// interface I2 extends I1{ public void q(); }//end interface I2 //===================================// class A extends Object{ public String toString(){ return "toString in A"; }//end toString() //---------------------------------// public String x(){ return "x in A"; }//end x() //---------------------------------// }//end class A //===================================// class B extends A implements I2{ public void p(){ System.out.println("p in B"); }//end p() //---------------------------------// public void q(){ System.out.println("q in B"); }//end q(); //---------------------------------// }//end class B //===================================// class C extends Object implements I2{ public void p(){ System.out.println("p in C"); }//end p() //---------------------------------// public void q(){ System.out.println("q in C"); }//end q(); //---------------------------------// }//end class B //===================================// public class Poly06{ public static void main( String[] args){ I1 var1 = new B(); var1.p();//OK //var1.q();//won't compile ((I2)var1).q();//OK System.out.println("");//blank line I2 var2 = new B(); var2.p();//OK var2.q();//OK //Following won't compile //String var3 = var2.x(); String var3 = ((A)var2).x();//OK System.out.println(var3); var3 = var2.toString();//OK System.out.println(var3); System.out.println("");//blank line Object var4 = new B(); //var4.p();//won't compile ((I1)var4).p();//OK System.out.println("");//blank line var2 = new C(); var2.p();//OK var2.q();//OK System.out.println("");//blank line }//end main }//end class Poly06 //===================================// Listing 15 |
Copyright 2002, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.
Richard has participated in numerous consulting projects involving Java, XML, or a combination of the two. He frequently provides onsite Java and/or XML training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Java Programming Tutorials, which has gained a worldwide following among experienced and aspiring Java programmers. He has also published articles on Java Programming in Java Pro magazine.
Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.
-end-