继承、super、this、抽象类

2020-09-28 14:26:19   撰写:云陌

 

今日内容:

  • 三大特性——继承

  • 方法重写

  • super关键字

  • this关键字

  • 抽象类

 

一、继承

1.1、继承概述

多个类中存在相同的属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需重复的顶一个这些属性和行为,只需要继承抽取出来的这个类即可。

生活中的继承关系图举例:

 

 

其中多个类可以称为子类,单独的那一个类一般称为父类、超类(super class)或者称为基类

继承表述的是事物之间的所属关系,这种关系是is-a的关系,例如图中兔子是属于食草动物,食草动物又属于动物。因此课件,父类更加通用,子类更加具体。我们通过继承的方式,可以使多种事物产生一个关系体系。

定义:

  • 继承:就是子类继承父类的属性行为,使得子类对象具有父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。

好处:

  • 1、提高了代码的复用性
  • 2、类与类之间产生了关系,是多态的前提

 

1.2、继承的格式

通过extends关键字,可以声明一个子类继承另外一个父类,具体的定义格式如下:

class 父类 {
...
}
class 子类 extends 父类 {
...
}

继承代码演示,如下:

public class ExtendsDemo1 {
    public static void main(String[] args) {
        // 创建一个讲师类对象
        Teacher teacher = new Teacher();
        // 为员工类的name进行赋值操作
        teacher.name = "小明"; // 调用的是teacher类中继承自父类Employee中的name属性
        // 通过teacher调用员工的printName()方法
        teacher.printName();  // teacher 自带的方法,不是继承的。事物的专属特性。
        // 通过teacher调用员工的work()方法
        teacher.work(); // teacher类继承自父类的方法
    }
}
class Employee {
    String name;  // 定义name属性

    // 定义员工的工作方法
    public void work() {
        System.out.println("尽心尽力在工作");
    }
}

class Teacher extends Employee {
    /**
     * String name;
     *
     * public void work() {
     *     System.out.println("尽心尽力在工作");
     * }
     */
    public void printName() {
        System.out.println("name:" + name);
    }
}

1.3、继承后的特点——成员变量

当我们类之间产生了关系之后,其中各类的成员变量,又产生了哪些影响呢?

成员变量不重名

如果子类父类中出现补充的成员变量,这时访问时没有影响的。代码:

public class ExtendsDemo2 {
    public static void main(String[] args) {
        // 创建一个子类对象
        Zi z = new Zi();
        // 调用子类中的show()方法
        z.show();
    }
}

class Fu {
    // 父类中的成员变量
    int num = 5;
}
class Zi extends Fu {
    // Zi类中的成员变量
    int num2 = 6;
    // Zi类中的成员方法
    public void show() {
        // 访问父类中的num
        System.out.println("Fu num: " + num); // 继承而来,就可以直接访问

        // 访问子类中的num2
        System.out.println("Zi num=" + num2);
    }
}

成员变量重名

如果子类父类中出现重名的成员变量,这个时候,我们的访问就会受到影响。代码如下:

public class ExtendsDemo3 {
    public static void main(String[] args) {
        // 创建一个子类对象
        Zi1 z = new Zi1();
        // 调用子类的show方法
        z.show();
    }
}
class Fu1 {
    // Fu类中的成员变量
    int num = 5;
}

class Zi1 extends Fu1{
    // Zi类中的成员变量
    int num = 6;

    // Zi类中的方法
    public void show () {
        // 访问父类中的num
        System.out.println("Fu num: " + num); // 继承而来,就可以直接访问

        // 访问子类中的num2
        System.out.println("Zi num=" + num);
    }
}

子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量是,需要使用super的关键字,修饰父类的成员变量,类似于我们之前学习的this关键字。

 

使用格式:

super.父类的成员变量名;

子类的方法需要修改,代码如下:

public class ExtendsDemo3 {
    public static void main(String[] args) {
        // 创建一个子类对象
        Zi1 z = new Zi1();
        // 调用子类的show方法
        z.show();
    }
}
class Fu1 {
    // Fu类中的成员变量
    int num = 5;
}

class Zi1 extends Fu1{
    // Zi类中的成员变量
    int num = 6;

    // Zi类中的方法
    public void show () {
        // 访问父类中的num
        System.out.println("Fu num: " + super.num); // 继承而来,就可以直接访问

        // 访问子类中的num2
        System.out.println("Zi num=" + this.num);
    }
}

 

Fu1类中的成员变量是非私有的,子类中可以直接访问。若Fu1类的成员变量私有化,子类是不能直接访问的。通常我们在编码时,我们遵循封装的原则,使用private修饰成员变量,如果访问父类的私有成员变量呢?可以在父类中提供公共getXxx()方法和setXxx()方法。

 

1.4、继承后的特点——成员方法

 

当类之间产生了关系,其中各类中的成员方法,又馋了哪些影响呢?

成员方法不重名

如果子类父类中出现不重名的成员方法,这时对我们的调用时没有影响的。对象调用方法的同时,会优先在子类中查找是否有对应的方法,若子类中存在就会执行子类中的方法,如果子类中没有这样的方法就会执行父类中相应的方法。

class Fu {
    public void show() {
        System.out.println("父类中的show方法");
    }
}
class Zi extends Fu {
    public void show2 () {
        System.out.println("子类中的show2的方法");
    }
}
public class ExtendsDemo4 {
    public static void main(String[] args) {
        // 创建子类对象
        Zi zi = new Zi();
        // 子类中没有show方法,但是可以找到父类方法去执行
        zi.show();
        zi.show2();
    }
}
成员方法重名——重写(Override)

如果子类父类中出现了重名的成员方法,这时的访问是一种特殊情况,叫做方法重写(Override)

  • 方法重写:子类中出现与父类一模一样的方法时(返回值类型,方法名,参数列表都相同),会出现覆盖效果,这种方式也被称为重写或者复写。声明不变,重新实现
class Fu {
    public void show() {
        System.out.println("父类中的show方法");
    }
}
class Zi extends Fu {
    @Override
    public void show () {
        System.out.println("Zi show");
    }

    public void show2 () {
        System.out.println("子类中的show2的方法");
    }
}
public class ExtendsDemo4 {
    public static void main(String[] args) {
        // 创建子类对象
        Zi zi = new Zi();
        // 子类中没有show方法,但是可以找到父类方法去执行
        zi.show();
        zi.show2();
    }
}
重写的应用

子类可以根据需要,定义属于自己的特殊的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现了父类的方法,从而进行了扩展的增强。比如新的手机,增加了来电显示头像的功能。代码:

public class ExtendsDemo5 {
    public static void main(String[] args) {
        // 创建一个子类对象
        NewPhone phone = new NewPhone();
        // 调用父类继承而来的方法
        phone.call();
        phone.sendMessage();
        // 调用子类重写的方法
        phone.showNum();
    }
}
class Phone {
    public void sendMessage () {
        System.out.println("发短信");
    }
    public void call () {
        System.out.println("打电话");
    }
    public void showNum() {
        System.out.println("显示来电号码");
    }
}

class NewPhone extends Phone {
    // 重写父类的来电显示号码功能,并且增加自己的显示姓名,图片功能
    public void showNum () {
        // 调用父类已经存在功能
        super.showNum();
        // 增加自己特有的显示姓名和图片功能
        System.out.println("显示来电姓名");
        System.out.println("显示头像");
    }
}

小贴士:这里写重写时,用到的super.父类的成员方法,表示调用父类的成员方法。

 

注意事项:

1、子类方法覆盖父类的方法,必须保证权限大于等于父类的权限

2、子类方法覆盖父类的方法,返回值、函数名、参数列表都要一摸一样。

 

 

1.5、继承后的特点——构造方法

 

当类之间产生了关系,其中各类中的构造方法,又产生了哪些影响呢?

首先我们要回忆两个事项:构造方法的定义格式和作用。

1、构造方法的名字和类名是一致的。所以子类是无法继承父类的构造方法的。

2、构造方法的作用是用于初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构造方法中默认有一个super(),表示调用父类的构造方法,当父类的成员变量进行初始化之后,才能够给子类使用。代码如下:

class Fu {
    private int n;
    Fu() {
        System.out.println("Fu");
    }
}
class Zi extends Fu {
    Zi() {
        // 调用父类的构造方法
        super();
        System.out.println("Zi");
    }
}
public class ExtendsDemo6 {
    public static void main(String[] args) {
        Zi zi = new Zi();
    }
}

 

1.6、super和this

父类空间优先于子类对象产生

在每次创建子类对象时,先初始化父类空间,再创建子类对象本身。目的是在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类的成员。代码体现在子类的构造方法调用时,一定要先调用父类的构造方法。理解图:

 

super和this的含义

  • super:代表父类的存储空间标识可以理解为父类的引用
  • this:代表当前对象的引用(谁调用它就代表谁)。

super和this的用法

  • 1、访问成员
  • 2、访问构造方法
 

子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。

super()和this()都必须是在构造方法的第一行,所以他们不能同时出现。

 

1.7、继承的特点

  • 1、Java只支持单继承,不支持多继承。
  • 2、Java支持多层继承(继承体系)。
  • 3、子类和父类是一种相对的概念。

顶层父类是Object类,所有的类默认继承Object,作为父类。

一个类既可以继承其他类,也可以被其他类继承

class A{}

class B extends A {}

class C extends B {}

 

二、抽象类

2.1、抽象类概述

父类中的方法,被子类重写,子类的实现都各不相同,父类的方法声明和方法主题,只有声明还有意义,而我们父类的方法主题已经没有存在的意义了。我们把这样没有方法主题的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。

定义:

  • 抽象方法:没有方法体的方法。
  • 抽象类: 包含了抽象方法的类。
 

2.2、abstract关键字使用格式

抽象方法

使用abstract关键字修饰方法,该方法就成为了抽象方法,抽象方法质保函一个方法名,而没有具体的方法体。

定义格式:

修饰符 abstract 返回值类型 方法名(参数列表);

代码举例:

public abstract void run();

抽象类:

如果一个类包含抽象方法,那么该类必须是抽象类:

定义格式:

abstract class ClassName {
// 内容主题
}

举例:

public abstract class Animal {
public abstract void run();
}

抽象类的使用:

继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。就必须有子类实现父类的抽象方法。否则,从最初的父类到最终的子类都不能创建对象,失去意义。

代码举例:

public class Cat extends Animal {
@Override
public void run () {
System.out.println("小猫喜欢再墙角走一走");
}
}

具体使用案例:

public class ExtendsDemo1 {
    public static void main(String[] args) {
        // 创建一个子类对象
        Cat cat = new Cat();
        // 使用变量名调用run()方法
        cat.run();
    }
}
abstract class Animal {
    public abstract void run();
}
class Cat extends Animal {
    @Override
    public void run() {
        System.out.println("小猫喜欢再墙头走一走");
    }
}

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,称为方法实现(实现方法)。

 

2.3、注意事项:

关于抽象类的时候,以下内容主要为语法中需要注意的细节

  • 1、抽象类不能创建对象,如果创建对象,则编译无法通过,只能创建其非抽象子类的对象

理解:假设创建了抽象类的对象,调用抽象的方法,而丑行类没有具体的方法体,没有意义的。

  • 2、抽象类中,可以有构造方法,是供子类创建对象是,初始化父类对象成员使用的。

理解:子类的构造方法中有默认的super(),需要访问父类的构造方法。

  • 3、抽象类中,不一定包含抽象方法,但是有抽象方法的类必然是一个抽象类。

理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类的对象,通常用于某些特殊的类结构设计。

  • 4、抽象类的子类,必须重写抽象类父类中的所有的抽象方法,否则,编译无法通过进行报错,除非子类也是抽象类。

理解:假设不重写抽象方法,则类中可能包含抽象方法。那么创建对象后调用抽象的方法,没有任何意义,因为方法没有方法体。

 

三、继承的综合案例

3.1、综合案例:群主发普通红包

群主发普通红包,某个群有很多成员,群主给发了普通红包,普通红包的规则:

1、群主的一笔金额,从群主的余额中扣除,平均分成多分,让成员领取。

2、成员领取红包之后,保存在成员的余额中。

根据当前的描述,完成案例中所有的类的定义以及制定类之间的继承关系,并完成发红包的操作。

 

3.2、案例分析

根据描述分析,得出继承体系:

3.3、案例实现

定义用户类:

public class User {
    // 成员变量
    private String username;  // 用户名
    private double leftMonry;  // 余额

    public User(){ }

    public User(String username, double leftMonry) {
        this.username = username;
        this.leftMonry = leftMonry;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public double getLeftMonry() {
        return leftMonry;
    }

    public void setLeftMonry(double leftMonry) {
        this.leftMonry = leftMonry;
    }

    public void show () {
        System.out.println("用户名:"+ username + ", 余额为:" + leftMonry + "元");
    }
}

public class QunZhu extends User {

    public QunZhu() {
    }

    public QunZhu(String username, double leftMonry) {
        // 通过super调用父类的构造方法
        super(username, leftMonry);
    }

    /**
     * 群主发红包,就是将一个整数的金额,分成若干等份。
     * 1、获取群主的余额,是否能够发出这个红包,不能就返回null,并提示
     * 2、修改群主的余额。
     * 3、拆分红包
     *      3.1、如果能够整除,那么久平均分。
     *      3.2、如果说不能整数,那么久把余数最大的分给最后一份。
     */

    public ArrayList<Double> send (int money, int count) {
        // 获取群主的余额1
        double leftMoney = getLeftMonry();
        if (money > leftMoney) {
            return null;
        }

        // 修改群主余额
        setLeftMonry(leftMoney - money);

        // 创建一个集合保存等份的金额
        ArrayList<Double> list = new ArrayList<>();

        // 扩大100倍,相当于折算为"分"为单位。避免小数已婚算损失精度的问题
        money = money * 100;

        // 每份的金额
        int m = money / count;

        // 不能整除的余数
        int l = money % count;

        // 无论能否整除,n-1份都是每份的等额金额
        for (int i = 0; i < count - 1; i++) {
            // 缩小100被,折算成“元”
            list.add(m / 100.0);
        }

        // 判断是否整除
        if (l == 0) {
            // 能整除,最终一份金额,和之前的金额一致。
            list.add(m / 100.0);
        } else {
            list.add((m+1) / 100.0);
        }
        return list;
    }
}

public class Member extends User{
    public Member() {
    }

    public Member(String username, double leftMonry) {
        super(username, leftMonry);
    }

    // 打开红包,就是从集合中,随机取出一份,保存到自己的余额中
    public void openHongbao(ArrayList<Double> list) {
         // 创建 Random 对象
        Random random = new Random();
        // 随机生成一个下标
        int index = random.nextInt(list.size());
        // 移除一个金额
        Double money = list.remove(index);
        // 直接调用父类的方法,设置到余额中
        setLeftMonry(money);
    }
}

public class ExtendsTest {
    public static void main(String[] args) {
        // 创建群主对象
        QunZhu qunZhu = new QunZhu("群主", 200);

        // 创建一个键盘录入
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入金额:");
        int money = scanner.nextInt();
        System.out.println("请输入个数:");
        int count = scanner.nextInt();

        // 发送红包
        ArrayList<Double> sendList = qunZhu.send(money, count);

        // 判断,如果余额不足的情况怎么办
        if (sendList == null) {
            System.out.println("余额不足...");
            return;
        }

        // 创建三个成员
        Member m = new Member();
        Member m1 = new Member();
        Member m2 = new Member();

        // 打开红包
        m.openHongbao(sendList);
        m1.openHongbao(sendList);
        m2.openHongbao(sendList);

        // 展示信息
        qunZhu.show();
        m.show();
        m1.show();
        m2.show();
    }
}

课后作业:请大家扩展思维完成作业。

案例扩展:

1、如果成员的余额不为0,将如何处理?

2、如果群主想输入带小数的金额,又将如何处理?

3、红包个数,不能满足当前人数的需要,如何处理?