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(); //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修饰
}
写在最后
为了这次面试,也收集了很多的面试题!
以下是部分面试题截图
interface
修饰的,而不是class
-
接口不能单独被实例化
-
接口中的方法默认是被public abstract修饰的
-
接口中的变量默认是被public static final 修饰的
-
让类与接口连接起来,术语叫:实现接口。使用
implements
关键字,写在类名的末尾,后面写接口名 -
若这个类还继承了父类,那么接口应写在父类名的后面,如下:
class Student extends Person implements Ishape {
}
尤其切记:接口中的方法,默认是被public abstract修饰的,有如下错误的代码
interface IShape {
abstract void draw(); //接口里,默认的就是被 public abstract修饰
}
写在最后
为了这次面试,也收集了很多的面试题!
以下是部分面试题截图
[外链图片转存中…(img-LyNQo4Ik-1714631374213)]