目录
一、继承
1、继承的含义及作用
2、示例代码
3、子类对象的内存存储
4、super
5、父类和子类成员的访问顺序
6、重写
7、初始化成员变量
8、继承方式
9、final
10、protected
二、组合
三、多态
1、概念#
2、实现多态的条件
3、向上转型#
4、向下转型
5、动态绑定#
6、多态的优势
7、不要在构造方法中调用重写方法
一、继承
1、继承的含义及作用
一类可能含有多个子类,如:动物的子类有,猫、狗……。这些子类具有共性,将共性提取出为父类,如:动物的共性是,有名字、年龄……
继承则是子类(派生类)继承父类(基类/超类)的成员(共性)。
继承实现了共性的抽取,代码复用。
继承就是 is-a 的关系:猫 is a 动物。
2、示例代码
标志:extends
父类:Animal
package inherit;
public class Animal {
public String name;
public int age;
public void eat() {
System.out.println(this.name+"在吃饭...");
}
}
子类:Cat、Dog
package inherit;
public class Cat extends Animal{
public void mimi() {
System.out.println(name+"咪咪叫.....");
}
}
package inherit;
public class Dog extends Animal{
public void bark() {
System.out.println(name+"汪汪叫....");
}
public void wag() {
System.out.println(name+"在摇尾巴...");
}
}
3、子类对象的内存存储
继承了父类的成员变量:
如果,父类 Base 和子类 Derived 分别所包含的成员变量如下:
那么子类对象的内存存储:
4、super
- super.data 访问父类的成员变量。
- super.func() 访问父类的成员方法。
- super() 调用父类的构造函数(必须在第一行,因此不能与 this() 共存)。
- 不能在静态成员方法中使用(没对象,就没 this,就没 this 的 super)。
5、父类和子类成员的访问顺序
成员不同:看子类->看父类->都没有则报错。
成员相同:优先子类。
成员相同,想访问父类的:用 super。
6、重写
父类和子类的成员方法:
- 重载(overload):方法名相同,参数列表不同,返回值不要求。
- 重写(override):方法名相同,参数列表相同,返回值类型“相同”。
注意:
① 返回值类型可以不同,要求是存在父子关系。如:返回值类型是 Animal 和 Cat。
② 子类重写的方法的修饰词,不能比父类的权限更低。如父类为 protected,子类就只能是 protected 或 public。
③ static、private、final修饰的方法,以及构造方法不能重写。
④ @Override 注解可以检查子类中重写的方法在父类中是否存在。如:父类是 eat,子类写成 eat2 就会报错,没有注解就没提示,不改过来重写就没成功。
为什么需要重写:
实际生活中,开发出新手机程序,不可能直接去掉旧版的方法(还有老用户),而是在子类中重写旧版方法。
7、初始化成员变量
有父才有子,初始化子类成员前,一定要先初始化父类成员。
① 构造函数初始化普通成员变量:
注:若父类存在参数列表为空的构造函数(默认或者自己定义),编译器会自动在子类构造函数的第一行加 super(),即子类的构造函数中必须有 super()。但是 this() 没手动加,就没有,即不是必须有的。
② 代码块的执行顺序:
父类静态->子类静态->父类实例->父类构造->子类实例->子类构造。
8、继承方式
- 不支持多继承:即不支持一个类有多个父类。
- 最好不超过三层继承关系。
9、final
- 修饰变量:常量。
final 修饰数组对象的引用 array,array不能更改,但数组对象能更改。
- 修饰类:不能被继承。
如:Cat 这个类不能再被其他类继承。
- 修饰方法:不能被重写。(后续学习)
10、protected
修饰类中成员,可以被同一包中的类访问、可以被不同包中的子类访问。
子类会继承父类中的 private 成员变量,只不过不能访问。
二、组合
组合就是 has-a 的关系,用一个类的实例作为另一个类的字段,传入的实例是动态的。如:车 has 轮胎、方向盘、喇叭......。
class Car{
private Tire tire; // 复用轮胎
private Engine engine; // 复用发动机
private VehicleSystem vs; // 复用车载系统
......
}
组合和继承都可实现代码复用,同样可行的情况下,最好用组合(整体类与局部类之间相对独立、更灵活)。
三、多态
1、概念#
同一个声明,不同的对象,实现不同的行为(多种形态,即多态)。如:动物(声明)的实例对象(不同对象),可以是猫(吃猫粮)、狗(吃狗粮)......。
2、实现多态的条件
- 继承关系。
- 子类对父类方法实现重写(定义不同的行为)。
- 用父类引用(向上转型)调用子类重写方法(实现同一声明)。
3、向上转型#
- 用父类类型引用变量,引用子类类型对象。
Animal animal = new Dog("小七", 3);
- 用父类类型引用形参,接受子类类型对象。
public void func(Animal animal){
......
}
public static void main(String args[]){
Dog dog = new Dog("小七", 3);
func(dog);
}
- 用返回值类型为父类类型的方法,返回子类类型对象。
public Animal func(){
Dog dog = new Dog("小七", 3);
return dog;
}
一个例子:toString。Object 是所有类的父类,Dog 与 Object 存在继承关系,println 的形参就属于上述的第二种向上转型(父类的引用),toString 定义在 Object 类中,Dog 类就可以重写 toString。Object 的引用调用 Dog 的重写方法,这样就实现了多态,同一个 println 方法根据不同的对象实现不同的打印。
注:向上转型,不能调用子类特有的方法。
4、向下转型
向上转型后,不能使用子类特有方法,想使用,需要向下转型回来。
Animal animal = new Dog("小七", 3);
Dog dog = (Dog)animal; // 向下转型
dog.bark();
但是不能乱转,乱转会报错,不安全,比如:
Animal animal = new Dog("小七", 3);
Cat cat = (Cat)animal; // 类型转换错误
为了避免错误,使用 instanceof 检验:
Animal animal = new Dog("小七", 3);
if(animal instanceof Cat){
Cat cat = (Cat)animal;
cat.mew();
}
else
System.out.println("不能转换");
5、动态绑定#
用 javap -c 字节码文件名 查看反汇编文件:
动态绑定:编译时期调用的父类方法,运行时期根据具体对象调用对象的方法(灵活)。即在运行时期才知道方法的具体实现(哪个类当中的)。
静态绑定:在编译时期,根据方法名、方法的参数列表等,就确定方法的具体实现。
6、多态的优势
① 减小“圈复杂度”,即循环、if-else 的个数。比如,有以下继承关系:
// 父类
public class Animal {
public void eat() {
System.out.println(" 正在吃饭....");
}
}
// 子类 Dog
public class Dog extends Animal{
@Override
public void eat() {
System.out.println(" 正在吃狗粮..");
}
}
// 子类 Cat
public class Cat extends Animal{
@Override
public void eat() {
System.out.println(" 正在吃猫粮...");
}
}
实现一个各种动物数组的循环 eat,if-else 版:
多态版:
子类越多,这种优势越明显。
② 扩展性强。一个类,不需要改动原有代码。比如:增加鸟类,用多态,实现鸟类即可;用 if-else,还得改原有代码的 if-else 部分。
注意:成员变量和构造方法,没有多态性。
7、不要在构造方法中调用重写方法
// 父类
class B {
public B() {
// do nothing
func(); // 因为子类重写,调用的重写方法
}
public void func() {
System.out.println("B.func()");
}
}
// 子类
class D extends B {
private int num = 1; // 子类实例成员变量
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
// 测试
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
// 执行结果
D.func() 0
分析:创建子类 D 对象->执行父类构造函数->因为子类重写func,执行子类的func->打印 D.func() 0 (0 是 num 的默认值)->执行子类实例成员的初始化,此时才为1。
为了避免这种不易发现的错误,不要在构造函数中调用重写方法。