[Java] 继承和多态
1. 继承
1.1 什么是继承
生活中有很多继承的例子,如儿子继承父亲的家产,学弟继承学长的宿舍等,而在Java中继承就是子类继承了父类里面的成员,也就相当于在父类的基础上进行拓展的内容放到子类里面,从简单到复杂的过程,而Java里面的继承是通过extends关键字实现的。
1.2 继承的作用和好处
当有两个类dog类和cat类如下:
public class Dog {public String name;public int age;public void eat() {System.out.println(this.name + "在吃东西");}public void map() {System.out.println(this.name + "在喵喵叫");}
}
public class Cat {public String name;public int age;public void eat() {System.out.println(this.name + "在吃东西");}public void wap() {System.out.println(this.name + "在汪汪叫");}
}
通过观察会发现上面的dog类和cat类里面有一些共有的成员变量和方法,这时候我们就在想能不能把这些元素放到同一个类里面被其他需要的类使用呢?
这时候就想到了继承,继承就可以实现这一需求,这时我们再创建一个类:
public class Test {public String name;public int age;public void eat() {System.out.println(this.name + "在吃东西");}
}
此时我们就可以通过关键字extends来实现将Test类里面的成员用到dog和cat类中。
而通过继承可以减少代码的复用。
1.3 继承的语法
修饰符 class 子类 extends 父类 { }
上面的cat和dog类就可以通过extends关键字实现继承,如下图:
public class Dog extends Test {public void map() {System.out.println(this.name + "在喵喵叫");}
}
public class Cat extends Test {public void wap() {System.out.println(this.name + "在汪汪叫");}
}
此时dog类和cat类就包含了父类里面的成员,在内存里也就是:
1.4 子类访问父类里面的成员变量
1. 子类和父类里面的成员变量不同名
这时候可以直接访问:
//父类
public class Base {int a;int b;
}
//子类
public class Derived extends Base {int c;public void test() {a = 10;b = 20;c = 30;}
}
子类可以直接使用this访问子类和父类中的成员变量。
2. 子类和父类中存在成员变量同名
//父类
public class Base {int a;int b;
}
//子类
public class Derived extends Base {int c;int a;public void test() {a = 10;b = 20;c = 30;}
}
此时父类和子类都有成员变量a,此时子类会优先访问子类的变量。
- 当子类和父类有相同名字的成员变量时候时候,会优先访问子类的成员变量。
- 若子类和父类都没有的成员变量访问则会编译错误。
1.5 子类访问父类里面的成员方法
1. 子类和父类方法名字不同。
则子类直接访问访问父类的方法。并且子类和父类的方法可以构成方法的重载,如下面代码:
public class Base {public void sit() {System.out.println("hehe");}
}public class Derived extends Base {public void print() {System.out.println("haha");}public void sit(int a) {System.out.println("hehe");}public void test() {print();//调用子类的方法//构成重载sit();//调用父类的方法sit(10);//调用子类的方法}
}
当子类和父类的方法相同时,则子类调用父类时,则会就近原则优先调用子类。代码如下:
public class Base {public void sit() {System.out.println("hehe");}
}public class Derived extends Base {public void print() {System.out.println("haha");}public void sit() {System.out.println("haha");}public void test() {print();//调用子类的方法sit();//子类和父类都有该方法会优先调用子类的方法,会打印haha。}
}
1.6 super关键字
上面我们发现子类和父类当中如果出现相同的成员变量或者成员方法时,子类调用该成员时会优先调用子类的成员,那如何调用父类的成员呢?
这里就要用到super关键字了,super关键字就是用来在子类中调用父类的成员的,当子类和父类有相同的成员时,就使用super关键字区分。
super关键字用于子类调用父类成员变量:
下面代码可以发现当子类和父类都有同一个成员变量a时,就可以通过super关键字来调用父类的成员变量a,当然this既可以调用子类里面的成员变量,又可以调用父类的成员变量,只有子类和父类有相同名字成员变量时,才用super加以区分。
public class Base {int a;int b;
}public class Derived extends Base {int a;int c;public void test1() {this.a = 10;//this调用的父类的成员变量。super.a = 20;//super调用的子类的成员变量。this.b = 40;//this调用的子类的成员变量。}
}
在堆区里是这样的:
super关键字用于子类调用父类成员方法:
这里和成员变量一样,用super关键字来区分相同的成员方法,这里的成员方法支持方法的重载:
public class Base {public void print() {System.out.println("nihao");}public void sit() {System.out.println("hehe");}
}public class Derived extends Base {public void print() {System.out.println("buhao");}public void sit(int a) {System.out.println("hehe");}public void test() {this.print();//调用子类的方法super.print();//调用父类的方法//构成重载this.sit();//调用父类的方法this.sit(10);//调用子类的方法}
}
super关键字用于调用构造方法:
当父类有构造方法的时候,子类也必须有构造方法,因为子类要通过super关键字先调用父类的构造方法,再设置自己的成员方法,并且通过super(父类的构造方法名)的形式调用,并且必须放在构造方法的第一行。所以super和this不能共用,代码如下:
public class Base {int a;int b;//父类的构造方法public Base(int a, int b) {this.a = a;this.b = b;}
}public class Derived extends Base {int c;int d;//子类的构造方法public Derived(int a, int b, int c, int d) {super(a,b);//第一行先调用父类的构造方法this.c = c;this.d = d;}
}
当子类和父类都没有构造方法是,Java中提供了隐藏的构造方法在子类和父类中。
当父类中定义了无参的 构造方法时,子类的构造方法第一行不写调用父类的代码时,Java也会隐藏的在第一行写上该代码,但如果是有参的,则需要自己书写。
1.7 this和super
相同:
- 都不能在静态方法中使用。
- 都是关键字。
- 在构造方法中调用时必须是第一条语句。
不同:
- this是对当前对象的成员调用,而super是对从父类中继承下来的成员的调用。
- 在构造方法中,this用于对本类构造方法的访问,而super是对父类的构造方法的访问。两者不能同时存在。
1.8 继承关系上的调用顺序
我们之前说过,在初始化成员变量时,在实例化一个类时,调用优先级是:静态代码块>实例代码块>构造方法。
而在同时调用一个继承父类的子类和该父类时,调用优先级是:父类的静态代码块优于子类的静态代码块,父类的实例和构造优于子类的实例和构造,而在第二次实例化对象时,静态初始化不再调用,因为类只加载一次。
1.8 protected
我们知道protected是访问限定符,而它修饰的变量使用的范围是同一包下的都可以访问,再加上不同包下的子类可以访问,代码如下:
这里变量d是用protected修饰的在demo2包里,而在demo3里面继承Base,可以调用d变量。
package demo2;public class Base {protected int d;
}package demo3;
import demo2.Base;public class Derived extends Base {public void test() {super.d = 20;}
}
1.9 继承方式
Java里面提供了以下继承方式:
单继承:就是A 继承 B。
多层继承:A 继承 B ,B 继承 C
不同类继承同一个类,A 继承 C,B 继承 C;
但是不支持一个类继承多个类:A 继承 B, A 继承 C;这种是错误的继承方式。
1.10 final关键字
finai关键字有三种用法:
1. 密封类 当一个类被final修饰后,则该类不能再被其他的类继承了。
2.修饰变量:final修饰变量后该变量就变成常量了。
3.密封成员方法:表示该方法不能被重写。
1.11 组合
组合就是说对象之间是包含关系,它可以把类与类之间分开成独立的个体,需要的时候相互调用,不同于继承的思想,当父类改变时一定会影响子类,相当于汽车这个对象是由轮胎,发动机等对象组成的,而汽车这个类里面可以调用轮胎和发动机类里面的一些属性和方法来使用,如果这时加入一个电池类,但是油车没有电池,电车才有,如果是继承关系那么油车就不能继承Car这个类了,但如果是组合思想,电车就可以单独调用这个电池类。
class Tire {//轮胎类
}
class Engine {//发动机类
}
public class Car {public Tire tire;public Engine engine;
}
2. 多态
2.1 方法的重写
方法的重写是在继承的条件下,子类对父类的方法进行重写,子类重写的方法,返回值类型,方法名,参数都完全相同,并且子类的访问修饰符的范围要大于等于父类的访问修饰符的范围。
当然参数之间也可以不用相同,参数之间构成父子类关系也可以实现方法的重写,这种叫做协变类型。
可以使用@Override来判断该方法是否重写,如果重写错误,还会显示报错。
private修饰的方法,构造方法,静态方法,final修饰的方法等都能重写。
下面代码中,Cat继承Animal,子类对父类的eat方法进行了重写。
public class Animal{public String name;public int age;public Animal() {}public void eat() {System.out.println("正在吃食物");}
}
public class Cat extends Animal{public Cat() {super();}@Overridepublic void eat() {System.out.println("正在吃猫粮");}
}
2.2 向上转型
向上转型就是父类的引用指向子类的对象。下面代码父类Animal引用指向子类Cat对象。
Animal animal = new Cat();
animal.eat();
这时候父类的引用可以调用父类的成员,但是只能调用子类的重写父类的方法,其他方法不能调用。
常见的向上转型的方式:
- 直接赋值
- 参数传递
- 返回值传递
2.3 动态绑定和静态绑定
动态绑定就是,在父类的引用调用重写的方法时,编译过程调用的是父类的方法,但在运行时绑定的却是子类的方法,最后运行结果调用的是子类的方法。
这是因为在Java虚拟机中存在一块空间为方法区,这里的每个方法都绑定一个地址,在运行时父类方法绑定的地址变为子类方法绑定的地址,所以调用的子类的方法。
动态绑定的条件 :
- 发生向上转型
- 存在方法的重写
- 父类引用调用了重写的方法
静态绑定是在编译时候绑定的,比如说方法的重载,根据参数来选择调用的方法。
2.4 多态
多态就是同一种类型的引用调用同一种方法,该引用所指的对象不同,得到的结果也不同。
下面代码中,父类的引用分别指向子类Bird和子类Cat,都调用eat方法,产生的结果也不相同,这就是多态。
public class Animal{public String name;public int age;public Animal() {}public void eat() {System.out.println("正在吃食物");}
}public class Cat extends Animal{public Cat() {super();}@Overridepublic void eat() {System.out.println("正在吃猫粮");}
}public class Bird exteends Animal {public Bird() {super();}@Overridepublic void eat() {System.out.println("正在吃鸟粮");}public void fly() {System.out.println("正在飞");}
}
public class Test {public static void main(String[] args) {Animal animal1 = new Cat();animal1.eat();Animal animal2 = new Bird();animal2.eat();}
}
结果:
多态产生的条件:
- 子类对父类的方法进行重写
- 继承的条件下
- 通过父类的引用调用子类的重写的方法。
2.5 向下转型
向下转型就是说将父类引用的类型强制类型转换为子类的类型。
Animal animal = new Cat();Cat cat = (Cat)animal;
向下转型后,就可以调用子类的成员。
但是如果父类引用所指的对象的类型,与强制类型转换的类型不一样就会编译报错,产生ClassCastExceprion(类型转换异常),所以向上转型也不太安全,所以不经常使用。
为了解决这种安全性,Java引入了关键字 instanceof 用来判断强制类型转换是否安全。
Animal animal = new Cat();if(animal instanceof Cat) {Cat cat = (Cat)animal;}else {System.out.println("不安全");}
2.6 圈复杂度
圈复杂度是用来描述代码的复杂程度的,一般跟条件语句和循环语句的数量有关,而多态就可以很好的解决圈复杂度过大的问题。
假设要打印形状,就可以利用多态来实现:
public class Shape {public Shape() {}public void print() {System.out.println("画一个图案");}
}
public class Rect extends Shape {public Rect() {super();}public void print() {System.out.println("画一个矩形");}
}
public class Cycle extends Shape {public Cycle() {super();}public void print() {System.out.println("画一个圆形");}
}public class Test {public static void drawShapes(Shape shape) {shape.print();}public static void main(String[] args) {Rect rect = new Rect();Cycle cycle = new Cycle();Shape[] shapes = {rect,cycle,rect,cycle};for(Shape shape : shapes) {drawShapes(shape);}}
}
打印结果如下:
利用多态实现的代码的圈复杂度相对来说就很低。
注意:尽量不要在构造方法中调用重写的方法,构造方法也支持动态绑定。减少使用实例化的方法。否则可能会出现问题。可以使用finlal和private修饰的方法,它们不支持方法的重写。