初识 Java 语言(六)- 多态、抽象类以及接口
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(); //error
}
}
通过上面的代码,我们可以发现几个问题:
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) { //animal 这个对象,是由 Cat 类向上转型而来,就进入 if 语句
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 修饰。
当然在抽象类里,也是可以不全是抽象方法。如下
abstract class Shape {
public abstract void draw(); //被 abstract 修饰,这样就能省略花括号
public void display() {
System.out.println("显示方法");
}
}
抽象类的作用
抽象类存在的最大意义就是为了被继承。因为抽象类是不能自己进行实例化的。要想使用这个类,只能通过继承的方式,通过子类来进行重写这个方法里面所有的抽象方法。
包含抽象方法的类,称为抽象类。方法和类都是由 abstract 修饰的
抽象类中可以定义成员变量和成员方法(抽象与非抽象)
抽象类不能被实例化
抽象类存在的意义就是为了被继承
一个普通类继承了抽象类,那么普通类要重写抽象类中所有的抽象方法
抽象方法不能是被 final 修饰的, 也不能被 private 修饰。final 和 abstract 不能共存。
一个抽象类 A 继承了另外一个抽象类 B,此时有一个普通类 C 继承了抽象类 A,那么此时普通类 C 需要重写 A 和 B 两个类的所有抽象方法。
如果一个普通类继承了抽象类,普通类又不重写父类的抽象方法,此时则可以将这个子类也变为抽象类。就不用重写父类的抽象方法。
=============================================================
我们都知道在 Java 中,一个类只能继承一个父类,并不像 C++那样能够实现多继承。有人就想啊,如果我继承了一个抽象类,那么就不能再继承其他类了,那该怎么办。
所以在 Java 中引入了接口的概念,接口是抽象类的升级版呢。
在上文中的我们将 Shape 写成一个类,可以实现多态,那么我们实现成接口,该怎么实现呢?如下:
interface IShape {
public abstract void draw(); //接口里,默认的就是被 public abstract 修饰
}
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(); //接口里,默认的就是被 public abstract 修饰
}
class Cycle implements IShape {
void draw() { //访问权限并没有与接口中的方法相等或大于。即就是这里必须写 public
System.out.println("画一个圆");
}
}
提示:
1、 我们创建接口时,接口的命名一般以大写字母 I 开头
2、 接口的命名一般使用“形容词”词性的单词
3、 阿里编码规范,接口中的方法和成员变量不要加任何的修饰符,保持代码的简洁性
在 Java 中无法实现多继承,所以我们只能通过实现多个接口,来达到类似于多继承的情况。那么该如何实现多个接口呢?我们以一个例如来说:
//青蛙,既能在陆上跳,也能在水里游
interface ISwimming {
void swimming(); //默认是被 public abstract 修饰的
}
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(); //默认是被 public abstract 修饰的
}
interface IJump {
void jump();
}
//通过 extends,实现接口之间的扩展
interface IAmphibious extends IJump, ISwimming {
}
如上代码,我们就将上面两个接口,结合在了一个接口里,新的接口里,还可以重新添加其他的方法。这样的话,就将很多功能的接口,结合了在一起,此时青蛙类再去实现 IAmphibious 接口,就能拥有接口里的所有方法。
接口里面的方法,只能是抽象方法,不能是普通方法。且这些方法,默认是被 public abstract 修饰的
在 JDK1.8 之后,接口里可以实现普通方法,但是需要用 default 修饰,即默认方法
接口里的成员变量,默认是被 public abstract final 修饰的
接口同样不能单独的实例化
类和接口之间用 implements 来实现,意为:实现接口
接口与接口之间,可以用 extends 来继承,意为:扩展接口
一个类,可以实现 N 个接口,主要目的就是为了达到“多继承”的情况
接口也是可以发生向转型和多态的
评论