目录
一、为什么会有继承
Java中使用类来对现实世界中的实体来进行描述,但是现实世界非常复杂,复杂之中却也存在一些联系,比如狗和鸡都是动物,它们都有一些共同的属性:颜色、体重、吃东西、喝水...
转换成代码:
可以发现,两个类中有很多重复的属性和方法,那么我们就可以把这些共性拿出来,实现代码复用
二、认识继承
1、继承的概念

上图中,Dog和Chicken都继承了Animal类,那么Animal类就是父类/基类/超类,Dog和Chicken就是Animal的子类/派生类,继承之后,子类就可以复用父类的成员。
2、继承的语法
在Java中,如果要表示类之间的继承关系,需要使用extends关键字:
注意:
(1)子类会将父类中的成员变量和成员方法继承到子类中
(2)子类继承父类后,必须要添加新成员,以显示出与父类的不同,否则就没必要继承了。
3、父类成员访问
3.1 子类访问父类的成员变量
1、子类和父类中不存在同名变量时
2、子类和父类存在同名成员变量
此时会优先访问子类的成员变量。
总结:成员变量遵循就近原则,自己有则优先访问自己的,自己没有则在父类中寻找,父类中也没有代码就会报错。
思考:那么如何在子类中访问同名的父类成员变量呢?
3.2 子类访问父类的成员方法
1、子类和父类中没有同名的成员方法
此时优先在子类中找,找到则访问,找不到则在父类中寻找,找到则访问,找不到则报错。
2、子类和父类中有同名的成员方法
代码运行结果:
说明当子类和父类中存在同名且同参数列表(构成方法重写)的成员方法时,优先访问子类的成员方法。
如果子类和父类的方法名相同,但参数列表不同(构成方法重载),则根据传递的参数选择合适的方法访问。
代码运行结果:
总结:与访问父类的成员变量类似,遵循就近原则,如果子类与父类中的方法构成重载,那么根据传递的参数访问合适的方法。
思考:如何在子类中访问构成重写的成员方法呢?
4、super关键字
为了解决上述两个思考问题,我们需要使用到super关键字。
该关键字的主要作用就是在子类方法中访问父类的成员。
注意:
(1)super关键字类似于this关键字,只能在非静态方法中使用
(2)super关键字用来在子类方法中,访问父类的成员变量和方法
5、子类和父类中的构造方法
子类中有两部分成员,一类是从父类继承来的,一类是自己新增的,所以在构造子类对象的时候,先要调用父类的构造方法,把父类的成员初始化之后,再调用子类的构造方法,初始化自己新增的成员。
注意:
(1)在子类构造方法中,super()调用父类构造方法时,必须是子类构造方法的第一条语句;
(2)如果父类的构造方法是默认的或者无参的,那么在子类构造方法第一行默认有隐藏的super(),用来调用父类构造方法;
(3)如果父类的构造方法是有参数的,此时编译器不再给子类生成默认的构造方法,需要我们手动定义子类构造方法;
(4)super只能在子类构造方法中出现一次。
6、代码运行顺序
如果父类和子类中都有实例代码块、静态代码块、构造方法,那么它们的执行顺序是如何呢?我们通过一段代码来试验一下:
class Father{
public Father() {
System.out.println("父类构造方法");
}
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类实例代码块");
}
}
class Son extends Father{
public Son() {
System.out.println("子类构造方法");
}
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类实例代码块");
}
}
public class Test2 {
public static void main(String[] args) {
Son son1 = new Son();
System.out.println("——————————————————————");
Son son2 = new Son();
}
}
代码运行结果:
根据代码运行结果,可以得出以下结论:
(1)父类静态代码块最早执行,优先于子类静态代码块;
(2)父类实例代码块和父类构造方法紧接着执行;
(3)子类实例代码块和子类构造方法最后执行;
(4)第二次实例化子类对象时,父类和子类的静态代码块都不再会执行。
7、继承与组合
组合和继承类似,也是一种表达类之间的方式,组合是将一个类的实例,作为另一个类的字段,以此来达到代码复用的效果。
举个栗子:一辆汽车和它的轮胎、发动机、方向盘、车身、座椅等的关系就可以用组合来表示,因为汽车是由这些部件组成的。
组合和继承都可以实现代码复用,应该使用哪种需要根据应用场景来选择,一般建议:能用组合尽量用组合。
三、多态
1、多态的概念
多态,即多种状态,就是同一种行为,不同的对象去做会产生不同的状态/结果。
比如,一张高考数学试卷,让一个小学生做,结果可能是0分;让一个高中生做,结果可能是100分,让一个高中数学老师做,结果可能是120分。
2、实现多态的条件
在Java中实现多态,必须满足以下几个条件:
(1)必须在继承体系下;
(2)子类必须要对父类中的方法进行重写;
(3)通过父类的引用调用重写的方法。
2.1 重写
重写(override)也叫覆写、覆盖,是子类对父类中非static、private、final修饰,以及非构造方法的实现过程进行重新编写,方法名、返回值类型和参数列表不变。
方法重写的规则:
(1)子类重写父类的方法时,一般要求必须与父类方法原型一致:修饰符,方法名、返回值类型、参数列表要完全一致;
(2)被重写的方法返回值类型可以不同,但必须具有父子类关系;
(3)子类中重写的方法的访问权限必须大于等于父类的访问权限。比如,父类方法如果被protect修饰,子类中重写该方法的修饰符只能为protect或者public。
访问权限关系图:
(4) 父类的方法如果被static、private、final修饰,子类不能重写。
2.2 向上转型
向上转型就是用父类引用来引用一个子类对象。
例如:
class Animal{
public String name;
public void eat(){ //父类重写方法的原型
System.out.println(name+"吃东西");
}
public Animal(String name) { //父类构造方法
this.name = name;
}
}
class Dog extends Animal{
public Dog(String name){ //子类构造方法
super(name);
}
public void eat(){ //子类重写方法
System.out.println(name + "正在吃狗粮");
}
public void bark(){ //子类普通方法
System.out.println(name+"正在汪汪叫");
}
}
class Bird extends Animal{
public Bird(String name){ //子类构造方法
super(name);
}
public void eat(){ //子类重写方法
System.out.println(name+"正在吃虫子");
}
public void fly(){ //子类普通方法
System.out.println(name + "正在天空飞翔");
}
}
public class Test {
public static void main(String[] args) {
Animal animal1 = new Dog("炫炫");
Animal animal2 = new Bird("坤坤");
}
}
上述代码中,父类是Animal,子类是Dog和Bird,在main函数中,我们分别new了一个Dog类的对象和一个Bird类的对象各自存入到一个Animal父类的引用中,这就是一次向上转型。当我们通过animal1和animal2这两个引用来调用eat方法时,结果如下:
animal1和animal2两个父类的引用并没有调用父类的eat方法,而是分别调用了两个子类中重写的eat方法,但是当我们通过反汇编查看字节码文件时,可以发现,它调用的还是父类的方法???
//图先欠着
是因为在代码运行时发生了动态绑定。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。函数重载就是典型的代表。
向上转型的三种使用场景:
(1)第一种就是刚才的直接赋值:子类对象赋值给父类引用。
(2)第二种是方法传参:
(3)第三种是作为方法的返回类型:
向上转型的优点:让代码变得更加简单灵活。
向上转型的缺点:不能调用子类特有的方法。
当通过向上转型调用子类自己的方法时,代码就会报错,此时就需要借助向下转型了。
2.3 向下转型
向下转型就是将已经进行向上转型的父类引用再还原为子类对象。
向下转型一般用的比较少,因为它不安全,可能会转换失败:
代码虽然可以通过编译,但运行时出现异常。因为animal1本来向上转型之后引用的是一个鸟类对象,它实际指向的对象是鸟,当我们把它向下转型还原为狗类时就会无法正常还原。
Java中为了提高向下转型的安全性,引入了instanceof关键字,如果该表达式为真,则可以安全转换:
此时代码就不会抛出异常了。
3、多态的优缺点
优点:
(1)能够降低代码的“圈复杂度”,避免使用大量的if-else语句;
(2)可扩展能力强,使用多态的方式改动代码的成本也比较低,如果需要新增一种动物的eat方法,只要创建一个新的类,重写一个eat方法就可以了。
缺点:
代码的运行效率较低。