8. Reuse (2)

Chapter Summary

  • Combining composition and inheritance
    • Ensure proper cleaning
    • name hidden
  • Choices between composition and inheritance
  • protected
  • Upward transformation
    • Revisiting Composition and Inheritance

Combining Composition and Inheritance

You will often use composition and inheritance together. The following example shows class creation using inheritance and composition, along with the necessary constructor initialization:

class Plate {
    
    
    Plate(int i) {
    
    
        System.out.println("Plate constructor");
    }
}

class DinnerPlate extends Plate {
    
    
    DinnerPlate(int i) {
    
    
        super(i);
        System.out.println("DinnerPlate constructor");
    }
}

class Utensil {
    
    
    Utensil(int i) {
    
    
        System.out.println("Utensil constructor");
    }
}

class Spoon extends Utensil {
    
    
    Spoon(int i) {
    
    
        super(i);
        System.out.println("Spoon constructor");
    }
}

class Fork extends Utensil {
    
    
    Fork(int i) {
    
    
        super(i);
        System.out.println("Fork constructor");
    }
}

class Knife extends Utensil {
    
    
    Knife(int i) {
    
    
        super(i);
        System.out.println("Knife constructor");
    }
}

// A cultural way of doing something:
class Custom {
    
    
    Custom(int i) {
    
    
        System.out.println("Custom constructor");
    }
}

public class PlaceSetting extends Custom {
    
    
    private Spoon sp;
    private Fork frk;
    private Knife kn;
    private DinnerPlate pl;

    public PlaceSetting(int i) {
    
    
        super(i + 1);
        sp = new Spoon(i + 2);
        frk = new Fork(i + 3);
        kn = new Knife(i + 4);
        pl = new DinnerPlate(i + 5);
        System.out.println("PlaceSetting constructor");
    }

    public static void main(String[] args) {
    
    
        PlaceSetting x = new PlaceSetting(9);
    }
}

Insert image description here

Although the compiler forces you to initialize the base class and requires you to initialize the base class at the beginning of the constructor, it does not monitor you to ensure that you initialize the member objects. Notice how the classes are cleanly separated. You don't even need source code for method reuse code. You can only import at most one package. (This is true of both inheritance and composition.)

Ensure proper cleaning

Java does not have the concept of destructors in C++, which are methods that are called automatically when an object is destroyed. The reason may be that in Java, objects are usually forgotten rather than destroyed, allowing the garbage collector to reclaim memory as needed. Usually this is fine, but sometimes your class may perform some activities during its lifetime that need to be cleaned up. The initialization and cleanup section mentioned that you have no way of knowing when the garbage collector will be called, or even if it will be called. Therefore, if you want to clean something up for a class, you must explicitly write a special method to do it, and make sure the client programmer knows they have to call this method. Most importantly - as described in the "Exceptions" chapter - you must prevent exceptions by placing such cleanup in **finally** clauses.

Consider an example of a CAD system that draws pictures on the screen:

class Shape {
    
    
    Shape(int i) {
    
    
        System.out.println("Shape constructor");
    }

    void dispose() {
    
    
        System.out.println("Shape dispose");
    }
}

class Circle extends Shape {
    
    
    Circle(int i) {
    
    
        super(i);
        System.out.println("Drawing Circle");
    }

    @Override
    void dispose() {
    
    
        System.out.println("Erasing Circle");
        super.dispose();
    }
}

class Triangle extends Shape {
    
    
    Triangle(int i) {
    
    
        super(i);
        System.out.println("Drawing Triangle");
    }

    @Override
    void dispose() {
    
    
        System.out.println("Erasing Triangle");
        super.dispose();
    }
}

class Line extends Shape {
    
    
    private int start, end;

    Line(int start, int end) {
    
    
        super(start);
        this.start = start;
        this.end = end;
        System.out.println(
                "Drawing Line: " + start + ", " + end);
    }

    @Override
    void dispose() {
    
    
        System.out.println(
                "Erasing Line: " + start + ", " + end);
        super.dispose();
    }
}

public class CADSystem extends Shape {
    
    
    private Circle c;
    private Triangle t;
    private Line[] lines = new Line[3];

    public CADSystem(int i) {
    
    
        super(i + 1);
        for (int j = 0; j < lines.length; j++)
            lines[j] = new Line(j, j * j);
        c = new Circle(1);
        t = new Triangle(1);
        System.out.println("Combined constructor");
    }

    @Override
    public void dispose() {
    
    
        System.out.println("CADSystem.dispose()");
        // The order of cleanup is the reverse
        // of the order of initialization:
        t.dispose();
        c.dispose();
        for (int i = lines.length - 1; i >= 0; i--) {
    
    
            lines[i].dispose();
        }
        super.dispose();
    }

    public static void main(String[] args) {
    
    
        CADSystem x = new CADSystem(47);
        try {
    
    
            // Code and exception handling...
        } finally {
    
    
            x.dispose();
        }
    }
}

Insert image description here

Everything in this system is some kind of Shape (itself an Object since it implicitly inherits from the root class). In addition to calling the base class version of the method using super , each class also overrides dispose()the method. The specific Shape classes— Circle , Triangle , and Line —all have "draw" constructors, although any method called during the object's lifetime can be responsible for doing whatever needs to be cleaned up. Each class has its own dispose()method for restoring non-memory contents to the state before the object existed.

In main(), there are two keywords that you haven't seen before and won't be explained in detail until the "Exceptions" chapter: try and finally . The try keyword indicates that the following block (delimited by curly braces) is a protected region, which means it gets special treatment. One of the special treatment is that no matter how the try block exits, the code in the finally clause after this guard area is always executed. (Through exception handling, the try block can be left in many unusual ways .) Here, the finally clause means, "No matter what happens, always call x.dispose()."

In the cleanup method (in this case dispose()), you must also pay attention to the order in which the base class and member object cleanup methods are called, in case one subobject depends on the other. First, any class-specific cleanup is performed in the reverse order of creation. (In general, this requires that the base class element is still accessible.) Then call the base class cleanup method, as shown here.

In many cases, cleanup isn't an issue; you just let the garbage collector do the job. However, when you have to perform explicit cleanup, it takes a little more effort and care because there's nothing to rely on in terms of garbage collection. The garbage collector may never be called. If called, it can recycle objects in any order it wants. You can't rely on garbage collection for anything except memory reclamation. If you want to clean up, you can use your own cleanup method, don't use it finalize().

name hidden

If a method name of a Java base class is overloaded multiple times, redefining the method name in a derived class does not hide any base class version. Overloading works regardless of whether the method is defined at this level or in a base class:

class Homer {
    
    
    char doh(char c) {
    
    
        System.out.println("doh(char)");
        return 'd';
    }

    float doh(float f) {
    
    
        System.out.println("doh(float)");
        return 1.0f;
    }
}

class Milhouse {
    
    
}

class Bart extends Homer {
    
    
    void doh(Milhouse m) {
    
    
        System.out.println("doh(Milhouse)");
    }
}

public class Hide {
    
    
    public static void main(String[] args) {
    
    
        Bart b = new Bart();
        b.doh(1);
        b.doh('x');
        b.doh(1.0f);
        b.doh(new Milhouse());
    }
}

Insert image description here

All of Homer 's overloaded methods are available in Bart , although Bart introduces a new overloaded method. As you'll see in the next chapter, it's more common than overloading to override a method of the same name, using exactly the same method signature and return type as in the base class. Otherwise it would be confusing.

You have seen the Java 5 **@Override** annotation, it is not a keyword, but it can be used like a keyword. You can choose to add this annotation when you intend to override a method, and if you accidentally use overloading instead of overriding, the compiler will generate an error message:

// reuse/Lisa.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// {WillNotCompile}

class Lisa extends Homer {
    
    
  @Override void doh(Milhouse m) {
    
    
    System.out.println("doh(Milhouse)");
  }
}

The {WillNotCompile} tag excludes this file from the Gradle build of this book, but if you compile it by hand, you will see: method does not override a method from its superclass. Methods do not override methods in superclasses, The **@Override** annotation prevents you from accidentally overloading.

Choices between composition and inheritance

Both composition and inheritance allow placing subobjects in new classes (composition is explicit, while inheritance is implicit). You may be wondering the difference between the two and how to choose between the two.

Use composition, not inheritance, when you want to include the functionality of an existing class in a new class. That is, embed an object (usually private) in the new class to implement its functionality. Users of the new class see the interface of the new class you defined, not the interface of the embedded object.

Sometimes it makes sense to give users of a class direct access to the composed components in the new class. Just declare the member object as public (think of this as a type of "semi-delegation"). Member objects hide the implementation, so it's safe. When the user knows you are assembling a set of parts, it makes the interface easier to understand. The following car object is a good example:

// reuse/Car.java
// Composition with public objects
class Engine {
    
    
    public void start() {
    
    }
    public void rev() {
    
    }
    public void stop() {
    
    }
}

class Wheel {
    
    
    public void inflate(int psi) {
    
    }
}

class Window {
    
    
    public void rollup() {
    
    }
    public void rolldown() {
    
    }
}

class Door {
    
    
    public Window window = new Window();
    
    public void open() {
    
    }
    public void close() {
    
    }
}

public class Car {
    
    
    public Engine engine = new Engine();
    public Wheel[] wheel = new Wheel[4];
    public Door left = new Door(), right = new Door(); // 2-door
    
    public Car() {
    
    
        for (int i = 0; i < 4; i++) {
    
    
            wheel[i] = new Wheel();
        }
    }
    
    public static void main(String[] args) {
    
    
        Car car = new Car();
        car.left.window.rollup();
        car.wheel[0].inflate(72);
    }
}

Because the combination of car in this example is also part of the problem analysis (not part of the underlying design), declaring members as public helps client programmers understand how to use the class and reduces the code complexity faced by the class creator. However, remember this is a special case. In general, properties should still be declared private .

When using inheritance, take an existing class and develop a new version of it. Usually this means taking a generic class and specializing it for a particular need. If you think about it for a moment, you will find that it makes no sense to use a vehicle object to form a car - the car does not contain transportation, it is transportation. This "is a" relationship is expressed using inheritance, while the "has a" relationship is expressed using composition.

protected

Now that inheritance has been touched, the keyword protected becomes meaningful. In an ideal world, the keyword private alone would suffice. In actual projects, we often want to hide a thing from the outside world as much as possible, and allow members of derived classes to access it.

The keyword protected serves this purpose. It says "This is private as far as the user of the class is concerned . But it is accessible to any subclass that inherits it or is in the same package." ( protected also provides package access)

Although it is possible to create protected properties, it is best to declare the property as private to always retain the right to change the underlying implementation. Then control the access rights of the inheritors of the class through protected .

// reuse/Orc.java
// The protected keyword
class Villain {
    
    
    private String name;
    
    protected void set(String nm) {
    
    
        name = nm;
    }
    
    Villain(String name) {
    
    
        this.name = name;
    }
    
    @Override
    public String toString() {
    
    
        return "I'm a Villain and my name is " + name;
    }
}

public class Orc extends Villain {
    
    
    private int orcNumber;
    
    public Orc(String name, int orcNumber) {
    
    
        super(name);
        this.orcNumber = orcNumber;
    }
    
    public void change(String name, int orcNumber) {
    
    
        set(name); // Available because it's protected
        this.orcNumber = orcNumber;
    }
    
    @Override
    public String toString() {
    
    
        return "Orc " + orcNumber + ": " + super.toString();
    }
    
    public static void main(String[] args) {
    
    
        Orc orc = new Orc("Limburger", 12);
        System.out.println(orc);
        orc.change("Bob", 19);
        System.out.println(orc);
    }
}

Output:

Orc 12: I'm a Villain and my name is Limburger
Orc 19: I'm a Villain and my name is Bob

change()The method can access set()the method because set()the method is protected . Note that the methods of class OrctoString() also use the base class' version.

Upward transformation

The most important aspect of inheritance is not providing methods for new classes. It is a relationship between the new class and the base class. In short, this relationship can be expressed as "the new class is a type of the existing class".

This description is not a fancy way of explaining inheritance, it is directly supported by the language. For example, suppose there is a base class Instrument representing a musical instrument and a derived class Wind . Because inheritance ensures that all methods of the base class are also available in the derived class, any message sent to the base class can also be sent to the derived class. If Instrument has a play()method, Wind has that method too. This means that you can accurately say that the Wind object is also a type of Instrument . The following example shows how the compiler supports this concept:

// reuse/Wind.java
// Inheritance & upcasting
class Instrument {
    
    
    public void play() {
    
    }
    
    static void tune(Instrument i) {
    
    
        // ...
        i.play();
    }
}

// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
    
    
    public static void main(String[] args) {
    
    
        Wind flute = new Wind();
        Instrument.tune(flute); // Upcasting
    }
}

tune()The method accepts a reference of type Instrument . However, in Wind 's main()method, tune()the method passes in a Wind reference. Given Java's strict type checking, it might seem odd that a method that accepts one type accepts another, unless you realize that a Wind object is also an Instrument object, and that Instrument 's tunemethods must exist on Wind . In tune(), the code works on Instrument and all derived classes of Instrument . This behavior of converting Wind references to Instrument references is called _upcast_.

The term is based on the traditional class inheritance graph: the top of the graph is the root, and then spreads down. (Of course, you can draw any class diagram you think is helpful.) Then, the class diagram of Wind.java is:

Insert image description here

In the inheritance diagram, the transformation from a derived class to a base class is upward, so it is usually called "upward transformation". Because you are converting from a more specific class to a more general class, upcasting is always safe. In other words, a derived class is a superset of the base class. It may contain more methods than the base class, but it must have at least the same methods as the base class. During upward transformation, the class interface may only lose methods but not add methods. This is why the compiler still allows upward casts without any explicit casts or other special markers.

It is also possible to perform downcasting as the opposite of upcasting, but there are problems, which are explored in more depth in the next chapter and in the "Type Information" chapter.

Revisiting Composition and Inheritance

In object-oriented programming, the most likely way to create and use code is to package data and methods together into a class, and then use objects of that class. You can also use existing classes to create new classes through composition. Inheritance is actually not very commonly used. So while we emphasize inheritance many times in teaching OOP, it doesn't mean to use it whenever possible. Quite the contrary, try to use it sparingly, unless it really helps to use inheritance. One of the clearest ways to judge whether to use composition or inheritance is to ask yourself whether you need to upcast the new class to the base class. If upward transformation is necessary, then inheritance is necessary, but if not, further consideration should be given to whether inheritance should be used. The "Polymorphism" chapter presents one of the strongest reasons to use upcasting, but you can choose the better of the two by simply remembering to ask "Do I need upcasting?"

Guess you like

Origin blog.csdn.net/GXL_1012/article/details/132216715