Summary: We consider polymorphism, one of the key ideas in object-oriented programming. Polymorphism permits us to write methods that can use objects in a variety of related classes. Prerequisites: Class basics, interfaces. Contents:
As we saw at the end of the reading on interfaces, when we write a method that uses an interface as the type of a parameter, we can call that method with any class that implements the interface. The ability to call a method with an object that belongs to any one of a group of related classes is an aspect of the idea of polymorphism.
Why include polymorphism in a programming language? Because it significantly reduces programming effort. In particular, polymorphism allows you to write one method for a variety of types, rather than to write one method per type. For example, suppose we don't trust the built-in method Math.sqrt want to write a method, squareRoot, that approximates the square root of an Integer greater than or equal to one. We can use a fairly straightforward technique: Guess a value no greater than the square root and a value no smaller than the square root and repeatedly refine those guesses until they are close to each other. In pseudocode, we might write: To compute the square root of N, where N >= 1 Let lower = 1 // 1*1 <= N when N >= 1 Let upper = N // N*N >= N when N >= 1 while (upper-lower is too large) { Let mid = (lower + upper) / 2; // If the new guess is too low, update lower if (mid*mid < N) lower = mid; // If the new guess is too high, update higher else upper = mid; } // while return (upper+lower)/2; (Note that is strategy is probably less efficient than one you may already know, but is a little more straightforward, and requires less division.) As we write this in Java, we will find it convenient to convert the Integer to a double so that the comparisons are straightforward. We will also need to put it in a class, say Helper. We would then write: public class Helper { public static double squareRoot(Integer i) { double n = i.doubleValue(); double lower = 1.0; double upper = n; double mid; while (upper-lower > 0.01) { mid = (upper+lower)/2.0; if (mid*mid < n) lower = mid; else upper = mid; } // while return (upper+lower)/2.0; } // squareRoot(Integer) // ... } // class Helper If we later need a method to compute the square root of a BigInteger and don't want to use (or don't know about) polymorphism, we can use cut-and-paste to write the following method: public double squareRoot(BigInteger bi) { double n = bi.doubleValue(); double lower = 1.0; double upper = n; double mid; while (upper-lower > 0.01) { mid = (upper+lower)/2.0; if (mid*mid < n) lower = mid; else upper = mid; } // while return (upper+lower)/2.0; } // squareRoot(BigInteger) Of course, if we made a mistake in the definition of the first squareRoot (as I did while writing this essay), or later realized that there's a better implementation, we need to rewrite not just squareRoot(Integer), but also squareRoot(BigInteger). As these two short examples suggest, repetition of code is likely to lead to problems in maintaining and updating your program. Using polymorphism, we can write one method that serves both purposes. In particular, since both Integers and BigIntegers are Numbers, and since all Numbers provide a doubleValue method, we can write public static double squareRoot(Number num) { double n = num.doubleValue(); double lower = 1.0; double upper = n; double mid; while (upper-lower > 0.01) { mid = (upper+lower)/2.0; if (mid*mid < n) lower = mid; else upper = mid; } // while return (upper+lower)/2.0; } // squareRoot(Number) Now, we can call Helper.squareRoot on an Integer, a BigInteger, a Double, a BigDecimal, a Float, or any of the other types that are also Numbers. If someone later designs another kind of Number (say, a Rational), Helper.squareRoot will work on that new kind of number without any additional effort on the part of the programmer who wrote the squareRoot method This example also suggests a second reason to have polymorphism: If we write a polymorphic method, it will work not only with all existing classes that implement an interface, but also with all future classes that implement that interface. In effect, we have programmed for the future. A third reason to have polymorphic methods is that they typically require more careful thought than do type-specific methods. That is, because the methods must be general, programmers cannot rely on features of particular types. Experience suggests that these general-purpose solutions are often somewhat better. (Of course, this better is certainly subjective. At times, the type-specific methods are more efficient and clearly easier to write.)
Let us consider an example in which polymorphism significantly simplifies our task. Suppose we want to combine chunks of text on the screen. For example, we might place two columns of text side-by-side, we might place two rows of text above each others, or we might outline a piece of text. How do we print out the combination of two columns? For each line, we print the corresponding row from the first column, some amount of whitespace between the two columns, and the corresponding row from the second column. We might phrase this in Java as pen.println(col1.row(i) + separator + col2.row(i)); What methods should these blocks of text provide? As the example suggests, we should probably be able to get one row from a block of text. Let's call that method row. We should also be able to get the number of rows. Let's call that method height. Finally, we should be able to get the width of the block (so that we can more easily place blocks side-by-side). Let's call that method width Putting it all together, we get the following interface: Here's a method (that we can put in a utility class) that knows how to print TextBlocks. /** * Print a textblock to the specified destination. */ public static void print(PrintWriter pen, TextBlock block) { for (int i = 0; i < block.rows; i++) { pen.println(block.row(i)); } // for } // print(PrintWriter, TextBlock) The simplest implementation of a TextBlock is a single line of text. We'll use these single lines as the building block of more complex blocks. How can we combine lines (and the combinations of lines)? We might combine them horizontally, vertically, or even put a box around them. Let's consider the last of those. To put a box around a TextBlock, we simply need to figure out what to return for the calls to row, height, and width. The height of a boxed block is only slightly bigger than the height of the underlying block, with room for a row above and a row beneath. Similarly, the width of a boxed block is only slightly bigger than the width of the underlying block, with room for a character on the left and a character on the right. The hardest of the three methods to write is row. If the height of the underlying block is h, for rows 1 to h, we surround the i-1th row of the underlying block with the left and right symbols of the box (e.g., vertical bars). For row 0, we return the top row of the box symbol, such as +-----+. For row h, we return a similar string. Putting it all together, we get the following. Because BoxedBlocks can be created from any TextBlocks, we can create them from lines of text or even from other text blocks. For example, consider the following TextBlock tb = new BoxedBlock(new BoxedBlock(new TextLine("Hello"))); If we print out tb, we get something like the following: +-------+ |+-----+| ||Hello|| |+-----+| +-------+ Once we add in horizontal and vertical composition (which we will do in the lab), we can build a wide variety of layouts.
Spring 2005 [Samuel A. Rebelsky] * Designed TextBlock example. Sunday, 25 September 2005 [Samuel A. Rebelsky] * Wrote introductory text. * Began writing narrative of TextBlock example. * Began rewriting code of TextBlock example. Tuesday, 26 September 2005 [Samuel A. Rebelsky] * Finished writing code for TextBlock example. * Finished writing narrative. Monday, 27 February 2006 [Samuel A. Rebelsky] * Minor edits.