六. 多态

六. 多态

多态是面向对象编程语言中,继封装和继承之外的第三个重要特性。多态主要是消除类型之间的耦合。

1. 方法调用绑定

将一个方法调用和一个方法主体关联起来称作绑定。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做前期绑定

多态的一个特点是父类引用指向子类对象,在编译时编译器只看作父类的类型,无法得知究竟会调用哪个方法,而在运行时才确定对象类型。通过一种后期绑定也叫动态绑定的方法来实现。代码示例:

public class Test {

    public static void test(Fruit fruit) {
        fruit.func();
    }

    public static void main(String[] args) {
        Fruit apple1 = new Apple();
        test(apple1);
        Apple apple2 = new Apple();
        test(apple2);
        Orange orange = new Orange();
        test(orange);
    }
}

class Fruit {

    public void func() {
        System.out.println("This is the Fruit.");
    }
}

class Apple extends Fruit {

    @Override
    public void func() {
        System.out.println("This is the apple.");
    }
}

class Orange extends Fruit {

    @Override
    public void func() {
        System.out.println("This is the orange.");
    }
}

代码结果:

image-20200723193424591

Java 中除了 staticfinal 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定。

2. 可扩展性

可以从上面那个例子中看到,由于多态机制,可以向系统中添加任意多的新类型,而不需要修改 test() 方法。

3. 两个“陷阱”

第一个:“重写”私有方法

private 方法可以当作是 final 的,对于派生类来说是隐蔽的。因此,如果子类写了一个与父类中私有方法相同名的方法,实际是一个与父类无关的全新的方法,根本不算是重写方法。(可以使用@Override注解检测出问题)

第二个:属性与静态方法

只有普通的方法调用可以是多态的,如果直接访问一个属性,该访问会在编译时解析:

public class FieldAccess {
    public static void main(String[] args) {
        Super sup = new Sub(); // 向上转型
        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());
    }
}

class Super {
    public int field = 0;

    public int getField() {
        return field;
    }
}

class Sub extends Super {
    public int field = 1;

    @Override
    public int getField() {
        return field;
    }

    public int getSuperField() {
        return super.field;
    }
}

输出结果:

image-20200723201156866

Sub 对象向上转型为 Super 引用时,任何属性访问都被编译器解析,因此不是多态的。在这个例子中,Super.fieldSub.field 被分配了不同的存储空间,Sub 实际上包含了两个称为 field 的属性:它自己的和来自 Super 的。然而,在引用 Subfield 时,默认的 field 属性并不是 Super 版本的 field 属性。为了获取 Superfield 属性,需要显式地指明 super.field

当然,在实际中基本不会有上述情况,因为通常会将所有的属性都指明为 private,而不能直接访问它们,只能通过方法来访问。此外,一般也不会给基类属性和派生类属性起相同的名字,这样做会令人困惑。

此外,如果一个方法是静态(static)的,它的行为就不具有多态性。(静态的方法只与类关联,与单个的对象无关。)

4. 构造器调用顺序

构造器不同于其他类型的方法,它并不具有多态性(我们会把它看作是隐式声明的静态方法)。

在派生类的构造过程中总会调用基类的构造器,初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。

原因:构造器需要检查对象是否被正确地构造。由于属性通常声明为 private,派生类只能访问自己的成员而不能访问基类的私有成员。只有基类的构造器拥有权限来初始化自身的元素,因此必须得调用所有构造器。(如果在派生类的构造器主体中没有显式地调用基类构造器,编译器会默默调用无参构造器。)

下面的例子展示了使用了组合、继承和多态之后的构建顺序:

public class Sandwich extends PortableLunch {
    private Bread b = new Bread();
    private Cheese c = new Cheese();
    private Lettuce l = new Lettuce();

    public Sandwich() {
        System.out.println("Sandwich()");
    }

    public static void main(String[] args) {
        new Sandwich();
    }
}

class Meal {
    Meal() {
        System.out.println("Meal()");
    }
}

class Bread {
    Bread() {
        System.out.println("Bread()");
    }
}

class Cheese {
    Cheese() {
        System.out.println("Cheese()");
    }
}

class Lettuce {
    Lettuce() {
        System.out.println("Lettuce()");
    }
}

class Lunch extends Meal {
    Lunch() {
        System.out.println("Lunch()");
    }
}

class PortableLunch extends Lunch {
    PortableLunch() {
        System.out.println("PortableLunch()");
    }
}

输出结果:

image-20200723205138912

从创建 Sandwich 对象的输出中可以看出对象的构造器调用顺序如下:

  1. 先是基类构造器被调用。这个步骤被不断重复,使得顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。
  2. 按声明顺序初始化成员。
  3. 调用派生类构造器的方法体。

5. 协变返回类型

Java 5 中引入了协变返回类型,这表示派生类的被重写方法可以返回基类方法返回类型的派生类型:

public class CovariantReturn {
    public static void main(String[] args) {
        Mill m = new Mill();
        Grain g = m.process();
        System.out.println(g);
        m = new WheatMill();
        g = m.process();
        System.out.println(g);
    }
}

class Grain {
    @Override
    public String toString() {
        return "Grain";
    }
}

class Wheat extends Grain {
    @Override
    public String toString() {
        return "Wheat";
    }
}

class Mill {
    Grain process() {
        return new Grain();
    }
}

class WheatMill extends Mill {
    @Override
    Wheat process() {
        return new Wheat();
    }
}

输出结果:

image-20200723211241701

关键区别在于 Java 5 之前的版本强制要求被重写的 process() 方法必须返回 Grain 而不是 Wheat,即使 Wheat 派生自 Grain,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的 Wheat 类型。

6. 向下转型与运行时类型信息

由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,那么为了重新获取类型信息,就需要在继承层次中向下移动,使用向下转型

向上转型永远是安全的,因为基类不会具有比派生类更多的接口。但是对于向下转型,无法知道一个 Fruit 是 Apple,还是 Orange 或其他一些类型。

在某些语言中(如 C++),必须执行一个特殊的操作来获得安全的向下转型,但是在 Java 中,每次转型都会被检查来确保它是希望的那种类型。如果不是,就会得到 ClassCastException (类转型异常)。

例如:Apple 向上转型为 Fruit 之后只能再向下转型为 Apple 而不能转为 Orange。

参考资料:On Java 8

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值