CATEGORII DOCUMENTE |
Asp | Autocad | C | Dot net | Excel | Fox pro | Html | Java |
Linux | Mathcad | Photoshop | Php | Sql | Visual studio | Windows | Xml |
As usual, constructors are different from other kinds of methods. This is also true when polymorphism is involved. Even though constructors are not polymorphic (although you can have a kind of "virtual constructor," as you will see in Chapter 11), it's important to understand the way constructors work in complex hierarchies and with polymorphism. This understanding will help you avoid unpleasant entanglements.
The order of constructor calls was briefly discussed in Chapter 4, but that was before inheritance and polymorphism were introduced.
A constructor for the base class is always called in the constructor for a derived class, chaining upward 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 properly. 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.)
Let's take a look at an example that shows the effects of composition, inheritance, and polymorphism on the order of construction:
//: Sandwich.java
// Order of constructor calls
class Meal
}
class Bread
}
class Cheese
}
class Lettuce
}
class Lunch extends Meal
}
class PortableLunch extends Lunch
}
class
public static void main(String[] args)
} ///:~
This example creates a complex class out of other classes,
and each class has a constructor that announces itself.
The important class is
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
This means that the order of constructor calls for a complex object is as follows:
The base-class constructor is called. This step is repeated recursively such that the root of the hierarchy is constructed first, followed by the next-derived class, etc., until the most-derived class is reached.
Member initializers are called in the order of declaration.
The body of the derived-class constructor is called.
The order of the constructor calls is important. When you inherit, you know all about the base class and can access any public and protected members of the base class. This means that you must be able to assume that all the members of the base class are valid when you're in the derived class. In a normal method, construction has already taken place, so all the members of all parts of the object have been built. inside the constructor, however, you must be able to assume that all members that you use have been built. The only way to guarantee this is for the base-class constructor to be called first. Then when you're in the derived-class constructor, all the members you can access in the base class have been initialized. "Knowing that all members are valid" inside the constructor is also the reason that, whenever possible, you should initialize all member objects (that is, objects placed in the class using composition) at their point of definition in the class (e.g.: b, c, and l in the example above). If you follow this practice, you will help ensure that all base class members and member objects of the current object have been initialized. Unfortunately, this doesn't handle every case, as you will see in the next section.
When you use composition to create a new class, you never worry about finalizing the member objects of that class. Each member is an independent object and thus is garbage collected and finalized regardless of whether it happens to be a member of your class. With inheritance, however, you must override finalize( ) in the derived class if you have any special cleanup that must happen as part of garbage collection. When you override finalize( ) in an inherited class, it's important to remember to call the base-class version of finalize( ), since otherwise the base-class finalization will not happen. The following example proves this:
//: Frog.java
// Testing finalize with inheritance
class DoBaseFinalization
class Characteristic
protected void finalize()
}
class LivingCreature
protected void finalize() catch(Throwable t)
}
}
class Animal extends LivingCreature
protected void finalize() catch(Throwable t)
}
}
class Amphibian extends Animal
protected void finalize() catch(Throwable t)
}
}
public class Frog extends Amphibian
protected void finalize() catch(Throwable t)
}
public static void main(String[] args)
} ///:~
The class DoBaseFinalization simply holds a flag that indicates to each class in the hierarchy whether to call super.finalize( ). This flag is set based on a command-line argument, so you can view the behavior with and without base-class finalization.
Each class in the hierarchy also contains a member object of class Characteristic. You will see that regardless of whether the base class finalizers are called, the Characteristic member objects are always finalized.
Each overridden finalize( ) must have access to at least protected members since the finalize( ) method in class Object is protected and the compiler will not allow you to reduce the access during inheritance. ("Friendly" is less accessible than protected.)
In Frog.main( ), the DoBaseFinalization flag is configured and a single Frog object is created. Remember that garbage collection and in particular finalization might not happen for any particular object so to enforce this, System.runFinalizersOnExit(true) adds the extra overhead to guarantee that finalization takes place. Without base-class finalization, the output is:
not finalizing bases
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
You can see that, indeed, no finalizers are called for the base classes of Frog. But if you add the "finalize" argument on the command line, you get:
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
Amphibian finalize
Animal finalize
LivingCreature finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
Although the order the member objects are finalized is the same order that they are created, technically the order of finalization of objects is unspecified. With base classes, however, you have control over the order of finalization. The best order to use is the one that's shown here, which is the reverse of the order of initialization. Following the form that's used in C++ for destructors, you should perform the derived-class finalization first, then the base-class finalization. That's because the derived-class finalization could call some methods in the base class that require that the base-class components are still alive, so you must not destroy them prematurely.
The hierarchy of constructor calls brings up an interesting dilemma. What happens if you're inside a constructor and you call a dynamically-bound method of the object being constructed? Inside an ordinary method you can imagine what will happen - the dynamically-bound call is resolved at run-time because the object cannot know whether it belongs to the class the method is in or some class derived from it. For consistency, you might think this is what should happen inside constructors.
This is not exactly the case. If you call a dynamically-bound method inside a constructor, the overridden definition for that method is used. However, the effect can be rather unexpected, and can conceal some difficult-to-find bugs.
Conceptually, the constructor's job is to bring the object into existence (which is hardly an ordinary feat). Inside any constructor, the entire object might be only partially formed - you can know only that the base-class objects have been initialized, but you cannot know which classes are inherited from you. A dynamically-bound method call, however, reaches "forward" or "outward" into the inheritance hierarchy. It calls a method in a derived class. If you do this inside a constructor, you call a method that might manipulate members that haven't been initialized yet - a sure recipe for disaster.
You can see the problem in the following example:
//: PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
abstract class Glyph
}
class RoundGlyph extends Glyph
void draw()
}
public class PolyConstructors
} ///:~
In Glyph, the draw( ) method is abstract, so it is designed to be overridden. Indeed, you are forced to override it in RoundGlyph. But the Glyph constructor calls this method, and the call ends up in RoundGlyph.draw( ), which would seem to be the intent. But look at the output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
When Glyph's constructor calls draw( ), the value of radius isn't even the default initial value 1. It's zero. This would probably result in either a dot or nothing at all being drawn on the screen, and you'd be staring, trying to figure out why the program won't work.
The order of initialization described in the previous section isn't quite complete, and that's the key to solving the mystery. The actual process of initialization is:
The storage allocated for the object is initialized to binary zero before anything else happens.
The base-class constructors are called as described previously. At this point, the overridden draw( ) method is called, (yes, before the RoundGlyph constructor is called), which discovers a radius value of zero, due to step 1.
Member initializers are called in the order of declaration.
The body of the derived-class constructor is called.
There's an upside to this, which is that everything is at least initialized to zero (or whatever zero means for that particular data type) and not just left as garbage. This includes object handles that are embedded inside a class via composition. So if you forget to initialize that handle you'll get an exception at run time. Everything else gets zero, which is usually a telltale value when looking at output.
On the other hand, you should be pretty horrified at the outcome of this program. You've done a perfectly logical thing and yet the behavior is mysteriously wrong, with no complaints from the compiler. (C++ produces more rational behavior in this situation.) Bugs like this could easily be buried and take a long time to discover.
As a result, a good guideline for constructors is, "Do as little as possible to set the object into a good state, and if you can possibly avoid it, don't call any methods." The only safe methods to call inside a constructor are those that are final in the base class. (This also applies to private methods, which are automatically final.) These cannot be overridden and thus cannot produce this kind of surprise.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 907
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved