Thinking in Java - Study Notes - (8) Polymorphism

Java Programming Ideas - Chapter 8 - Polymorphism

In object-oriented programming languages, polymorphism is the third fundamental feature after data abstraction and inheritance.

Polymorphism separates interface and implementation from another perspective by separating what to do and how to do it. Not only does polymorphism improve code organization and readability, it also creates extensible programs—programs that "grow" both when a project is initially created and as new functionality needs to be added.

  • "Encapsulation" creates new data types by combining characteristics and behaviors. "
  • Implementation hiding" separates the interface from the implementation by "privatizing" the details.
  • The role of polymorphism is to eliminate the coupling relationship between types.
  • Inheritance allows an object to be treated as either its own type or its base type, and the same code can run on these different types without distinction.

Polymorphism is also known as dynamic binding, late binding or runtime binding .

method call binding

Associating a method call with a method body is called binding .
If the binding is performed before the program is executed (by the compiler and linker, if any), it is called early binding .

    public static void doSomething(Shape i) {
    // ...
        i.play();
    }

The doSomething method accepts a Shape reference, so in this case, how does the compiler know that the Shape reference refers to a Circle object and not a Triangle or Rectangle object? In fact, the compiler has no way of knowing.

The solution is late binding , which means binding according to the type of object at runtime.

Except for static methods and final methods ( private methods are final methods) in Java , all other methods are late binding.

final prevents others from overriding the method. But perhaps more importantly: Doing so effectively "turns off" dynamic binding, or tells the compiler that it doesn't need to be dynamically bound.

Weaknesses: Fields and Static Methods

Once you understand the mechanics of polymorphism, you might start to think that everything can happen polymorphically. However, only ordinary method calls can be polymorphic. For example, if you access a field directly, the access will be resolved at compile time.

class FieldAccess {
    public static void main(String[] args) {
        Parent parent = new Son();
        System.out.printf("parent.field = %s,\nparent.getField() = %s,\n", parent.field, parent.getField());
        Son son = new Son();
        System.out.printf("son.field = %s,\nson.getField() = %s\n", son.field, son.getField());
    }
}

class Parent {

    Parent() { }
    public String field = "Parent";
    public String getField() {
        return field;
    }

}

public class Son extends Parent {
    //    public static String s = "static field of Son";
    public Son(){
    }
    public String field = "Son";
    public String getField() {
        return field;
    }
} /*
输出结果
parent.field = Parent,    //注意这里
parent.getField() = Son,
son.field = Son,
son.getField() = Son
*/

If a method is static, its behavior is not polymorphic. Static methods are associated with classes, not individual objects.

Constructors and Polymorphism

Although constructors are not polymorphic (they are actually static methods, except that the static declaration is implicit), it is important to understand how constructors behave in complex hierarchies through polymorphism.

The order in which the constructors are called

one example:

class Counter {
    private static int i = 0;
    static int increment() {
        return i++;
    }
}
class Meal {
    static {
        System.out.printf("%2d. Meal(), static\n", Counter.increment());
    }
    {
        System.out.printf("%2d. Meal(), non-static\n", Counter.increment());
    }
    Meal() { System.out.printf("%2d. Meal()\n", Counter.increment()); }
}
class Bread {
    static {
        System.out.printf("%2d. Bread(), static\n", Counter.increment());
    }
    {
        System.out.printf("%2d. Bread(), non-static\n", Counter.increment());
    }
    Bread() { System.out.printf("%2d. Bread()\n", Counter.increment()); }
}
class Cheese {
    static {
        System.out.printf("%2d. Cheese(), static\n", Counter.increment());
    }
    {
        System.out.printf("%2d. Cheese(), non-static\n", Counter.increment());
    }
    Cheese() { System.out.printf("%2d. Cheese()\n", Counter.increment()); }
}
class Lunch extends Meal {
    static {
        System.out.printf("%2d. Lunch(), static\n", Counter.increment());

    }
    {
        System.out.printf("%2d. Lunch(), non-static\n", Counter.increment());
    }
    Lunch() { System.out.printf("%2d. Lunch()\n", Counter.increment()); }
}
class PortableLunch extends Lunch {
    static {
        System.out.printf("%2d. PortableLunch, static\n", Counter.increment());
    }

    PortableLunch() { System.out.printf("%2d. PortableLunch()\n", Counter.increment()); }
    {
        System.out.printf("%2d. PortableLunch(), non-static\n", Counter.increment());
    }
}


public class Sandwich extends PortableLunch {
    static {
        System.out.printf("%2d. Sandwich(), static\n", Counter.increment());
    }
    // 非static成员按顺序初始化
    private Bread b = new Bread();
    private Cheese c = new Cheese();

    public Sandwich() { System.out.printf("%2d. Sandwich()\n", Counter.increment()); }
    public static void main(String[] args) {
        new Sandwich();
    }
    {
        System.out.printf("%2d. Sandwich(), non-static\n", Counter.increment());
    }
}/* 输出结果
 0. Meal(), static
 1. Lunch(), static
 2. PortableLunch, static
 3. Sandwich(), static
 4. Meal(), non-static
 5. Meal()
 6. Lunch(), non-static
 7. Lunch()
 8. PortableLunch(), non-static
 9. PortableLunch()
10. Bread(), static
11. Bread(), non-static
12. Bread()
13. Cheese(), static
14. Cheese(), non-static
15. Cheese()
16. Sandwich(), non-static
17. Sandwich()
*/

The order in which constructors are called during class initialization:

  1. call base class constructor

  2. Initiate methods of members are called in declaration order

  3. The body that invokes the constructor of the exported class.

Behavior of polymorphic methods inside constructors

What happens if a dynamically bound method of the object being constructed is called inside a constructor?

Inside a normal method, the invocation of dynamic binding is determined at runtime, because the object has no way of knowing whether it belongs to the class in which the method resides, or a derived class of that class.

class Glyph {
    void draw() {
        System.out.println("Glyph.draw()");
    }
    Glyph() {
        System.out.println("Glyph() before draw()");
        draw();
        System.out.println("Glyph() after draw()");
    }

}
class RoundGlyph extends Glyph {
    // 这里初始化为1,但是输出结果中却是0
    private int radius = 1;
    RoundGlyph(int r) {
        radius = r;
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }

    @Override
    void draw() {
        super.draw();
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}
public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}/* 输出结果
Glyph() before draw()
Glyph.draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.draw(), radius = 5
*/

The actual process of initialization is:

  1. Initialize the storage space allocated to the object to binary zeros before anything else happens

  2. Call the base class constructor. At this point, the overridden draw() method is called (to be called before the RoundGlyph constructor is called. Due to step 1, we will find that the value of radius is 0 at this time.

  3. The initialization methods of members are called in the order in which they are declared.

  4. Calls the constructor body of the exported class.

This has the advantage that everything is at least initialized to zero (or the equivalent of "zero" in some special data type), rather than just being left as garbage.

We should be quite shocked by the results of this procedure. Logic-wise, we've done it perfectly, but it's behaving incredibly wrong, and the compiler doesn't complain.

There is a valid guideline when writing constructors: "Use the simplest possible way to bring the object to its normal state; if possible, avoid calling other methods".

The only methods that are safe to call inside a constructor are final methods in the base class (also for private methods, which are automatically final ). These methods cannot be overridden, so the above surprising problem does not arise.

Design with Inheritance

A general guideline is: "Use inheritance to express differences in behavior, and fields to express changes in state."

Downcasting and Runtime Type Recognition

Since upcasting (moving up the inheritance hierarchy) loses concrete type information, we thought that by downcasting—that is, moving down the inheritance hierarchy—we should be able to get type information.
In Java, all casts are checked! Even if we just do an ordinary type conversion in the form of another bracket, it will still be checked when entering runtime. This behavior of checking types at runtime is called "runtime type identification" ( RTTI ).

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325162100&siteId=291194637