Java基础2-Java面向对象三大特性(基础篇)(一):https://developer.aliyun.com/article/1535605
三、多态
1、多态的概念
面向对象的多态性,即“一个接口,多个方法”。多态性体现在父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式。多态性允许一个接口被多个同类使用,弥补了单继承的不足。多态概念可以用树形关系来表示,如图 4 所示。
图4 多态示例图
从图 4 中可以看出,老师类中的许多属性和方法可以被语文老师类和数学老师类同时使用,这样也不易出错。
2、多态的好处
可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
子代父类实例化,然后就相当于一个父亲有很多儿子,送快递的给这个父亲的儿子送东西,他只需要送到父亲的家就行了,至于具体是那个儿子的,父亲还会分不清自己的儿子么,所以你就不用操心了。
使用多态是一种好习惯 多态方式声明是一种好的习惯。当我们创建的类,使用时,只用到它的超类或接口定义的方法时,我们可以将其索引声明为它的超类或接口类型。
它的好处是,如果某天我们对这个接口方法的实现方式变了,对这个接口又有一个新的实现类,我们的程序也需要使用最新的实现方式,此时只要将对象实现修改一下,索引无需变化。
比如Map< String,String> map = new HashMap < String,String>();
想换成HashTable实现,可以Map< String,String> map = new HashTable < String,String>();
比如写一个方法,参数要求传递List类型,你就可以用List list = new ArrayList()中的list传递,但是你写成ArrayList list = new ArrayList()是传递不进去的。尽管方法处理时都一样。另外,方法还可以根据你传递的不同list(ArrayList或者LinkList)进行不同处理。
3、Java中的多态
java里的多态主要表现在两个方面:
A、引用多态
父类的引用可以指向本类的对象;
父类的引用可以指向子类的对象;
这两句话是什么意思呢,让我们用代码来体验一下,首先我们创建一个父类Animal和一个子类Dog,在主函数里如下所示:
注意:我们不能使用一个子类的引用来指向父类的对象,如:
这里我们必须深刻理解引用多态的意义,才能更好记忆这种多态的特性。为什么子类的引用不能用来指向父类的对象呢?我在这里通俗给大家讲解一下:就以上面的例子来说,我们能说“狗是一种动物”,但是不能说“动物是一种狗”,狗和动物是父类和子类的继承关系,它们的从属是不能颠倒的。当父类的引用指向子类的对象时,该对象将只是看成一种特殊的父类(里面有重写的方法和属性),反之,一个子类的引用来指向父类的对象是不可行的!!
B、方法多态
根据上述创建的两个对象:本类对象和子类对象,同样都是父类的引用,当我们指向不同的对象时,它们调用的方法也是多态的。
创建本类对象时,调用的方法为本类方法;
创建子类对象时,调用的方法为子类重写的方法或者继承的方法;
使用多态的时候要注意:如果我们在子类中编写一个独有的方法(没有继承父类的方法),此时就不能通过父类的引用创建的子类对象来调用该方法!!!
注意: 继承是多态的基础。
C、引用类型转换
了解了多态的含义后,我们在日常使用多态的特性时经常需要进行引用类型转换。
引用类型转换:
1.向上类型转换(隐式/自动类型转换),是小类型转换到大类型
就以上述的父类Animal和一个子类Dog来说明,当父类的引用可以指向子类的对象时,就是向上类型转换。如:
2. 向下类型转换(强制类型转换),是大类型转换到小类型(有风险,可能出现数据溢出)。
将上述代码再加上一行,我们再次将父类转换为子类引用,那么会出现错误,编译器不允许我们直接这么做**,虽然我们知道这个父类引用指向的就是子类对象,但是编译器认为这种转换是存在风险的。**如:
那么我们该怎么解决这个问题呢,我们可以在animal前加上(Dog)来强制类型转换。如:
但是如果父类引用没有指向该子类的对象,则不能向下类型转换,虽然编译器不会报错,但是运行的时候程序会出错,如:
其实这就是上面所说的子类的引用指向父类的对象,而强制转换类型也不能转换!!
还有一种情况是父类的引用指向其他子类的对象,则不能通过强制转为该子类的对象。如:
这是因为我们在编译的时候进行了强制类型转换,编译时的类型是我们强制转换的类型,所以编译器不会报错,而当我们运行的时候,程序给animal开辟的是Dog类型的内存空间,这与Cat类型内存空间不匹配,所以无法正常转换。这两种情况出错的本质是一样的,所以我们在使用强制类型转换的时候要特别注意这两种错误!!下面有个更安全的方式来实现向下类型转换。。。。
3. instanceof运算符,来解决引用对象的类型,避免类型转换的安全性问题。
instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。
我们来使用instanceof运算符来规避上面的错误,代码修改如下:
利用if语句和instanceof运算符来判断两个对象的类型是否一致。
补充说明:在比较一个对象是否和另一个对象属于同一个类实例的时候,我们通常可以采用instanceof和getClass两种方法通过两者是否相等来判断,但是两者在判断上面是有差别的。Instanceof进行类型检查规则是:你属于该类吗?或者你属于该类的派生类吗?而通过getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断,不会存在继承方面的考虑;
总结:在写程序的时候,如果要进行类型转换,我们最好使用instanceof运算符来判断它左边的对象是否是它右边的类的实例,再进行强制转换。
D、重写和重载
多态一般可以分为两种,一个是重写override,一个是重载overload。
重写是由于继承关系中的子类有一个和父类同名同参数的方法,会覆盖掉父类的方法。重载是因为一个同名方法可以传入多个参数组合。 注意,同名方法如果参数相同,即使返回值不同也是不能同时存在的,编译会出错。 从jvm实现的角度来看,重写又叫运行时多态,编译时看不出子类调用的是哪个方法,但是运行时操作数栈会先根据子类的引用去子类的类信息中查找方法,找不到的话再到父类的类信息中查找方法。 而重载则是编译时多态,因为编译期就可以确定传入的参数组合,决定调用的具体方法是哪一个了。 复制代码
- 向上转型和向下转型
public static void main(String[] args) { Son son = new Son(); //首先先明确一点,转型指的是左侧引用的改变。 //father引用类型是Father,指向Son实例,就是向上转型,既可以使用子类的方法,也可以使用父类的方法。 //向上转型,此时运行father的方法 Father father = son; father.smoke(); //不能使用子类独有的方法。 // father.play();编译会报错 father.drive(); //Son类型的引用指向Father的实例,所以是向下转型,不能使用子类非重写的方法,可以使用父类的方法。 //向下转型,此时运行了son的方法 Son son1 = (Son) father; //转型后就是一个正常的Son实例 son1.play(); son1.drive(); son1.smoke(); //因为向下转型之前必须先经历向上转型。 //在向下转型过程中,分为两种情况: //情况一:如果父类引用的对象如果引用的是指向的子类对象, //那么在向下转型的过程中是安全的。也就是编译是不会出错误的。 //因为运行期Son实例确实有这些方法 Father f1 = new Son(); Son s1 = (Son) f1; s1.smoke(); s1.drive(); s1.play(); //情况二:如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错, //但是运行时会出现java.lang.ClassCastException错误。它可以使用instanceof来避免出错此类错误。 //因为运行期Father实例并没有这些方法。 Father f2 = new Father(); Son s2 = (Son) f2; s2.drive(); s2.smoke(); s2.play(); //向下转型和向上转型的应用,有些人觉得这个操作没意义,何必先向上转型再向下转型呢,不是多此一举么。其实可以用于方法参数中的类型聚合,然后具体操作再进行分解。 //比如add方法用List引用类型作为参数传入,传入具体类时经历了向下转型 add(new LinkedList()); add(new ArrayList()); //总结 //向上转型和向下转型都是针对引用的转型,是编译期进行的转型,根据引用类型来判断使用哪个方法 //并且在传入方法时会自动进行转型(有需要的话)。运行期将引用指向实例,如果是不安全的转型则会报错。 //若安全则继续执行方法。 } public static void add(List list) { System.out.println(list); //在操作具体集合时又经历了向上转型 // ArrayList arr = (ArrayList) list; // LinkedList link = (LinkedList) list; } 复制代码
总结: 向上转型和向下转型都是针对引用的转型,是编译期进行的转型,根据引用类型来判断使用哪个方法。并且在传入方法时会自动进行转型(有需要的话)。运行期将引用指向实例,如果是不安全的转型则会报错,若安全则继续执行方法。
- 编译期的静态分派
其实就是根据引用类型来调用对应方法。
public static void main(String[] args) { Father father = new Son(); 静态分派 a= new 静态分派(); //编译期确定引用类型为Father。 //所以调用的是第一个方法。 a.play(father); //向下转型后,引用类型为Son,此时调用第二个方法。 //所以,编译期只确定了引用,运行期再进行实例化。 a.play((Son)father); //当没有Son引用类型的方法时,会自动向上转型调用第一个方法。 a.smoke(father); // 复制代码
} public void smoke(Father father) { System.out.println("father smoke"); } public void play (Father father) { System.out.println("father"); //father.drive(); } public void play (Son son) { System.out.println("son"); //son.drive(); } 复制代码
- 方法重载优先级匹配
public static void main(String[] args) { 方法重载优先级匹配 a = new 方法重载优先级匹配(); //普通的重载一般就是同名方法不同参数。 //这里我们来讨论当同名方法只有一个参数时的情况。 //此时会调用char参数的方法。 //当没有char参数的方法。会调用int类型的方法,如果没有int就调用long //即存在一个调用顺序char -> int -> long ->double -> ..。 //当没有基本类型对应的方法时,先自动装箱,调用包装类方法。 //如果没有包装类方法,则调用包装类实现的接口的方法。 //最后再调用持有多个参数的char...方法。 a.eat('a'); a.eat('a','c','b'); } public void eat(short i) { System.out.println("short"); } public void eat(int i) { System.out.println("int"); } public void eat(double i) { System.out.println("double"); } public void eat(long i) { System.out.println("long"); } public void eat(Character c) { System.out.println("Character"); } public void eat(Comparable c) { System.out.println("Comparable"); } public void eat(char ... c) { System.out.println(Arrays.toString(c)); System.out.println("..."); } // public void eat(char i) { // System.out.println("char"); // } 复制代码