面向对象 05:三大特性之——继承,继承在 Java 中的相关使用,区分关键字 super 和 this,方法重写的注意点

一、前言

记录时间 [2024-05-13]

系列文章简摘:
面向对象 01:Java 面向对象相关内容整体概述
面向对象 02:区分面向过程与面向对象,类和对象的关系
面向对象 03:类与对象的创建、初始化和使用,通过 new 关键字调用构造方法,以及创建对象过程的内存分析
面向对象 04:三大特性之——封装,封装的含义和作用,以及在 Java 中的使用方式,附完整的测试案例代码

更多 Java 相关文章,请参考专栏哦。

本文讲述面向对象编程的三大特性之——继承。通过案例分析,讲述了继承的概念,及其在 Java 中的相关使用。此外,文章详细介绍了关键字 super 和 this 的区别,还有方法重写的注意点。


面向对象编程(Object-Oriented Programming, OOP)的三大特性是封装、继承和多态,这三大特性是 OOP 的基础,为设计灵活、可维护和可扩展的软件系统提供了核心机制。

这三个特性共同构成了面向对象编程的基础,使开发者能够设计出高内聚、低耦合的软件系统,促进了软件工程的高效管理和复杂问题的有效解决。

  • 高内聚:一个模块内部各个组成部分(如类的方法、函数、变量等)之间应该紧密关联,共同完成一个具体且单一的功能,不允许外部干涉。
  • 低耦合:各模块之间的依赖关系应该尽可能地减少和简化,一个模块应尽量少地依赖其他模块,模块之间的接口应清晰、简单,且只暴露必要的功能给外部使用。

二、什么是继承

1. 概述

继承是面向对象编程中的一个基本概念,它允许创建一个新类(称为子类、派生类或扩展类)来继承现有类(称为父类、基类或超类)的特性和行为。例如,学生属于人类,老师也属于人类,那么学生和老师作为子类,都继承了人类这个父类。

继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。

在 Java 语言中,只允许一个类直接继承自另一个类,这是单继承

子类继承父类,使用 extends 关键字。即,子类是父类的扩展。

继承应当用来表达类与类之间的 is-a 关系,即,子类是父类的一种特殊类型。

除继承外,类与类之间的关系还有依赖、组合、聚合等等。

继承的主要目的是促进代码的复用性和模块化设计,从而减少代码重复并提高程序的可维护性。设计时应当谨慎使用继承,避免滥用导致体系结构过于复杂,提倡使用组合和接口作为替代方案,以保持设计的灵活性和可维护性。


2. 基本特征

继承的基本特征主要有如下这些:

  • 属性和方法的继承:子类自动获得父类中所有非私有publicprotected)的属性和方法。这意味着子类可以使用或重写这些特性而无需重新实现它们。
  • 代码重用:通过继承,子类可以重用父类的代码,无需从头开始编写相同的功能,这样可以减少开发时间和潜在的错误。
  • 层次结构:继承有助于建立类的层次结构,使得类之间的关系更加清晰,反映出现实世界中的对象分类。
  • 多态性:继承是实现多态性的基础,即子类对象可以被视为父类对象使用,这样可以在不修改现有代码的情况下引入新的子类对象。
  • 类型兼容性:在大多数面向对象的编程语言中,子类对象被视为是其父类类型的对象,这称为向上转型 upcasting
  • 构造函数与初始化:尽管子类继承父类的属性和方法,但构造函数不会被继承。子类构造函数可以通过 super 关键字(或相关语言的等价物)显式或隐式地调用父类的构造函数来初始化父类的成员。
  • 访问权限:继承还涉及到访问权限控制,比如 Java 中的 publicprotectedprivate 修饰符,决定了哪些成员在继承体系中是可见和可访问的。

3. 案例分析

在 Java 中,子类继承父类,使用 extends 关键字。

下面使用动物类、狮子、长颈鹿,这三个类的关系,模拟 Java 中的继承。

动物类作为父类 / 基类;狮子、长颈鹿作为两个子类,都继承动物类:

// Animal:父类
public class Animal {
    
}

// Lion:子类 / 派生类
public class Lion extends Animal {
    
}

// Giraffe:子类 / 派生类
public class Giraffe extends Animal {
    
}

三、如何使用继承

1. 子类继承父类

子类自动获得父类中所有非私有publicprotected)的属性和方法。

例如,在 Animal 类中编写一个方法 shout(),其子类 LionGiraffe 也可以使用这个方法。

Animal 类中编写一个属性 category,其子类 LionGiraffe 也可以使用属性。

// Animal:父类
public class Animal {
    
    // 父类中的属性
    public String category = "animal";
    
    // 父类中的方法
    public void shout() {
        System.out.println("the animal is shouting");
    }
    
}

使用方式:

public class Application {
    public static void main(String[] args) {

        Lion lion = new Lion();

        // 子类继承父类的方法
        lion.shout();

        // 子类继承父类的属性
        System.out.println(lion.category);

    }
}

2. 封装私有属性

但是,当父类中的属性是私有 private 时,子类就无法使用了。方法同理。

私有的东西无法被继承。

例如,当 Animal 类中的属性 categoryprivate 时,其子类 LionGiraffe 便无法使用。

解决方式:

根据封装思想,我们可以为属性 category 预留一些可操作的方法,比如 GetterSetter 方法。

private String category = "animal";

public String getCategory() {
    return category;
}

public void setCategory(String category) {
    this.category = category;
}

3. Object 类

在面向对象编程中,继承树描述了一个或多个类之间的继承关系,形象地展示了类的层次结构。每个节点代表一个类,根节点通常是抽象类或者基类,没有父类,而叶子节点是具体类,不再有子类继承。

IDEA 中,通过快捷键 Ctrl+H 可以快速查看继承树。

在 Java 中,所有类都默认直接或间接地继承自 Object 类。

Object 类是 Java 类层次结构的根类,它位于 java.lang 包中。如果在定义一个类时没有明确指定其父类,那么这个类将自动继承 Object 类。

Object 类提供了许多通用的方法,这些方法在很多类中都会用到,例如:

  • toString():返回该对象的字符串表示。
  • equals(Object obj):比较两个对象是否相等。
  • hashCode():返回该对象的哈希码值。
  • getClass():返回此 Object 的运行时类。
  • clone():创建并返回此对象的一个副本。
  • notify(), notifyAll(), wait():用于线程间的通信。
  • finalize():在垃圾回收器确定不再有对该对象的引用之前,由对象的垃圾回收器调用。

由于所有类都是 Object 的子类,因此任何对象都可以调用 Object 类中的这些方法。如果需要,子类可以重写这些方法以提供更具体的实现。


四、关键字 super

1. 概述

在 Java 中,super 是一个关键字,用于引用父类(基类)的成员变量、方法或构造函数。

主要用于以下几个场景:

  • 访问父类的成员变量: 当子类中定义了与父类同名的成员变量(即变量重写或遮蔽),想要在子类中访问父类的该变量时,可以使用 super.变量名 的形式。
  • 调用父类的方法: 如果子类重写了父类的方法,但又想在子类方法内部调用父类中被重写的方法,可以使用 super.方法名(参数列表)
  • 调用父类的构造函数: 在子类的构造函数中,通过 super(参数列表) 形式调用父类的特定构造函数。这必须是子类构造函数中的第一个语句,并且只能出现一次。这样做是为了确保父类的成员被正确初始化。

2. 注意事项

正确使用 super 可以帮助实现代码的清晰性和逻辑的正确性,特别是在复用和扩展父类功能的场景下。

  • 使用 super基于继承关系的,只有在子类中才有意义。
  • super 不仅可以访问直接父类的成员,理论上可以通过多层继承链,间接访问到更上层祖先类的成员,只要这些成员在访问权限允许的范围内。
  • superthis 有相似之处,但 this 用于引用当前对象或当前类的成员,而 super 则用于引用父类的成员。
  • 当没有明确指定调用父类构造函数时,编译器会自动插入一个对父类无参构造函数的调用。如果父类没有无参构造函数,子类构造函数必须显式调用带有适当参数的父类构造函数。

3. 案例分析

在子类中访问父类变量

在子类中访问父类变量,使用 super.变量名 的形式;访问子类内部变量,使用 this.变量名 的形式。

接下来使用上面的 Animal 类,及其子类 Lion 编写测试案例。

Step 1:在父类 Animal 中新增 protected 属性 name

protected String name = "Animal";

Step 2:在子类 Lion 中新增 private 属性 name,以及测试方法 testName(String name)

// Lion:子类 / 派生类
public class Lion extends Animal {
    
    private String name = "Lion";

    public void testName(String name) {
        System.out.println(name);
        System.out.println(this.name);
        System.out.println(super.name);
    }

}

Step 3:在测试类中进行测试

Lion lion = new Lion();
lion.testName("test");

Step 4:得到结果

# test 是测试类传入方法的局部变量
test

# Lion 是 this 关键字指向的当前类中的变量
Lion

# Animal 是 super 关键字指向的父类中的变量
Animal

在子类中访问父类方法

在子类中访问父类方法,使用 super.方法名(参数列表) 形式;访问子类内部方法,使用 this.方法名(参数列表) 的形式。

接下来使用上面的 Animal 类,及其子类 Giraffe 编写测试案例。

Step 1:在父类 Animal 中新增方法 print()

public void print() {
	System.out.println("父类打印内容:Animal");
}

Step 2:在子类 Giraffe 中同样新增方法 print(),以及测试方法 testPrint()

// Giraffe:子类 / 派生类
public class Giraffe extends Animal {

    public void print() {
        System.out.println("子类打印内容:Giraffe");
    }

    public void testPrint() {
        print();
        this.print();
        super.print();
    }

}

Step 3:在测试类中进行测试

Giraffe giraffe = new Giraffe();
giraffe.testPrint();

Step 4:得到结果如下,进一步说明了使用好关键字,可以很好地避免混淆和歧义。

# print(); 语句打印的内容,调用了子类的方法
子类打印内容:Giraffe

# this.print(); 语句打印的内容,调用了子类的方法
子类打印内容:Giraffe

# super.print(); 语句打印的内容,调用了父类的方法
父类打印内容:Animal

子类调用父类构造

在子类的构造函数中,通过 super() 形式调用父类的无参构造函数;通过 super(参数列表) 形式调用父类的有参构造函数。

  • 当没有明确指定调用父类构造函数时,编译器会自动插入一个对父类无参构造函数的调用。
  • 如果父类没有无参构造函数,子类构造函数必须显式调用带有适当参数的父类构造函数。

调用父类构造函数,必须是子类构造函数中的第一个语句,并且只能出现一次。

接下来通过案例进行分析:

Step 1:给父类 Animal 显式地定义一个无参构造函数

public Animal() {
    System.out.println("Animal 无参构造执行了");
}

Step 2:新建子类 Dog,继承父类 Animal,在 Dog 中显式地定义一个无参构造函数

public class Dog extends Animal{
    public Dog() {
        System.out.println("Dog 无参构造执行了");
    }
}

Step 3:在测试类中实例化 Dog 对象,并进行测试

Dog dog = new Dog();

Step 4:得到结果,发现子类和父类的无参构造方法都执行了

# 得到结果
# 发现子类和父类的无参构造方法都执行了
Animal 无参构造执行了
Dog 无参构造执行了

说明子类实例化时,会默认调用父类的构造方法。

相当于在子类构造方法中,隐藏了代码 super();

public Dog() {
    // 隐藏代码,调用了父类的无参构造

    super();    // 调用父类构造器,必须在子类构造器的第一行
    System.out.println("Dog 无参构造执行了");
}

4. super 和 this

下面是有关 super 的注意点,以及 superthis 的区别。

super 注意点:
    1. super 调用父类的构造方法,必须在构造方法的第一个
    2. super 必须只能出现在子类的方法或者构造方法中
    3. super 和 this 不能同时调用构造方法

VS this:
    代表的对象不同
        this: 本身调用者这个对象
        super: 代表父类对象的应用
    前提
        this: 没有继承也可以使用
        super: 只能在继承条件才可以使用
    构造方法
        this(): 本类的构造
        super(): 父类的构造

五、方法重写

1. 概述

方法重写(Override)是面向对象编程中的一个重要概念。它允许子类提供一个与其父类具有相同名称、返回类型和参数列表的方法实现。当子类的对象调用这个方法时,将执行子类中重写后的方法,而不是父类中的原始版本。这为多态性提供了基础,使得子类能够表现出与父类不同的行为。

方法重写应用场景:接口、抽象类等。

要进行方法重写,需要遵循以下规则:

  • 方法名、返回类型和参数列表必须与父类方法完全匹配
  • 访问权限不能比父类中被重写的方法更严格
    • 如果父类中的方法是 public 的,子类中的重写方法不能是 privateprotected
    • 子类可以增加访问权限,比如将父类的 protected 方法改为 public
  • 静态方法不能被重写
    • 静态方法属于类,而不是实例;
    • 即使子类定义了一个与父类同名的静态方法,这也被认为是两个独立的方法,而不是重写。
  • 最终方法(final methods)不能被重写
    • 父类中被声明为 final 的方法表示其设计为不允许子类改变其行为。
  • @Override 注解:虽然不是强制性的,但在方法声明前添加 @Override 注解是一个好习惯。编译器会检查是否正确地重写了父类的方法,如果方法签名不匹配,编译时会报错。

2. 静态方法不能重写

在 Java 中,静态方法不能被重写的原因主要在于静态方法是属于类本身的,而非实例,因此不参与面向对象的多态行为。

即使子类定义了一个与父类同名的静态方法,这也被认为是两个独立的方法,而不是重写。

下面通过一个具体的例子来说明这一点:

假设有一个父类 Parent,其中包含一个静态方法 printMessage(),以及一个子类 Child,也在尝试定义一个同名的静态方法 printMessage()

编写父类 Parent

class Parent {
    public static void printMessage() {
        System.out.println("这是父类的静态方法");
    }
}

编写子类 Child

class Child extends Parent {
    public static void printMessage() {
        System.out.println("这是子类的静态方法");
    }
}

编写测试类

  • 静态方法可以直接通过类名调用
  • 通过对象实例调用,尽管不推荐,但也是可行的
public class Main {
    public static void main(String[] args) {
        
        // 直接通过类名调用
        Parent.printMessage(); 		// 输出 "这是父类的静态方法"
        Child.printMessage(); 		// 输出 "这是子类的静态方法"

        // 通过对象实例调用,尽管不推荐,但也是可行的
        // 通过父类引用指向子类实例
        Parent parentInstance = new Child();
        parentInstance.printMessage(); 		// 依然输出 "这是父类的静态方法",说明不是重写行为
    }
}

从上面的例子可以看出,尽管 Child 类定义了自己的 printMessage() 静态方法,但这并不构成重写,因为当通过父类引用指向子类实例并调用静态方法时,打印的是父类的静态方法内容

这表明静态方法的调用是基于其所属类的类型,而非实例的运行时类型,证明了静态方法的调用是在编译时期就已经确定的,即静态绑定

当直接通过子类引用调用时,会调用子类的静态方法,但如果希望通过父类引用访问到子类的静态方法,这是不可能的,这进一步说明静态方法不支持多态性


3. 常规方法重写

子类重写父类的方法,一定是非静态的。

假设有一个父类 Mother,其中包含一个非静态方法 printMessage(),以及一个子类 Kid,也在尝试定义一个同名的非静态方法 printMessage()

当通过父类引用指向子类实例并调用方法时,打印的是子类的方法内容,说明构成了方法重写。

class Mother {
    public void printMessage() {
        System.out.println("这是父类的静态方法");
    }
}

class Kid extends Mother {
    // 方法快速重写:右键 Generate ==> Override Methods
    @Override	// 注解,有功能的注释
    public void printMessage() {
        System.out.println("这是子类的静态方法");
    }
}

public class OverrideNotStatic {
    public static void main(String[] args) {

        // 通过对象实例调用,通过父类引用指向子类实例
        Mother motherInstance = new Kid();

        // 输出 "这是子类的静态方法",子类重写了父类的方法
        motherInstance.printMessage();
    }
}

4. 方法重写注意点

下面是有关方法重写注意点:

重写: 需要有继承关系,子类重写父类的方法
    1. 方法名必须相同
    2. 参数列表列表必须相同
    3. 修饰符: 范围可以扩大但不能缩小     public > protected > default > private
    4. 抛出的异常: 范围可以被缩小,但不能扩大     Exception(大) --> ClassNotFoundException(小)

重写,子类的方法和父类必要一致,方法体不同。

为什么需要重写:
    1.父类的功能,子类不一定需要,或者不一定满足

六、总结

本文讲述面向对象编程的三大特性之——继承。通过案例分析,讲述了继承的概念,及其在 Java 中的相关使用。此外,文章详细介绍了关键字 super 和 this 的区别,还有方法重写的注意点


一些参考资料

狂神说 Java 零基础:https://www.bilibili.com/video/BV12J41137hu/
TIOBE 编程语言走势: https://www.tiobe.com/tiobe-index/
Typora 官网:https://www.typoraio.cn/
Oracle 官网:https://www.oracle.com/
Notepad++ 下载地址:https://notepad-plus.en.softonic.com/
IDEA 官网:https://www.jetbrains.com.cn/idea/
Java 开发手册:https://developer.aliyun.com/ebook/394
Java 8 帮助文档:https://docs.oracle.com/javase/8/docs/api/

  • 36
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值