1. Trang chủ
  2. » Công Nghệ Thông Tin

Thinking in Java 4th Edition phần 3 pptx

108 674 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 108
Dung lượng 1,4 MB

Nội dung

package polymorphism.music; import static net.mindview.util.Print.*; class Stringed extends Instrument { public void play(Note n) { print("Stringed.play() " + n); } } class Brass extends Instrument { public void play(Note n) { print("Brass.play() " + n); } } public class Music2 { public static void tune(Wind i) { i.play(Note.MIDDLE_C); } public static void tune(Stringed i) { i.play(Note.MIDDLE_C); } public static void tune(Brass i) { i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); Stringed violin = new Stringed(); Brass frenchHorn = new Brass(); tune(flute); // No upcasting tune(violin); tune(frenchHorn); } } /* Output: Wind.play() MIDDLE_C Stringed.play() MIDDLE_C Brass.play() MIDDLE_C *///:~ This works, but there’s a major drawback: you must write type-specific methods for each new Instrument class you add. This means more programming in the first place, but it also means that if you want to add a new method like tune( ) or a new type of Instrument, you’ve got a lot of work to do. Add the fact that the compiler won’t give you any error messages if you forget to overload one of your methods and the whole process of working with types becomes unmanageable. Wouldn’t it be much nicer if you could just write a single method that takes the base class as its argument, and not any of the specific derived classes? That is, wouldn’t it be nice if you could forget that there are derived classes, and write your code to talk only to the base class? That’s exactly what polymorphism allows you to do. However, most programmers who come from a procedural programming background have a bit of trouble with the way polymorphism works. Exercise 1: (2) Create a Cycle class, with subclasses Unicycle, Bicycle and Tricycle. Demonstrate that an instance of each type can be upcast to Cycle via a ride( ) method. Polymorphism 195 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com The twist The difficulty with Music.java can be seen by running the program. The output is Wind.play( ). This is clearly the desired output, but it doesn’t seem to make sense that it would work that way. Look at the tune( ) method: public static void tune(Instrument i) { // i.play(Note.MIDDLE_C); } It receives an Instrument reference. So how can the compiler possibly know that this Instrument reference points to a Wind in this case and not a Brass or Stringed? The compiler can’t. To get a deeper understanding of the issue, it’s helpful to examine the subject of binding. Method-call binding Connecting a method call to a method body is called binding. When binding is performed before the program is run (by the compiler and linker, if there is one), it’s called early binding. You might not have heard the term before because it has never been an option with procedural languages. C compilers have only one kind of method call, and that’s early binding. The confusing part of the preceding program revolves around early binding, because the compiler cannot know the correct method to call when it has only an Instrument reference. The solution is called late binding, which means that the binding occurs at run time, based on the type of object. Late binding is also called dynamic binding or runtime binding. When a language implements late binding, there must be some mechanism to determine the type of the object at run time and to call the appropriate method. That is, the compiler still doesn’t know the object type, but the method-call mechanism finds out and calls the correct method body. The late-binding mechanism varies from language to language, but you can imagine that some sort of type information must be installed in the objects. All method binding in Java uses late binding unless the method is static or final (private methods are implicitly final). This means that ordinarily you don’t need to make any decisions about whether late binding will occur—it happens automatically. Why would you declare a method final? As noted in the last chapter, it prevents anyone from overriding that method. Perhaps more important, it effectively “turns off” dynamic binding, or rather it tells the compiler that dynamic binding isn’t necessary. This allows the compiler to generate slightly more efficient code for final method calls. However, in most cases it won’t make any overall performance difference in your program, so it’s best to only use final as a design decision, and not as an attempt to improve performance. Producing the right behavior Once you know that all method binding in Java happens polymorphically via late binding, you can write your code to talk to the base class and know that all the derived-class cases will work correctly using the same code. Or to put it another way, you “send a message to an object and let the object figure out the right thing to do.” 196 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com The classic example in OOP is the “shape” example. This is commonly used because it is easy to visualize, but unfortunately it can confuse novice programmers into thinking that OOP is just for graphics programming, which is of course not the case. The shape example has a base class called Shape and various derived types: Circle, Square, Triangle, etc. The reason the example works so well is that it’s easy to say “a circle is a type of shape” and be understood. The inheritance diagram shows the relationships: The upcast could occur in a statement as simple as: Shape s = new Circle(); Here, a Circle object is created, and the resulting reference is immediately assigned to a Shape, which would seem to be an error (assigning one type to another); and yet it’s fine because a Circle is a Shape by inheritance. So the compiler agrees with the statement and doesn’t issue an error message. Suppose you call one of the base-class methods (that have been overridden in the derived classes): s.draw(); Again, you might expect that Shape’s draw( ) is called because this is, after all, a Shape reference—so how could the compiler know to do anything else? And yet the proper Circle.draw( ) is called because of late binding (polymorphism). The following example puts it a slightly different way. First, let’s create a reusable library of Shape types: //: polymorphism/shape/Shape.java package polymorphism.shape; public class Shape { public void draw() {} public void erase() {} } ///:~ //: polymorphism/shape/Circle.java package polymorphism.shape; import static net.mindview.util.Print.*; public class Circle extends Shape { Polymorphism 197 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com public void draw() { print("Circle.draw()"); } public void erase() { print("Circle.erase()"); } } ///:~ //: polymorphism/shape/Square.java package polymorphism.shape; import static net.mindview.util.Print.*; public class Square extends Shape { public void draw() { print("Square.draw()"); } public void erase() { print("Square.erase()"); } } ///:~ //: polymorphism/shape/Triangle.java package polymorphism.shape; import static net.mindview.util.Print.*; public class Triangle extends Shape { public void draw() { print("Triangle.draw()"); } public void erase() { print("Triangle.erase()"); } } ///:~ //: polymorphism/shape/RandomShapeGenerator.java // A "factory" that randomly creates shapes. package polymorphism.shape; import java.util.*; public class RandomShapeGenerator { private Random rand = new Random(47); public Shape next() { switch(rand.nextInt(3)) { default: case 0: return new Circle(); case 1: return new Square(); case 2: return new Triangle(); } } } ///:~ //: polymorphism/Shapes.java // Polymorphism in Java. import polymorphism.shape.*; public class Shapes { private static RandomShapeGenerator gen = new RandomShapeGenerator(); public static void main(String[] args) { Shape[] s = new Shape[9]; // Fill up the array with shapes: for(int i = 0; i < s.length; i++) s[i] = gen.next(); // Make polymorphic method calls: for(Shape shp : s) shp.draw(); } } /* Output: Triangle.draw() Triangle.draw() Square.draw() Triangle.draw() Square.draw() Triangle.draw() Square.draw() 198 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Triangle.draw() Circle.draw() *///:~ The base class Shape establishes the common interface to anything inherited from Shape— that is, all shapes can be drawn and erased. The derived classes override these definitions to provide unique behavior for each specific type of shape. RandomShapeGenerator is a kind of “factory” that produces a reference to a randomly- selected Shape object each time you call its next( ) method. Note that the upcasting happens in the return statements, each of which takes a reference to a Circle, Square, or Triangle and sends it out of next( ) as the return type, Shape. So whenever you call next( ), you never get a chance to see what specific type it is, since you always get back a plain Shape reference. main( ) contains an array of Shape references filled through calls to RandomShapeGenerator.next( ). At this point you know you have Shapes, but you don’t know anything more specific than that (and neither does the compiler). However, when you step through this array and call draw( ) for each one, the correct type-specific behavior magically occurs, as you can see from the output when you run the program. The point of creating the shapes randomly is to drive home the understanding that the compiler can have no special knowledge that allows it to make the correct calls at compile time. All the calls to draw( ) must be made through dynamic binding. Exercise 2: (1) Add the @Override annotation to the shapes example. Exercise 3: (1) Add a new method in the base class of Shapes.java that prints a message, but don’t override it in the derived classes. Explain what happens. Now override it in one of the derived classes but not the others, and see what happens. Finally, override it in all the derived classes. Exercise 4: (2) Add a new type of Shape to Shapes.java and verify in main( ) that polymorphism works for your new type as it does in the old types. Exercise 5: (1) Starting from Exercise 1, add a wheels( ) method in Cycle, which returns the number of wheels. Modify ride( ) to call wheels( ) and verify that polymorphism works. Extensibility Now let’s return to the musical instrument example. Because of polymorphism, you can add as many new types as you want to the system without changing the tune( ) method. In a well-designed OOP program, most or all of your methods will follow the model of tune( ) and communicate only with the base-class interface. Such a program is extensible because you can add new functionality by inheriting new data types from the common base class. The methods that manipulate the base-class interface will not need to be changed at all to accommodate the new classes. Consider what happens if you take the instrument example and add more methods in the base class and a number of new classes. Here’s the diagram: Polymorphism 199 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com All these new classes work correctly with the old, unchanged tune( ) method. Even if tune( ) is in a separate file and new methods are added to the interface of Instrument, tune( ) will still work correctly, even without recompiling it. Here is the implementation of the diagram: //: polymorphism/music3/Music3.java // An extensible program. package polymorphism.music3; import polymorphism.music.Note; import static net.mindview.util.Print.*; class Instrument { void play(Note n) { print("Instrument.play() " + n); } String what() { return "Instrument"; } void adjust() { print("Adjusting Instrument"); } } class Wind extends Instrument { void play(Note n) { print("Wind.play() " + n); } String what() { return "Wind"; } void adjust() { print("Adjusting Wind"); } } class Percussion extends Instrument { void play(Note n) { print("Percussion.play() " + n); } String what() { return "Percussion"; } void adjust() { print("Adjusting Percussion"); } } class Stringed extends Instrument { 200 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com void play(Note n) { print("Stringed.play() " + n); } String what() { return "Stringed"; } void adjust() { print("Adjusting Stringed"); } } class Brass extends Wind { void play(Note n) { print("Brass.play() " + n); } void adjust() { print("Adjusting Brass"); } } class Woodwind extends Wind { void play(Note n) { print("Woodwind.play() " + n); } String what() { return "Woodwind"; } } public class Music3 { // Doesn’t care about type, so new types // added to the system still work right: public static void tune(Instrument i) { // i.play(Note.MIDDLE_C); } public static void tuneAll(Instrument[] e) { for(Instrument i : e) tune(i); } public static void main(String[] args) { // Upcasting during addition to the array: Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new Woodwind() }; tuneAll(orchestra); } } /* Output: Wind.play() MIDDLE_C Percussion.play() MIDDLE_C Stringed.play() MIDDLE_C Brass.play() MIDDLE_C Woodwind.play() MIDDLE_C *///:~ The new methods are what( ), which returns a String reference with a description of the class, and adjust( ), which provides some way to adjust each instrument. In main( ), when you place something inside the orchestra array, you automatically upcast to Instrument. You can see that the tune( ) method is blissfully ignorant of all the code changes that have happened around it, and yet it works correctly. This is exactly what polymorphism is supposed to provide. Changes in your code don’t cause damage to parts of the program that should not be affected. Put another way, polymorphism is an important technique for the programmer to “separate the things that change from the things that stay the same.” Polymorphism 201 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Exercise 6: (1) Change Music3.java so that what( ) becomes the root Object method toString( ). Try printing the Instrument objects using System.out.println( ) (without any casting). Exercise 7: (2) Add a new type of Instrument to Music3.java and verify that polymorphism works for your new type. Exercise 8: (2) Modify Music3.java so that it randomly creates Instrument objects the way Shapes.java does. Exercise 9: (3) Create an inheritance hierarchy of Rodent: Mouse, Gerbil, Hamster, etc. In the base class, provide methods that are common to all Rodents, and override these in the derived classes to perform different behaviors depending on the specific type of Rodent. Create an array of Rodent, fill it with different specific types of Rodents, and call your base-class methods to see what happens. Exercise 10: (3) Create a base class with two methods. In the first method, call the second method. Inherit a class and override the second method. Create an object of the derived class, upcast it to the base type, and call the first method. Explain what happens. Pitfall: “overriding” private methods Here’s something you might innocently try to do: //: polymorphism/PrivateOverride.java // Trying to override a private method. package polymorphism; import static net.mindview.util.Print.*; public class PrivateOverride { private void f() { print("private f()"); } public static void main(String[] args) { PrivateOverride po = new Derived(); po.f(); } } class Derived extends PrivateOverride { public void f() { print("public f()"); } } /* Output: private f() *///:~ You might reasonably expect the output to be “public f( )”, but a private method is automatically final, and is also hidden from the derived class. So Derived’s f( ) in this case is a brand new method; it’s not even overloaded, since the base-class version of f( ) isn’t visible in Derived. The result of this is that only non-private methods may be overridden, but you should watch out for the appearance of overriding private methods, which generates no compiler warnings, but doesn’t do what you might expect. To be clear, you should use a different name from a private base-class method in your derived class. 202 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Polymorphism 203 Pitfall: fields and static methods Once you learn about polymorphism, you can begin to think that everything happens polymorphically. However, only ordinary method calls can be polymorphic. For example, if you access a field directly, that access will be resolved at compile time, as the following example demonstrates: 1 //: polymorphism/FieldAccess.java // Direct field access is determined at compile time. class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } } public class FieldAccess { public static void main(String[] args) { Super sup = new Sub(); // Upcast System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } } /* Output: sup.field = 0, sup.getField() = 1 sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0 *///:~ When a Sub object is upcast to a Super reference, any field accesses are resolved by the compiler, and are thus not polymorphic. In this example, different storage is allocated for Super.field and Sub.field. Thus, Sub actually contains two fields called field: its own and the one that it gets from Super. However, the Super version is not the default that is produced when you refer to field in Sub; in order to get the Super field you must explicitly say super.field. Although this seems like it could be a confusing issue, in practice it virtually never comes up. For one thing, you’ll generally make all fields private and so you won’t access them directly, but only as side effects of calling methods. In addition, you probably won’t give the same name to a base-class field and a derived-class field, because its confusing. If a method is static, it doesn’t behave polymorphically: //: polymorphism/StaticPolymorphism.java // Static methods are not polymorphic. class StaticSuper { public static String staticGet() {  1 Thanks to Randy Nichols for asking this question. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com return "Base staticGet()"; } public String dynamicGet() { return "Base dynamicGet()"; } } class StaticSub extends StaticSuper { public static String staticGet() { return "Derived staticGet()"; } public String dynamicGet() { return "Derived dynamicGet()"; } } public class StaticPolymorphism { public static void main(String[] args) { StaticSuper sup = new StaticSub(); // Upcast System.out.println(sup.staticGet()); System.out.println(sup.dynamicGet()); } } /* Output: Base staticGet() Derived dynamicGet() *///:~ static methods are associated with the class, and not the individual objects. Constructors and polymorphism As usual, constructors are different from other kinds of methods. This is also true when polymorphism is involved. Even though constructors are not polymorphic (they’re actually static methods, but the static declaration is implicit), it’s important to understand the way constructors work in complex hierarchies and with polymorphism. This understanding will help you avoid unpleasant entanglements. Order of constructor calls The order of constructor calls was briefly discussed in the Initialization & Cleanup chapter and again in the Reusing Classes chapter, but that was before polymorphism was introduced. A constructor for the base class is always called during the construction process for a derived class, chaining up the inheritance hierarchy so that a constructor for every base class is called. This makes sense because the constructor has a special job: to see that the object is built properly. A derived class has access to its own members only, and not to those of the base class (whose members are typically private). Only the base-class constructor has the proper knowledge and access to initialize its own elements. Therefore, it’s essential that all constructors get called; otherwise the entire object wouldn’t be constructed. That’s why the compiler enforces a constructor call for every portion of a derived class. It will silently call the default constructor if you don’t explicitly call a base-class constructor in the derived-class constructor body. If there is no default constructor, the compiler will complain. (In the case where a class has no constructors, the compiler will automatically synthesize a default constructor.) 204 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... Composing(shared), new Composing(shared), new Composing(shared), new Composing(shared) }; for(Composing c : composing) c.dispose(); } } /* Output: Creating Shared 0 Creating Composing 0 Creating Composing 1 Creating Composing 2 Creating Composing 3 Creating Composing 4 disposing Composing 0 disposing Composing 1 disposing Composing 2 disposing Composing 3 disposing Composing 4 Disposing Shared 0 *///:~ The... void tuneAll(Instrument[] e) { for(Instrument i : e) tune(i); } public static void main(String[] args) { // Upcasting during addition to the array: Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), 224 Thinking in Java Bruce Eckel Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com new Brass(), new Woodwind() }; tuneAll(orchestra); } } /* Output: Wind.play()... Refactor Musics .java by moving the common methods in Wind, Percussion and Stringed into an abstract class Exercise 10: (3) Modify Musics .java by adding a Playable interface Move the play( ) declaration from Instrument to Playable Add Playable to the derived classes by including it in the implement s list Change tune( ) so that it takes a Playable instead of an Instrument Complete decoupling Whenever a method... interfaces/interfaceprocessor/Processor .java package interfaces.interfaceprocessor; public interface Processor { String name(); Object process(Object input); } ///:~ //: interfaces/interfaceprocessor/Apply .java package interfaces.interfaceprocessor; import static net.mindview.util.Print.*; public class Apply { public static void process(Processor p, Object s) { print("Using Processor " + p.name()); print(p.process(s));... input) { return input; } } class Upcase extends Processor { String process(Object input) { // Covariant return return ((String)input).toUpperCase(); } } class Downcase extends Processor { String process(Object input) { return ((String)input).toLowerCase(); } } class Splitter extends Processor { String process(Object input) { // The split() argument divides a String into pieces: return Arrays.toString(((String)input).split("... Composing(Shared shared) { print("Creating " + this); this.shared = shared; this.shared.addRef(); } protected void dispose() { print("disposing " + this); shared.dispose(); } public String toString() { return "Composing " + id; } } public class ReferenceCounting { public static void main(String[] args) { Shared shared = new Shared(); Composing[] composing = { new Composing(shared), new Composing(shared),... abstract print( ) method that is overridden in a derived class The overridden version of the method prints the value of an int variable defined in the derived class At the point of definition of this variable, give it a nonzero value In the base-class constructor, call this method In main( ), create an object of the derived type, and then call its print( ) method Explain the results Exercise 4: (3) Create... reducing 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 Instrument are declared as public, but they’re automatically public anyway: Interfaces 2 23 ... http://www.simpopdf.com //: interfaces/music5/Music5 .java // Interfaces package interfaces.music5; import polymorphism.music.Note; import static net.mindview.util.Print.*; interface Instrument { // Compile-time constant: int VALUE = 5; // static & final // Cannot have method definitions: void play(Note n); // Automatically public void adjust(); } class Wind implements Instrument { public void play(Note n) { print(this... polymorphism/Frog .java // Cleanup and inheritance package polymorphism; import static net.mindview.util.Print.*; class Characteristic { private String s; Characteristic(String s) { this.s = s; print("Creating Characteristic " + s); } protected void dispose() { print("disposing Characteristic " + s); } } class Description { private String s; Description(String s) { this.s = s; print("Creating Description . Creating Shared 0 Creating Composing 0 Creating Composing 1 Creating Composing 2 Creating Composing 3 Creating Composing 4 disposing Composing 0 disposing Composing 1 disposing Composing. Description(String s) { this.s = s; print("Creating Description " + s); } protected void dispose() { print("disposing Description " + s); } } 206 Thinking in Java Bruce. Object method toString( ). Try printing the Instrument objects using System.out.println( ) (without any casting). Exercise 7: (2) Add a new type of Instrument to Music3 .java and verify that

Ngày đăng: 14/08/2014, 00:21

TỪ KHÓA LIÊN QUAN