首页 > 编程笔记 > Java笔记 阅读:18

Java多态用法详解(附带实例)

多态是面向对象的一大特性,封装和继承是为实现多态性做准备的。

多态是多种形态能力的特性。它可以提高程序的抽象程度和简洁性,最大限度降低类和程序模块间的耦合性。

Java 语言中,多态性体现在两方面,由方法重载实现的静态多态性(编译时多态)和方法重写实现的动态多态性(也称动态联编):

Java多态的实现

父类对象变量可指向子类对象形成多态,下面通过一个例子演示多态的实现。

【实例】定义一个代表动物的类 Animal,具有方法 eat(),再定义 Animal 的两个子类 Rabbit 和 Tiger,分别覆盖父类中的 eat() 方法,最后定义测试类进行测试。
class Pet {
    public void eat() {
        System.out.println("宠物吃宠物粮");
    }
}

class Dog extends Pet {
    public void eat() {
        System.out.println("狗吃狗粮");
    }
}

class Cat extends Pet {
    public void eat() {
        System.out.println("猫吃猫粮");
    }
}

public class TestAnimal {
    public static void main(String[] args) {
        Pet a1 = new Dog();
        Pet a2 = new Cat();
        a1.eat();
        a2.eat();
    }
}
运行该程序,结果为:

狗吃狗粮
猫吃猫粮

如 Pet a1 = new Dog() 和 Pet a2 = new Cat() 这两条代码所示,当 Pet 类型的变量指向其子类 Dog 和 Cat 对象,多态就产生了。此时,a1 和 a2 都具有两种类型,即编译类型和运行类型。以 a1 为例,其编译类型为声明对象变量的类型 Pet,其运行类型是指真实类型 Dog。

变量 a1 引用的是 Dog 类对象,因此语句 a1.eat() 调用 Dog 类的 eat() 方法,同理,a2.eat() 调用的是 Cat 类的 eat() 方法。从运行结果可以看出,虽然都是通过 Pet 类型的变量调用 eat() 方法,但是变量引用的对象不同,执行结果也不同,这就是多态的体现。

由此可知多态的特点:将子类的对象赋值给父类变量,在运行时就会表现具体子类的特征。

针对上例,我们进行扩充,假如再定义宠物喂养者类,负责给宠物投喂食物,给狗投喂狗粮,给猫投喂猫粮,如果不使用多态,则具体程序实现如下例所示。

【实例】定义宠物喂养者类 Feeder,分别给狗和猫进行食物投喂。
class Feeder {
    public void feed(Dog d) {
        System.out.println("开始投喂:");
        d.eat();
    }

    public void feed(Cat c) {
        System.out.println("开始投喂:");
        c.eat();
    }
}

public class FeedWithoutPolymorphism {
    public static void main(String[] args) {
        Feeder f = new Feeder();
        // 投喂狗
        f.feed(new Dog());
        // 投喂猫
        f.feed(new Cat());
    }
}
程序执行结果为:

开始投喂:
狗吃狗粮
开始投喂:
猫吃猫粮

在此例中,宠物喂养者需要针对不同宠物定义不同的喂养方法。如果宠物类型比较多,那么就要定义大量的喂养方法,而且每个方法内容都相同,这明显违背了代码“write once,only once”的原则。

如果使用多态,就可以在 Feeder 中只定义一个 feed() 方法,统一喂养所有类型的宠物,这时只需要修改 feed() 方法,变为如下代码所示:
public void feed(Pet p) {
    System.out.println("开始投喂:");
    p.eat();
}
其他代码保持不变,再次运行程序,结果和前面的实例一致。当调用 feed() 传递参数时,形参为父类类型,可以接收任意的子类对象,这就体现了多态。在实际开发中,也可以让接口类型作为形参,实参是任何实现了该接口的实现类,同样也可以体现多态,这种方式更加常见。

由此可见,只使用封装和继承的 Java 程序,可以称之为基于对象编程,而只有把多态加进来才能称之为面向对象编程。

Java父类与子类间的类型转化

根据之前实现多态的例子可以看出,Java 语言允许某个类型的引用变量引用其子类的实例。这如同前面介绍的数据类型转换,引用数据类型也可以进行类型转换。

假设 B 类是 A 类子类或间接子类,当我们用子类 B 创建一个对象,并把这个对象的引用赋给 A 类的对象:
A a;
B b = new B();
a = b;
称这个 A 类对象 a 是子类对象 b 的上转型对象。

上转型对象可以操作子类继承或覆盖成员变量,也可以使用子类继承的或重写的方法。但上转型对象不能操作子类新增的成员变量,不能使用子类新增的方法。

可以将对象的上转型对象再强制转换到一个子类对象,该子类对象又具备了子类所有属性和功能。

同样地,也可以把一个父类对象赋值给一个子类变量,这叫作向下转型,但这时需要进行强制类型转换。
b= (B)a;
向下转型可以调用子类类型中所有的成员。不过需要注意的是,如果父类对象指向的是子类对象,那么向下转型的过程是安全的,也就是编译也运行都不会出错。但是如果父类对象是父类本身,那么向下转型的过程是不安全的,即编译不会报错,但是运行时会出现 Java 强制类型转换异常,所以一般不建议进行向下转型。

下面通过一个例子说明子类与父类之间类型转换的使用原则:

【实例】定义父类 Mammal 和子类 Monkey,在主方法中定义上转型对象,并测试上转型对象调用父子类中相关方法的权限。
class Mammal { // 哺乳动物类
    private int n = 50;
    void crySpeak(String s) {
        System.out.println(s);
    }
}

public class Monkey extends Mammal { // 猴子类
    void computer(int aa, int bb) {
        int cc = aa * bb;
        System.out.println(cc);
    }

    void crySpeak(String s) {
        System.out.println("***" + s + "***");
    }

    public static void main(String args[]) {
        // mammal 是 Monkey 类对象的上转型对象
        Mammal mammal = new Monkey();
        Mammal mammal1 = new Mammal();
        mammal.crySpeak("I love this game");
        mammal.computer(10, 10); // 编译报错,因为上转型对象不能操作子类新增成员
        Monkey monkey = mammal; // 编译报错,向下转型必须进行强制类型转换
        // 把上转型对象强制转化为子类的对象
        Monkey monkey = (Monkey)mammal;
        monkey.computer(10, 10);
        monkey = (Monkey)mammal1;
        monkey.computer(10, 10);
    }
}
上述代码中,如果注释掉编译报错的两条语句,再执行程序,结果为:
**I love this game**
100
Exception in thread "main" java.lang.ClassCastException: class ch4.Mammal cannot be cast to class ch4.Monkey
    at ch4.Monkey.main(Monkey.java:29)
mammal 是个上转型对象,实际引用的是子类 Monkey 对象,所以语句 mammal.crySpeak("I love this game"); 实际调用的是 Monkey 中的重写的 crySpeak() 方法。

因为 computer() 方法是子类新增的方法,无法通过上转型对象调用该方法,所以语句 mammal.computer(10,10); 编译报错。

Monkey monkey=(Monkey)mammal; 语句把上转型对象强制转换为子类对象后,可以操作子类新增方法,原因是 mammal 实质上指向的是子类对象,而 monkey=(Monkey)mammal1; 语句也是强制把父类对象赋给子类变量,但是 mammal1 实质上指向的是父类对象,所以通过 mammal 调用子类新增方法,运行时报错。

总之,父类对象和子类对象的转化需要注意如下原则。

相关文章