前些天,我们简单地讲解了继承的概念,也理解了继承是什么。本期文章,我们将在继承的基础之上,引申出一个思想:多态。那么,多态是什么?本期文章概念居多,读者不妨多看几遍,或许会有不同的理解。我们往下看。
前期文章:
前言- IDEA如何配置?让你敲代码更轻松!
初识Java语言(一)- 基本数据类型及运算符
初识Java语言(二)- 方法以及递归
初识Java语言(三)- 数组
初识Java语言(四)-类和对象
初识Java语言(五)- 包和继承
一、多态
在上一篇文章中,我们所写的例子:猫和狗都是继承在Animal上,我们在重写方法后,是通过Cat实例的对象,进行调用重写方法的!其实,还有一种方式,是下面这样写的:
Cat cat = new Cat();
Animal animal = cat;
正如上面的代码所示,我们将子类对象赋值给父类类型的引用。这样的情况,我们就称为多态。
多态:一个对象变量(例如,变量animal)可以指示多种实际类型的现象称为多态。
向上转型
正如上面的代码所写,将子类对象赋值给父类类型的引用,这种情况叫做向上转型,如下图:
发生向上转型的三种情况:
-
直接赋值,正如上面的代码这种情况 -
方法传参 public static void draw(Animal animal) {
System.out.println(animal.name + "画画");
}
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "TOM";
draw(cat);
}
-
方法返回 public static void main(String[] args) {
Animal animal = createCat();
}
public static Animal createCat() {
return new Cat();
}
动态绑定
在上一篇文章中,我们在重写方法那个知识点,是这样写的:
是通过子类对象,调用了自己的eat方法,如果子类对象需要调用父类的方法,就需要用到super关键字。
那么今天,我们结合向上转型的知识,来看看,如何进行调用方法:
class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(this.name + " 吃东西(Animal)");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
public void eat() {
System.out.println(this.name + " 吃东西(cat)");
}
public void drink() {
System.out.println(this.name + " 喝水(cat)");
}
}
public class Demo {
public static void main(String[] args) {
Cat cat1 = new Cat("TOM");
Animal cat2 = new Cat("喵喵");
cat1.eat();
cat1.drink();
cat2.eat();
cat2.drink();
}
}
通过上面的代码,我们可以发现几个问题:
- cat类对象,没向上转型,只能调用cat类的方法和字段;若想调用父类的,则需要super关键字
- cat类,向上转型后,就是Animal类型。此时若还想调用子类中的方法,只能让子类中的这个方法变为重写方法,才能进行调用。
我们将上面这种情况:子类对象向上转型后,调用重写方法;这种情况,我们叫做动态绑定。也叫运行时绑定。
因此,在java中,调用某个类的方法,究竟执行了哪段代码(父类的,还是子类的代码),要看这个引用究竟是指向父类对象,还是子类对象,这个过程是程序运行时决定的。重写方法后,在动态绑定时,在编译阶段,是编译的父类的方法,而在运行时,是运行的子类的方法。这也是运行时绑定名字的由来吧。
发生动态绑定的两个必要条件:
- 向上转型;(子类对象 被 父类所引用)
- 通过父类的引用,调用子类中所重写的方法。
向下转型
自然,理解了向上转型,那么向下转型,就简单多了。向下转型,不是那么推荐使用,因为很容易出错。但是,对于我们初学,还是需要了解相关的概念的。
向下转型,自然而然,就是由父类对象,进行强制类型转换后,由子类类型所引用。我们先来看下面这一段代码:
Animal animal = new Animal("黑黑");
Cat cat = (Cat)animal;
这段代码会编译出错吗?
答案肯定是会编译出错的(ClassCastException, 类型转换异常)。我们都能够理解,向下转型,是由父类对象 转换为 子类类型所引用,但是其实还有一个很重要的点,那就是这个父类对象,本质上,就是一个子类对象向上转型后得到的。如下代码:
Animal animal = new Cat("喵喵");
Cat cat = (Cat)animal;
所以为了避免这种向下转型时,容易出错,所以还有一个关键字instanceof ,专门用于检测,当前这个对象向下转型,是否会抛出异常,如果会抛出异常的话,instanceof 返回的就是false,反之就是true。看如下代码:
Animal animal = new Cat("喵喵");
Cat cat = null;
if (animal instanceof Cat) {
cat = (Cat)animal;
}
所以在强制类型转换时:
- 只能在继承层次内进行强制类型转换。(也就说,被强制类型转换的对象,并不在当前的继承关系中,不能转换)
- 在将父类强制转换为子类时, 应该使用
instanceof 进行检查。
深入理解多态
有了上面的向上和向下转型的基础,我们来以一段代码,理解多态这种思想,究竟有何好处?
通过这样的方式,我们很轻松的就能都调用每个图形所对应的方法。如果我们不使用多态,我们就需要对传递进入draw方法的参数类型进行判断,判断是什么图形后,在才通过这个图形类进行方法的调用。大大的减少了代码量。
在上面代码中,Demo2中的代码,是类的调用者实现的,而像上面的Shape这些类,是由另外一个人实现的。调用者不需要知道,Shape类是怎么实现的,只需要知道怎么进行调用即可。此时Shape类进行调用draw方法,会根据传递过来的参数类型不同,从而调用不同的重写方法, 这就是多态。
那么使用多态的好处是什么?有以下几点:
-
类调用者对类的使用成本降低。 【封装让类的调用者不需要知道类的具体实现细节。多态能让类的调用者连这个类的类型是什么都不必知道,只需要这个类有这么一个方法即可】 -
能够降低代码的“圈复杂度”, 也就是说,能减少大量的if-else语句 【圈复杂度:是一种描述一段代码的复杂程度的方式,一段代码如果平铺直叙,那么就很容易理解。而如果有很多的循环语句、选择语句等,就认为理解起来更复杂。 可以通过计算一段代码的循环、选择语句的个数,这个个数就称为“圈复杂度”。】 -
可扩展能力更强。(比如:新添加一个图形,只需要这个类继承Shape,并重写方法即可)
面试题总结:重写(override)与重载(overload)的区别?
重载(overload)
? 方法重载是让类以统一的方式处理不同类型数据的一种手段。多个同名函数同时存在,具有不同的参数个数/类型。
? 重载Overloading是一个类中多态性的一种表现。 Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。
? 调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这就是多态性。
? 重载的时候,方法名要一样,但是参数类型和个数不一样,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准。
重载规则:
- 必须具有不同的参数列表;
- 可以有不同的返回类型,只要参数列表不同就可以了;
- 可以有不同的访问修饰符;
- 可以抛出不同的异常;
重写(override)
? 父类方法被默认修饰时,只能在同一包中,被其子类重写,如果不在同一包则不能重写。
? 父类的方法被protected时,不仅在同一包中,被其子类重写,还可以不同包的子类重写。
重写规则:
- 方法名、参数列表必须与父类方法的方法名和参数列表一致
- 方法的返回值类型相同
- 访问修饰限定符必须要大于或者等于父类方法的访问修饰限定符。并且父类方法不能是private修饰的,也不能是被final修饰的。
- 父类和子类所重写的方法,都不能被static修饰。
- 子类重写的方法,抛出的异常等级不能大于父类方法所抛出的异常
二、抽象类
从普通类到抽象类
在上面的Shape类中,我们可以发现一个问题:Shaper的draw方法里面都没有写,只是在这里用来被重写的。有人可能就会问,怎么这么麻烦,有没有简单一点的写法。
那肯定是有的,抽象方法就能解决这个问题。
abstract class Shape {
public abstract void draw();
}
注意:只要在一个类中,出现了抽象方法,那么这个类必须也是抽象的。也就是被abstract修饰。
当然在抽象类里,也是可以不全是抽象方法。如下
abstract class Shape {
public abstract void draw();
public void display() {
System.out.println("显示方法");
}
}
抽象类的作用
抽象类存在的最大意义就是为了被继承。因为抽象类是不能自己进行实例化的。要想使用这个类,只能通过继承的方式,通过子类来进行重写这个方法里面所有的抽象方法。
抽象类的8个注意事项
-
包含抽象方法的类,称为抽象类。方法和类都是由abstract修饰的 -
抽象类中可以定义成员变量和成员方法(抽象与非抽象) -
抽象类不能被实例化 -
抽象类存在的意义就是为了被继承 -
一个普通类继承了抽象类,那么普通类要重写抽象类中所有的抽象方法 -
抽象方法不能是被final修饰的, 也不能被private修饰。final和abstract不能共存。 -
一个抽象类A继承了另外一个抽象类B,此时有一个普通类C继承了抽象类A,那么此时普通类C需要重写A和B两个类的所有抽象方法。 -
如果一个普通类继承了抽象类,普通类又不重写父类的抽象方法,此时则可以将这个子类也变为抽象类。就不用重写父类的抽象方法。
三、接口
从类到接口
我们都知道在Java中,一个类只能继承一个父类,并不像C++那样能够实现多继承。有人就想啊,如果我继承了一个抽象类,那么就不能再继承其他类了,那该怎么办。
所以在Java中引入了接口的概念,接口是抽象类的升级版呢。
在上文中的我们将Shape写成一个类,可以实现多态,那么我们实现成接口,该怎么实现呢?如下:
interface IShape {
public abstract void draw();
}
class Rect implements IShape {
@Override
public void draw() {
System.out.println("画一个矩形");
}
}
这样,我们就实现了一个接口;
-
接口是由interface 修饰的,而不是class -
接口不能单独被实例化 -
接口中的方法默认是被public abstract修饰的 -
接口中的变量默认是被public static final 修饰的 -
让类与接口连接起来,术语叫:实现接口。使用implements 关键字,写在类名的末尾,后面写接口名 -
若这个类还继承了父类,那么接口应写在父类名的后面,如下: class Student extends Person implements Ishape {
}
尤其切记:接口中的方法,默认是被public abstract修饰的,有如下错误的代码
interface IShape {
abstract void draw();
}
class Cycle implements IShape {
void draw() {
System.out.println("画一个圆");
}
}
提示:
1、 我们创建接口时,接口的命名一般以大写字母I开头
2、 接口的命名一般使用“形容词”词性的单词
3、 阿里编码规范,接口中的方法和成员变量不要加任何的修饰符,保持代码的简洁性
实现多个接口
在Java中无法实现多继承,所以我们只能通过实现多个接口,来达到类似于多继承的情况。那么该如何实现多个接口呢?我们以一个例如来说:
interface ISwimming {
void swimming();
}
interface IJump {
void jump();
}
class Frog implements IJump, ISwimming {
public String name;
public void swimming() {
System.out.println("游泳");
}
public void jump() {
System.out.println("跳跃");
}
}
这样,我们就能让Frog类实现两个接口,解锁两个技能(跳跃和游泳)。这就是实现多个接口。
接口与接口之间的扩展
当然,接口与接口之间,也还是可以用extends来实现扩展。
interface ISwimming {
void swimming();
}
interface IJump {
void jump();
}
interface IAmphibious extends IJump, ISwimming {
}
如上代码,我们就将上面两个接口,结合在了一个接口里,新的接口里,还可以重新添加其他的方法。这样的话,就将很多功能的接口,结合了在一起,此时青蛙类再去实现IAmphibious接口,就能拥有接口里的所有方法。
接口的8个注意事项
- 接口里面的方法,只能是抽象方法,不能是普通方法。且这些方法,默认是被public abstract修饰的
- 在JDK1.8之后,接口里可以实现普通方法,但是需要用default修饰,即默认方法
- 接口里的成员变量,默认是被public abstract final修饰的
- 接口同样不能单独的实例化
- 类和接口之间用implements来实现,意为:实现接口
- 接口与接口之间,可以用extends来继承,意为:扩展接口
- 一个类,可以实现N个接口,主要目的就是为了达到“多继承”的情况
- 接口也是可以发生向转型和多态的
面试题总结:抽象类与接口的区别?
-
子类使用的关键字不一样
- 抽象类,是用来被继承的。需使用extends关键字。继承了抽象类,既可以重写全部的抽象方法,也可以不重写(子类变为抽象类)。
- 接口,是用来被实现的。需使用implements关键字。实现了这个接口,就必须重写接口里的所有方法,不能不写。
-
是否有构成方法?
- 抽象类,既然是一个类,肯定是有相应的构造方法的
- 接口,不是一个类,是所有抽象方法的集合,当然也就没有构造方法的概念
-
可选择的访问修饰限定符
- 抽象类的方法,排除private,其他的三个都是可以使用的
- 接口的方法,它别无选择,只能是public
-
添加新的方法对子类的影响
- 抽象类中,是允许存在非抽象方法的,如若添加非抽象方法,无需对子类进行新抽象方法的重写
- 接口中,在JDK1.8之后 ,也是允许有非抽象方法,只需被default修饰,这样的话,也无需对子类进行新方法的重写。但是添加新的抽象方法,那就必须得重写
-
子类所能继承的数量
- 抽象类,因为是类,在子类中,只能继承一个类
- 接口,就是因为接口的出现,才是Java有了类似于多继承的情况
好啦,本期更新就到此结束啦!!!最近这几篇文章,概念杂多,大家好好捋一捋思路,整理一下即可!!!我们下期见!!!
|