Java 面对对象之多态
Posted 木头的心
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 面对对象之多态相关的知识,希望对你有一定的参考价值。
阅读目录
- 多态(polymorphism)
- 向上类型转换(Upcast)和向下类型转换(Downcast)
- instanceof 关键字
面向对象第三大特征:多态
多态(polymorphism)
多态是面向对象的重要特性, 简单点说:“一个接口,多种实现”,就是同一种事物表现出的多种形态。
编程其实就是一个将具体世界进行抽象化的过程,多态就是抽象化的一种体现,把一系列具体事物的共同点抽象出来, 再通过这个抽象的事物, 与不同的具体事物进行对话。
一个程序跑起来需要经过编译→运行。而多态是一种运行期的行为,不是编译期的行为。
- 多态性是 OOP 中的一个重要特性,主要是用来实现动态联编的,换句话说,就是程序的最终状态只有在执行过程中才被决定而非在编译期间就决定了。这对于大型系统来说能提高系统的灵活性和扩展性。
- Java 中如何实现多态?使用多态的好处?
- 引用变量的两种类型:
- 编译时类型(模糊一点,一般是一个父类)
- 由声明时的类型决定。
- 运行时类型(运行时,具体是哪个子类就是哪个子类)
- 由实际对应的对象类型决定。
- 引用变量的两种类型:
多态的存在要有 3 个必要条件
- (多态是指方法的多态,属性没有多态)
- 要有继承;
- 要有方法重写;
- 父类引用指向子类对象。
【多态 示例 1】
package com.gta.testoop.polymorphism; public class Animal { public void voice(){ System.out.println("普通动物叫声!"); } } class Cat extends Animal{ //方法重写 public void voice() { System.out.println("喵喵喵~"); } } class Dog extends Animal{ //方法重写 public void voice() { System.out.println("汪汪汪~"); } } class Pig extends Animal{ //方法重写 public void voice() { System.out.println("哼哼哼~"); } }
测试类:
(如果没有采用多态,如下所示)
public class Test { //如果没有多态,采用方法重载 public static void testAnimalVoice(Cat c){ c.voice(); } public static void testAnimalVoice(Dog d){ d.voice(); } public static void testAnimalVoice(Pig p){ p.voice(); } public static void main(String[] args) { // TODO Auto-generated method stub Cat c=new Cat(); testAnimalVoice(c); } }
采用多态,如下所示:
public class Test { //采用多态 public static void testAnimalVoice(Animal a){//Animal是a的编译时类型 a.voice(); } public static void main(String[] args) { //①向上类型转换 Cat c=new Cat();//创建一个Cat对象c,Cat是Animal的子类; // Animal a=c; testAnimalVoice(c);//Cat是运行时类型,实际是执行Animal a=c;再执行testAnimalVoice方法。 //上面①等价于②:向下类型转换 Animal c1=new Cat();//父类引用指向子类对象 testAnimalVoice(c1); } }
如果子类有新方法:
则
此处报错,Animal 没有编译。Animal 类里没有 catchMouse 方法,c1 引用是 Animal 类,所以找不到该方法,编译通不过。
可以采用强制转型,将 c1 再转回 Cat 型。
多态:父类型的引用可以指向子类型的对象。
比如 Parent p = new Child();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;
如果有,再去调用子类的该同名方法。
(注意此处,静态 static 方法属于特殊情况,静态方法只能继承,不能重写 Override,如果子类中定义了同名同形式的静态方法,它对父类方法只起到隐藏的作用。调用的时候用谁的引用,则调用谁的版本。)
向上类型转换(Upcast)和向下类型转换(Downcast)
对象转型分为两种:一种叫向上转型 (父类对象的引用或者叫基类对象的引用指向子类对象,这就是向上转型),另一种叫向下转型。转型的意思是:如把 float 类型转成 int 类型,把 double 类型转成 float 类型,把 long 类型转成 int 类型,这些都叫转型。把一种形式转成另外一种形式就叫转型。除了基础数据类型的转型之外(基础数据类型的转型:大的可以转成小的,小的也可以转成大的。),对象领域里面也有对象之间的转型。
(1)向上类型转换(Upcast):将子类型转换为父类型。
对于向上的类型转换,不需要显示指定,即不需要加上前面的小括号和父类类型名。
(2)向下类型转换(Downcast):将父类型转换为子类型。
对于向下的类型转换,必须要显式指定,即必须要使用强制类型转换。
并且父类型的引用必须指向子类的对象,即指向谁才能转换成谁。
不然也会编译出错:
因为父类引用指向的是 Cat 类的对象,而要强制转换成 Dog 类,这是不可能的。
1.1 对象转型实例一
package javastudy.summary; /** * 父类Animal */ class Animal { public String name; public Animal(String name) { this.name = name; } } /** * 子类Cat继承Animal */ class Cat extends Animal { /** * Cat添加自己独有的属性 */ public String eyeColor; public Cat(String n, String c) { super(n);//调用父类Animal的构造方法 this.eyeColor = c; } } /** * 子类Dog继承Animal */ class Dog extends Animal { /** * Dog类添加自己特有的属性 */ public String furColor; public Dog(String n, String c) { super(n);//调用父类Animal的构造方法 this.furColor = c; } } /** * 下面是这三个类的测试程序 */ public class TestClassCast { /** * @param args */ public static void main(String[] args) { Animal a = new Animal("name"); Cat c = new Cat("catname","blue"); Dog d = new Dog("dogname", "black"); /** * a instanceof Animal这句话的意思是a是一只动物吗? * a是Animal这个类里面的是一个实例对象,所以a当然是一只动物,其结果为true。 */ System.out.println(String.format("a instanceof Animal的结果是%s",a instanceof Animal));//true /** * c是Cat类的实例对象的引用,即c代表的就是这个实例对象, * 所以“c是一只动物”打印出来的结果也是true。 * d也一样,所以“d是一只动物”打印出来的结果也是true。 */ System.out.println(String.format("c instanceof Animal的结果是%s",c instanceof Animal));//true System.out.println(String.format("d instanceof Animal的结果是%s",d instanceof Animal));//true /** * 这里判断说“动物是一只猫”,不符合逻辑,所以打印出来的结果是false。 */ System.out.println(String.format("a instanceof Cat的结果是%s",a instanceof Cat)); /** * 这句话比较有意思了,a本身是Animal类的实例对象的引用, * 但现在这个引用不指向Animal类的实例对象了,而是指向了Dog这个类的一个实例对象了, * 这里也就是父类对象的引用指向了子类的一个实例对象。 */ a = new Dog("bigyellow", "yellow"); System.out.println(a.name);//bigyellow /** * 这里的furColor属性是子类在继承父类的基础上新增加的一个属性,是父类没有的。 * 因此这里使用父类的引用对象a去访问子类对象里面新增加的成员变量是不允许的, * 因为在编译器眼里,你a就是Animal类对象的一个引用对象,你只能去访问Animal类对象里面所具有的name属性, * 除了Animal类里面的属性可以访问以外,其它类里面的成员变量a都没办法访问。 * 这里furColor属性是Dog类里面的属性,因此你一个Animal类的引用是无法去访问Dog类里面的成员变量的, * 尽管你a指向的是子类Dog的一个实例对象,但因为子类Dog从父类Animal继承下来, * 所以new出一个子类对象的时候,这个子类对象里面会包含有一个父类对象, * 因此这个a指向的正是这个子类对象里面的父类对象,因此尽管a是指向Dog类对象的一个引用, * 但是在编译器眼里你a就是只是一个Animal类的引用对象,你a就是只能访问Animal类里面所具有的成员变量, * 别的你都访问不了。 * 因此一个父类(基类)对象的引用是不可以访问其子类对象新增加的成员(属性和方法)的。 */ //System.out.println(a.furColor); System.out.println(String.format("a指向了Dog,a instanceof Animal的结果是%s",a instanceof Animal));//true /** * 这里判断说“a是一只Dog”是true。 * 因为instanceof探索的是实际当中你整个对象到底是什么东西, * 并不是根据你的引用把对象看出什么样来判断的。 */ System.out.println(String.format("a instanceof Dog的结果是%s",a instanceof Dog));//true /** * 这里使用强制转换,把指向Animal类的引用对象a转型成指向Dog类对象的引用, * 这样转型后的引用对象d1就可以直接访问Dog类对象里面的新增的成员了。 */ Dog d1 = (Dog)a; System.out.println(d1.furColor);//yellow } }
运行结果:
对象转型内存分析
在内存中可以看到,指向 Dog 类实例对象的引用对象 a 是一个 Animal 类型的引用类型,这就比较有意思了,Animal 类型指向了 Dog 这个对象,那么,在程序的眼睛里会把这只 Dog 当成一只普通的 Animal,既然是把 Dog 当成一只普通的 Animal,那么 Dog 类里面声明的成员变量 furColor 就不能访问了,因为 Animal 类里面没有这个成员变量。因此,从严格意义上来讲,这个 a 眼里只看到了这个子类对象里面的父类对象 Animal, 因此能访问得到的也只是这个 Animal 对象里面的 name 属性,而这个 Animal 对象外面的 furColor 属性是访问不到的,虽然 Dog 对象确实有这个属性存在,但 a 就是看不到,a 门缝里看 Dog——把 Dog 看扁了,不知道 Dog 还有 furColor 这个属性存在,因此 a 访问不了 furColor 属性,因此从严格意义上来讲,a 指向的只是这个 Dog 对象里面的 Animal 对象,也就是黄色箭头指向的那部分,a 就只看到了 Dog 里面这部分,而 Dog 外面的部分都看不到了。这就是父类引用指向子类对象,父类引用指向子类对象的时候,它看到的只是作为父类的那部分所拥有的属性和方法,至于作为子类的那部分它没有看到。
如果真的想访问 Dog 对象的 furColor 属性,那就采用对象转型的办法,把父类对象的引用转型成子类对象的引用。Dog d1 = (Dog)a; 这里采用的就是对象转型的办法,把 a 强制转换成一只 Dog 对象的引用,然后将这个引用赋值给 Dog 型的引用变量 d1,这样 d1 和 a 都是指向堆内存里面的 Dog 对象了,而且 d1 指向的就是这只 Dog 所有的部分了,通过这个 d1 就可以访问 Dog 对象里面所有的成员了。
1.2 对象转型实例二
public class TestClassCast { public void f(Animal a) { System.out.println(a.name); if (a instanceof Cat) { Cat cat = (Cat)a; System.out.println(cat.eyeColor+" eye"); }else if (a instanceof Dog) { Dog dog = (Dog)a; System.out.println(dog.furColor+" fur"); } } /** * @param args */ public static void main(String[] args) { Animal a = new Animal("name"); Cat c = new Cat("catname","blue"); Dog d = new Dog("dogname", "black"); TestClassCast testClassCast = new TestClassCast(); testClassCast.f(a); testClassCast.f(c); testClassCast.f(d); } }
这里的这些代码是对前面声明的三个类 Animal,Dog,Cat 测试的延续,这里我们在 TestClassCast 这里类里面测试这个三个类,这里我们在 TestClassCast 类里面 new 了一个 testClassCast 对象,为的是调用 TestClassCast 类里面声明的 f(Animal a) 这个方法,这个 f() 方法里面的参数类型是 Animal 类型,如果是 Animal 类型的参数,那么我们可以把这个 Animal 类型的子类对象作为参数传进去,这是可以的。如把一只 Dog 或者是一只 Cat 丢进 f() 方法里面这都是可以的,因为 Dog 和 Cat 也是 Animal。因此当程序执行到 testClassCast.f(a);,testClassCast.f(c);,testClassCast.f(d); 的时候,因为 f() 方法里面的参数是 Animal 类型的,所以我们可以把一个 Animal 对象传进去,除此之外,我们还可以直接把从 Animal 类继承下来的 Dog 类和 Cat 类里面的 Dog 对象和 Cat 对象作为实参传递过去,即是把 Animal 类型的子类对象作为参数传进去。这里就体现出了继承和父类对象的引用可以指向子类对象的好处了,如果说没有继承关系的存在,如果说父类的引用不可以指向子类对象,那么我们就得要在 Test 类里面定义三个 f() 方法了,即要定义这样的 f() 方法:
f(Animal a)、f(Dog d)、f(Cat c) 分别用来处理 Animal、Dog 和 Cat,使用三个方法来处理,将来程序的扩展起来就不是很容易了,因为面向对象可以帮助我们这些年来编程苦苦追求的一个境界是可扩展性比较好。可扩展性比较好的一个典型例子就是说当你建好一个建筑之后或者是你写好这个程序之后,把这个主建筑给建好了,将来你要加一些其他的功能的时候,尽量不要去修改主结构,这叫可扩展性好,你盖了一座大楼,你现在要在大楼的旁边添加一个厨房,那你在它旁边一盖就行了,如果有人告诉你,我添加一个厨房我需要把你整个大楼的主要柱子都给拆了然后再盖一遍,这你干吗,肯定不干。如果结构设计成这样,那就是设计得不好,可扩展性不好。所以这里如果要把 f() 方法写成三个重载的 f() 方法,那么将来我输出一只鸟的时候又得要添加一个 f(Bird b) 方法来处理鸟。这样扩展起来就太麻烦了,因为每处理一只动物都要添加一个新的方法,但是如果存在继承关系,如果父类对象的引用可以指向子类对象,那扩展起来就简单了,你可以把处理动物的方法写在一个方法 f(Animal a) 里面就够了,因为所有动物的种类都是从 Animal 类继承下来,因此给 f() 方法传递 Animal 类型的参数的时候可以直接把这个 Animal 类的子类对象传进去,这样不管是要增加什么动物的输出,我都可以调用 f(Animal a) 方法去处理,这种扩展性比每次都要增加一个新的处理方法的扩展性要好得多,这就是继承的一个好处,这就是对象转型对于可扩展性来的好处:“对象的引用可以指向子类对象”,是因为面向对象的编程里面存在这样的继承关系,使得程序的可扩展性比较好。
对象转型可以使父类对象的引用可以指向子类对象,给程序带来了比较好的可扩展性:我们可以在一个方法的参数里面定义父类的引用,然后实际当中传的时候传的是子类的对象,然后我们再在方法里面判断这个传过来的子类对象到底属于哪个子类,然后再去执行这个子类里面的方法或者调用这个子类里面的成员变量,因此程序的可扩展性比单独定义好多个方法要好一些。不过这个可扩展性还没有达到最好,使用多态就可以让程序的扩展性达到极致。
instanceof 关键字
instanceof 是 Java 的一个二元操作符,和 ==,>,< 是同一类东东。由于它是由字母组成的,所以也是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 类型的数据。
相关链接:
java 基础学习总结——多态 (动态绑定)
以上是关于Java 面对对象之多态的主要内容,如果未能解决你的问题,请参考以下文章