面向对象编程

继承

背景

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法)。
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加功能,这样产生新的类,称派生类(子类)。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。

继承主要解决的问题是:共性的抽取
子类可以拥有父类的内容
子类也可以拥有属于自己独有的内容
被继承的类, 我们称为 父类 , 基类 或 超类, 对于继承的类, 我们称为 子 类(派生类)和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果.
在这里插入图片描述
在这里插入图片描述

语法规则

基本语法

class 子类 extends 父类 {
    
}

使用 extends 指定父类.
Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
子类会继承父类的所有 public 的字段和方法.
对于父类的 private 的字段和方法, 子类中是无法访问的.

子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
举例:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
   }
    public void eat(String food) {
        System.out.println(this.name + "正在吃" + food);
   }
}
class Cat extends Animal {
    public Cat(String name) {
        // 使用 super 调用父类的构造方法. 
        super(name);
   }
}
class Bird extends Animal {
    public Bird(String name) {
        super(name);
   }
    public void fly() {
        System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
   }
}
public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat("小黑");
        cat.eat("猫粮");
        Bird bird = new Bird("圆圆");
        bird.fly();
   }
}
//小黑正在吃猫粮
//圆圆正在飞︿( ̄︶ ̄)︿

extends 英文原意指 “扩展”. 而我们所写的类的继承, 也可以理解成基于父类进行代码上的 “扩展”
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

对象的内存布局

当子类继承了父类之后,此时子类的内存布局应该是什么样子的?例如以下代码

class Base {
    public int m;
     }
class Derieve extends Base {
    public int n;
     }
public class TestDemo {
    public static void main(String[] args) {
        Base base1 = new Base();//语句1
        Derieve derieve = new Derieve();//语句2
        Base base2 = new Derieve();//语句3
   }
}

Base base1 = new Base();这句代码,base1就是一个引用变量,它指向了一个Base对象。
也就是说:base1 引用了一个Base对象,我们通过操作base1来操作Base对象。base1 中存储的就是Base对象地址的哈希码。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当父类和子类都有同名的数据成员:

class Base {
    public int m; }
class Derieve extends Base {
    public int m;
    public int n; }
public class TestDemo {
    public static void main(String[] args) {
        Base base = new Derieve();
   }
}

在这里插入图片描述
子类当中如何访问父类当中的相同的数据成员?

class Base {
    public int m=10; }
class Derieve extends Base {
    public int m=11;
    public int n;
    public void func() {
        System.out.println("访问Derieve类新增的数据成员m:"+m);
        //本课件后面会详细讲到super关键字
        System.out.println("通过super访问父类的数据成员:"+super.m);
   }
}
public class TestDemo {
    public static void main(String[] args) {
        Base base = new Derieve();
        System.out.println(base.m);
        Derieve derieve = new Derieve();
        System.out.println(derieve.m);
        System.out.println("=====子类当中如何访问,父类中的数据成员======");
        derieve.func();
   }
   }
//执行结果
10
11
=====子类当中如何访问,父类中的数据成员======
访问derieve类新增的数据成员m:11
通过super访问父类的数据成员:10

protected 关键字

对于类的调用者来说, protected 修饰的字段和方法是不能访问的
对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的

// Animal.java
public class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
   }
    public void eat(String food) {
        System.out.println(this.name + "正在吃" + food);
   }
}
// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
   }
    public void fly() {
        // 对于父类的 protected 字段, 子类可以正确访问
        System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
   }
}
// Test.java 和 Animal.java 不在同一个 包 之中了. 
public class Test {
    public static void main(String[] args) {
        Animal animal = new Animal("小动物");
        System.out.println(animal.name); // 此时编译出错, 无法访问 name
   }
}

private: 类内部能访问, 类外部不能访问
默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.
public : 类内部和类的调用者都能访问

我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private,就尽量不要用 public。
还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用。
在实际开发过程中会出现多层继承, 即子类还可以进一步的再派生出新的子类
一般我们不希望出现超过三层的继承关系.如果继承层次太多, 就需要考虑对代码进行重构了.

final 关键字

曾经我们学习过 final 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改)

final int a = 10; a = 20;  // 编译出错

final 关键字也能修饰类, 此时表示被修饰的类就不能被继承

final public class Animal {
   ...
}
public class Bird extends Animal {
   ...
}
// 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继承

final 关键字的功能是 限制 类被继承
我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承

组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.
举例:

public class Student {
   ...
}
public class Teacher {
   ...
}
public class School {
    public Student[] students;
    public Teacher[] teachers; }

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.
组合表示 has - a 语义
在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师。
继承表示 is - a 语义
在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物。

多态

继承其实就是实现多态的一个前提

向上转型

形如下面的代码

Bird bird = new Bird("圆圆");

代码也可以写成这个样子

Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型
向上转型发生的时机:
直接赋值
方法传参
方法返回

方法传参

public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird("圆圆");
        feed(bird);
   }
    public static void feed(Animal animal) {
        animal.eat("谷子");
   }
}
// 执行结果
圆圆正在吃谷子

此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例
方法返回

public class Test {
    public static void main(String[] args) {
        Animal animal = findMyAnimal();
   }
    public static Animal findMyAnimal() {
        Bird bird = new Bird("圆圆");
        return bird;
   }
}

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例

动态绑定

当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢

// Animal.java
public class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
   }
    public void eat(String food) {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "正在吃" + food);
   }
}
// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
        }
    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
   }
}
// Test.java
public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Animal("圆圆");
        animal1.eat("谷子");
        Animal animal2 = new Bird("扁扁");
        animal2.eat("谷子");
   }
}
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子

animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例,animal2 指向 Bird 类型的实例.
针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, animal2.eat() 实际调用了子类的方法.
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为动态绑定.

方法重写

针对刚才的 eat 方法来说:子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).
关于重写的注意事项
1.重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
2. 普通方法可以重写, static 修饰的静态方法不能重写.
3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.
4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外)
方法权限示例: 将子类的 eat 改成 private

// Animal.java
public class Animal {
    public void eat(String food) {
       ...
   }
}
// Bird.java
public class Bird extends Animal {
    // 将子类的 eat 改成 private
    private void eat(String food) {
       ...
   }
}
// 编译出错
Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
eat(java.lang.String)
 正在尝试分配更低的访问权限; 以前为public

另外, 针对重写的方法, 可以使用 @Override 注解来显式指定

// Bird.java
public class Bird extends Animal {
    @Override
    private void eat(String food) {
       ...
   }
}

有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
我们推荐在代码中进行重写方法时显式加上 @Override 注解

重写的设计原则

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容;

重载和重写的区别

在这里插入图片描述

理解多态

代码示例: 打印多种形状

class Shape {
    public void draw() {
        // 啥都不用干
   }
}
class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("○");
   }
}
class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("□");
   }
}
class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("♣");
   }
}
/我是分割线//
// Test.java
public class Test {
    public static void main(String[] args) {
        Shape shape1 = new Flower();
        Shape shape2 = new Cycle();
        Shape shape3 = new Rect();
        drawMap(shape1);
        drawMap(shape2);
        drawMap(shape3);
   }
 // 打印单个图形
    public static void drawShape(Shape shape) {
        shape.draw();
   }
}

在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道,也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态
多态顾名思义, 就是 "一个引用, 能表现出多种不同形态
使用多态的好处
1.类调用者对类的使用成本进一步降低
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可。
多态可以理解成是封装的更进一步
2. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下

public static void drawShapes() {
    Rect rect = new Rect();
    Cycle cycle = new Cycle();
    Flower flower = new Flower();
    String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
    
    for (String shape : shapes) {
        if (shape.equals("cycle")) {
            cycle.draw();
       } else if (shape.equals("rect")) {
            rect.draw();
       } else if (shape.equals("flower")) {
            flower.draw();
       }
   }
}

使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单

public static void drawShapes() {
    // 我们创建了一个 Shape 对象的数组. 
    Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
                      new Rect(), new Flower()};
    for (Shape shape : shapes) {
        shape.draw();
   }
}

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低

向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见, 但是也有一定的用途

// Animal.java
public class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
   }
    public void eat(String food) {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "正在吃" + food);
   }
}
// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
   }
    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
   }
    
    public void fly() {
        System.out.println(this.name + "正在飞");
   }
}

调用

Animal animal = new Bird("圆圆");
animal.eat("谷子");
// 执行结果
圆圆正在吃谷子
animal.fly();
// 编译出错
找不到 fly 方法

注意:
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.
虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.

对于 Animal animal = new Bird(“圆圆”) 这样的代码,
编译器检查有哪些方法存在, 看的是 Animal 这个类型
执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型
那么想实现刚才的效果, 就需要向下转型

// (Bird) 表示强制类型转换
Bird bird = (Bird)animal;
bird.fly();
// 执行结果
圆圆正在飞

这样就可以调用了
为了让向下转型更安全, 我们可以先判定一下看看

Animal animal = new Cat("小猫");
if (animal instanceof Bird) {
    Bird bird = (Bird)animal;
    bird.fly();
}

instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较
安全了.

super 关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办?
可以使用 super 关键字
super 表示获取到父类实例的引用. 涉及到两种常见用法

1.使用了 super 来调用父类的构造器

public Bird(String name) {
    super(name);
}

2.使用 super 来调用父类的普通方法

public class Bird extends Animal {
    public Bird(String name) {
        super(name);
   }
    @Override
    public void eat(String food) {
        // 修改代码, 让子调用父类的接口. 
        super.eat(food);
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
   }
}

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的
eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法
super 和 this区别
在这里插入图片描述

在构造方法中调用重写的方法(一个坑)

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用func

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 对象的同时, 会调用 B 的构造方法.
B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0
结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。

执行顺序

实例代码块和静态代码块。在没有继承关系时的执行顺序

class Person {
    public String name;
    public int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("构造方法执行");
   }
   {
        System.out.println("实例代码块执行");
   }
    static {
        System.out.println("静态代码块执行");
   }
}
public class TestDemo {
    public static void main(String[] args) {
        Person person1 = new Person("bit",10);
        System.out.println("============================");
        Person person2 = new Person("gaobo",20);
   }
   }

结果

静态代码块执行
实例代码块执行
构造方法执行
============================
实例代码块执行
构造方法执行

结论
1.静态代码块只执行一次
2、静态代码块先执行,实例代码块接着执行,最后构造方法执行
继承关系上的执行顺序

class Person {
    public String name;
    public int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Person:构造方法执行");
   }
   {
        System.out.println("Person:实例代码块执行");
   }
    static {
        System.out.println("Person:静态代码块执行");
   }
}
class Student extends Person{
    public Student(String name,int age) {
        super(name,age);
        System.out.println("Student:构造方法执行");
   }
   {
        System.out.println("Student:实例代码块执行");
   }
    static {
        System.out.println("Student:静态代码块执行");
   }
}
public class TestDemo4 {
    public static void main(String[] args) {
        Student student1 = new Student("张三",19);
        System.out.println("===========================");
        Student student2 = new Student("gaobo",20);
        }
    public static void main1(String[] args) {
        Person person1 = new Person("bit",10);
        System.out.println("============================");
        Person person2 = new Person("gaobo",20);
   }
}

结果:

Person:静态代码块执行
Student:静态代码块执行
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
===========================
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行

结论:
1、父类静态代码块优先于子类静态代码块执行,且是最早执行。
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值