Learning C# and OOP, Polymorphism and Interfaces, Part 3Baldwin uses a sample program to illustrate some of the things that you can do with interfaces, along with some of the things that you cannot do with interfaces. Published: November 11, 2003 C# Programming Notes # 128c PrefaceThis lesson is one of a series of lessons designed to teach you about Object-Oriented Programming (OOP) using C#. The first lesson in the group was entitled Learning C# and OOP, Getting Started, 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 C#. The previous lesson was entitled Learning C# and OOP, Polymorphism and Interfaces, Part 2. Necessary and significant aspects This miniseries will describe and discuss the necessary and significant aspects of OOP using C#. 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 C# programming language. Comparisons with Java Some of the lessons in this miniseries will also make comparisons between C# and Java with respect to both syntax and OOP concepts. This is one of those lessons. I will emphasize the similarities between these two programming languages, which are likely to dominate the programming world in the foreseeable future. By studying these lessons and learning about OOP using C#, you will also be learning quite a lot about OOP using Java. 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 programming tutorials. You will find a consolidated index at www.DickBaldwin.com. PreviewWhat is polymorphism? The meaning of the word polymorphism is something like one name, many forms. How does C# implement polymorphism? Polymorphism manifests itself in C# 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 C#:
I covered method overloading as one form of polymorphism (compile-time polymorphism) in a previous lesson. I also discussed runtime polymorphism implemented through method overriding and class inheritance in a previous lesson. I provided an introduction to the C# interface and began the discussion of interfaces and polymorphism in the two lessons prior to this one. The C# interface and polymorphism In this lesson, I will continue the explanation of the C# interface and polymorphism. As before, I will also compare the code illustrating the C# interface and polymorphism with similar Java code illustrating the Java interface and polymorphism. The purpose of the comparison is to emphasize the strong similarities that exist between C# and Java. A new relationship In previous lessons, I have explained that objects instantiated from classes that implement the same interface have a relationship that goes beyond the relationship imposed by the standard class hierarchy. I explained that because a class can implement many different interfaces, a single object in C# 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's reference. I also explained that because different classes can implement the same interface, objects instantiated from different classes can be treated as a common interface type. A sample program In the previous lesson, I presented a skeleton program that introduced you to important aspects of polymorphic behavior based on the C# interface. In this lesson, I will present another sample program that will take you deeper into the world of polymorphism as implemented using the C# interface. The sample program that I will discuss in this lesson will illustrate some of the things that you can do with interfaces, along with some of the things that you cannot do with interfaces. Discussion and Sample CodeLet's take a look at the sample program named Poly06. This program is designed to be very simple, while still illustrating runtime polymorphism using interfaces, class inheritance, and overridden methods. As mentioned above, I will compare this C# program with a Java program to emphasize the strong similarities between C# and Java. Will discuss in fragments I will discuss this program in fragments. You can view a complete listing of the C# program in Listing C15 near the end of the lesson. You can view a complete listing of the Java program in Listing J15 near the end of the lesson. Same structure as before This program has the same structure as the skeleton program named Poly05, which was discussed in the previous 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 of the program, something happens.
The interface definitions Listing C1 shows the definitions for two C# interfaces named I1
and I2.
Similarly, Listing J1 shows the definitions for two Java interfaces having
the same names and same declared methods.
Identical to program in previous lesson 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 in the previous lesson. I discussed these interfaces in detail in that lesson, and won't repeat that discussion here. The class named A Listing C2 shows the definition of the C# class named A
along with the definition of the method named x, and the overridden
method named ToString.
Listing J2 shows the definition of a corresponding class named A in
the Java program.
The methods named ToString and x were also fully defined and fully discussed in the program in the previous lesson, so they won't be discussed here. The method named B Listing C3 defines the C# class named B, which extends A,
and implements I2.
Actually implements two interfaces Although it isn't obvious from an examination of Listing C3 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:
Must define two methods As a result, the class named B must provide concrete definitions for the methods named p and q.
As you can see from Listing C3, the behavior of each of these methods is simply to display a message indicating that the method has been executed. This will be useful later to tell us exactly which method is executed when we exercise the objects from the main method of a driver class. A similar Java class named B
Listing J3 shows a similar Java class. Except for some minor syntax
differences, the Java class is identical to the C# class shown in Listing C3.
The class named C Listing C4 shows the C# class named C.
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. Different behavior It is important to note that the behaviors of the interface methods named p and q as defined in the class named C are different from the behaviors of those same interface methods as defined in the class named B. The classes named B and C both implement the interface named I2. Therefore, both classes must define the methods named p and q. However, there is no requirement that the two classes define the methods to have the same behavior. A similar Java class named C
Listing J4 shows a similar Java class. Again, except for some minor syntax
differences, the Java class is identical to the C# class
The driver class Now we are ready to get down to business and discuss the reasons that this lesson exists in the first place. Listing C5 shows the beginning of a C# 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.
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 interface type. A new object of the class B The code shown in Listing C5 instantiates a new object of the class B. Important: stored as type I1 It is important to note that the code in Listing C5 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 C5 successfully invokes the method
named p on the reference to the object of type B, producing the
screen output shown in Figure 1.
Why is this allowed? This is allowed because the method named p is declared in the interface named I1. The class named B indirectly implements the interface named I1 because it directly implements I2, which extends I1. Therefore, the reference to an object of the class named B can be treated as the interface type I1, and the method declared in the interface named I1 can be invoked on that reference. Which version of the method was executed? It is 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.
A similar Java driver class
Listing J5 shows the beginning of a similar Java driver class. Except for
some minor syntax differences, the code in Listing J5 is identical to the C#
code shown in Listing C5.
Attempt unsuccessfully to invoke q Next, the code in Listing C6 attempts, unsuccessfully, to invoke the method named q on the same reference variable of type I1.
Why did the method invocation 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.
Similar Java code
Listing J6 contains a Java statement that behaves identically to the C#
statement in Listing C6.
The solution is a type conversion Listing C7 shows the solution to the problem presented by the code in Listing
C6.
As is the case with polymorphism involving class inheritance, the solution is to change the type of the reference to a type that defines, declares, or inherits the method named q.
The solution that was used The solution that was used takes the form of using a cast operator to convert the type of the reference from type I1, to type I2. Then it becomes possible to invoke the method named q on that reference of type I2.
The output The code in Listing C7
produces the output shown in Figure 2.
Similar Java code
The Java code in Listing J7 behaves exactly like the C# code in Listing C7.
The code in Listing J7 provides the same solution to the same problem.
Using type I2 directly Listing C8 instantiates a new object of the class B and stores
the object's reference in a reference variable of the interface type I2.
The name of the reference variable is var2.
Invoke both methods successfully Then the code in Listing C8 successfully invokes the methods named p and q
on the object's reference, producing the output shown in Figure 3.
Why does this work? This works because:
Because the class named B implements I2, the reference to an object of type B can be stored in a reference variable of type I2. Because I2 inherits p and declares q, either or both of those methods can be invoked on the reference that is stored as the type I2. The behavior of the methods As always, the behavior of the methods is determined by the definitions of the methods defined in or inherited into the class from which the object was instantiated. Similar Java code
Listing J8 contains Java code that behaves identically to the C# code in Listing
C8.
Attempt, unsuccessfully, to invoke x on var2 Following this, the code in Listing C9 attempts, unsuccessfully, to invoke the method named x on the reference variable named var2 of type I2. This code produces a compiler error.
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, so the object does contain a method named x. 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:
Similar Java code
The Java code in Listing J9 behaves identically to the C# code in Listing C9.
Do the type conversion The type conversion required to solve the problem shown in Listing C9 is accomplished in Listing C10. The code in Listing C10 temporarily converts the reference to type A using a cast operator.
The String produced by the first statement in Listing C10 is passed
to the WriteLine method in the second statement in Listing C10 causing the
text shown in Figure 4 to be displayed on the computer screen.
Similar Java code
The Java code in Listing J10 behaves identically to the C# code in Listing C10,
producing the same output as that shown in Figure 4.
Get ready for a surprise At this point, if you feel like you understand 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 C11 successfully invokes the overridden ToString method on the object of the class B whose reference is stored as type I2.
How can this work? How can this work when the interface named I2 doesn't obviously declare or inherit a method named ToString. A subtle difference in behavior Recall that I told you in one of the previous lessons that although an interface cannot directly extend any class, it does implicitly extend the class named Object. Can invoke Object methods As a result, any of the methods defined in the Object class can be invoked on a reference to any object when the reference is stored as an interface type (such as I2). With respect to the 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.
The output Therefore, the two statements shown in Listing C11 produce the screen output shown in Figure 5.
Polymorphism applies 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 C11 was the overridden version defined in class A, and was not the default version defined in the Object class. The overridden version in class A was inherited into class B. Similar Java code
The Java code in Listing J11 behaves identically to the C# code in Listing C11,
producing the appropriate output for the overridden toString method.
The reverse of the above 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 C12 instantiates a new object of type B and stores it in a reference of type Object. Attempt unsuccessfully to invoke p Then the code in Listing C12 attempts, unsuccessfully, to invoke the method named p
on the reference.
Same song, an even different verse The code in Listing C12 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:
A similar Java statement
The Java code in Listing J12 is essentially the same as the C# code in Listing
C12.
Convert reference to type I1 The code in Listing C13 uses a cast operator to convert the reference
in the variable named var4 from type Object to type I1.
Then the code successfully invokes the method named
p
on the converted reference.
The output The code in Listing C13 compiles and executes successfully, producing the
screen output shown in Figure 6.
Similar Java code
At the risk of becoming extremely boring, the Java code in Listing J13 is
essentially the same as the C# code in Listing C13.
The point in boring you with this Java code is to emphasize that if you understand interfaces in Java, you also understand interfaces in C#. Similarly, if you understand interfaces in C#, you also understand interfaces in Java. Although there are some syntax differences between the use of interfaces in C# and Java, there are essentially no conceptual differences between the two. A walk in the park If you understand all of the above, then understanding the code in Listing C14 should be like a walk in the park on a sunny day.
By now, you should know exactly why the C# code in Listing C14 (and the
Java code in Listing J14) produces the screen output shown in Figure 7.
A new object of the class named C The code in Listing C14 instantiates a new object of the class named C and stores that object's reference in the reference variable named var2 of type I2.
Then the code in Listing C14 successfully invokes the methods named p and q on the object's reference. Why is this possible? This is possible because the class named C implements I2, which extends I1. Which methods were executed? The output shown in Figure 7 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 behaviors of the methods named p and q, as defined in the class named C, are different from the behaviors of the methods having the same signatures defined in the class named B. Therein lies much of the power of the C# interface. The power of the C# 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 a 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 for an object of the class from which that particular object was instantiated. This is runtime polymorphism based on interfaces and overridden methods. Java code again
Need I say it again? The behavior of the Java code shown in Listing J14 is
the same as the behavior of the C# code shown in Listing C14.
SummaryIf you don't understand interfaces ... If you don't understand interfaces, you don't understand C#, and it is highly unlikely that you will be successful as a C# programmer. Interfaces are indispensable in C# Beyond writing "Hello World," there is little if anything that can be accomplished using C# without understanding and using interfaces. What can you do with interfaces? The sample program that I discussed in this lesson has illustrated 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. What's Next?C# supports the use of static member variables and static methods in class definitions. While static members can be useful in some situations, the existence of static members tends to complicate the overall object-oriented structure of C#. 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. Complete Program ListingA complete listing of the C# program is shown in Listing C15 below. A complete listing of the Java program is shown in Listing J15 below.
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. About the authorRichard Baldwin is a college professor (at Austin Community College in Austin, Texas) and private consultant whose primary focus is a combination of Java, C#, and XML. In addition to the many platform and/or language independent benefits of Java and C# applications, he believes that a combination of Java, C#, and XML will become the primary driving force in the delivery of structured information on the Web. Richard has participated in numerous consulting projects and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Programming Tutorials, which has gained a worldwide following among experienced and aspiring programmers. He has also published articles in JavaPro 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- |
© 1996, 1997, 1998, 1999, 2000, 2001, 2002 Richard G. Baldwin |