第十讲 面向对象基础——继承

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yerenyuan_pku/article/details/81953722

继承的概述

  1. 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承单独的那个类即可;
  2. 多个类可以称为子类,单独这个类称为父类或者超类;
  3. 子类可以直接访问父类中的非私有的属性和行为。对于父类中私有的部分,子类对象是无法直接访问的;
  4. 通过extends关键字让类与类之间产生继承关系,如class SubDemo extends Demo{}

这里用一个例子来让我们走近继承。例,将学生(Student)和工人(Worker)的共性描述提取出来,单独进行描述,只要让学生和工人与单独描述的这个类有关系,就可以了。

// 将学生和工人的共享代码向上抽取到一个共性的类型中,这个类型中即包括学生和工人
class Person // 父类(超类或基类)
{
    String name;
    int age;
}

// 描述学生,属性:姓名、年龄,行为:学习。让学生和Person产生关系,就可以让学生使用Person中的共性的内容。
// 通过一个关键字extends继承。
class Student /*子类*/ extends Person
{
    // String name;
    // int age;
    void study()
    {
        System.out.println("good good");
    }
}

// 描述工人,属性:姓名、年龄,行为:工作
class Worker extends Person
{
    // String name;
    // int age;
    void work()
    {
        System.out.println("hard");
    }
}

class ExtendsDemo 
{
    public static void main(String[] args) 
    {
        Student s = new Student();
        s.name = "小明";
        s.age = 14;
        s.study();
    }
}

通过以上的例子,就可知道继承的好处了。

继承的好处

  1. 提高了代码的复用性,即多个类相同的成员可以放到同一个类中;
  2. 提高了代码的维护性,即如果功能的代码需要修改,修改一处即可;
  3. 让类与类之间产生了关系,有了这个关系,才有了多态的特性,其实这也是继承的一个弊端——类的耦合性很强;
  4. 设计原则:高内聚低耦合
    我们可以简单理解为:内聚就是自己完成某件事情的能力,耦合就是类与类之间的关系。我们在设计时候的原则是:自己能完成的就不麻烦别人,这样将来别人产生了修改,就对我的影响较小。由此可见,在开发中使用继承其实是在使用一把双刃剑。今天我们还是以继承的好处来使用,因为继承还有很多其他的特性。

继承的特点

Java只支持单继承,不支持多继承

Java语言中,Java只支持单继承(一个类只能有一个父类),不支持多继承(一个类可以有多个父类)。因为多继承容易带来安全隐患,当多个父类中定义了相同功能时,当功能内容不同时,子类对象不确定要运行哪一个。举例说明如下:

class A {
    void show() {
        System.out.println("a");
    }
}
class B {
    void show() {
        System.out.println("b");
    }
}
class C extends A, B {
    C c = new C();
    c.show(); // 此时该运行哪一个类中的方法呢?出现了调用的不确定性,因为方法的主体不同。
}

但是Java保留了这种机制,并用另一种体现形式来完成表示——(多实现),后面回讲。

Java支持多层继承(继承体系)

Java支持多层继承,也就是一个继承体系。

  • 那么如何使用一个继承体系中的功能呢?
    想要使用体系,先查阅体系父类的描述,因为父类中定义的是该体系中的共性功能,通过了解共性功能,就可以知道该体系的基本功能,那么这个体系就可以基本使用了。
  • 那么在具体调用时,要创建最子类的对象,为什么呢?
    一是因为有可能父类不能创建对象(如抽象类),二是创建子类对象可以使用更多的功能,包括基本的也包括特有的。
    简而言之,查阅父类功能,创建子类对象使用功能。

子父类出现后,类成员的特点

类中成员有:

  1. 成员变量;
  2. 成员函数;
  3. 构造函数。

当子父类出现后,代码上有一些什么特点呢?

子父类中的成员变量

如果子类中出现非私有的同名成员变量时,子类要访问本类中的变量用this,子类要访问父类中的同名变量用super,super的使用和this的使用几乎一致。

  • this代表的是本类对象的引用;
  • super代表的是父类的那片空间,并不代表父类对象的引用。

首先观察如下代码,并试着运行一下,你会得到什么结果呢?

class Fu
{
    int num = 4;
}

class Zi extends Fu
{
    int num2 = 5;
    void show()
    {
        int num3 = 6;
        System.out.println("num = " + num);
        System.out.println("num2 = " + num2);
        System.out.println("num3 = " + num3);
    }
}

class ExtendsDemo2 
{
    public static void main(String[] args) 
    {
        Zi z = new Zi();
        z.show();
    }
}

从运行结果来看,上述程序代码在内存中是怎么体现的呢?
这里写图片描述
但是当子父类中出现了同名的成员变量时,如下:

class Fu
{
    int num = 4;
}

class Zi extends Fu
{
    int num = 5;
    void show()
    {
        int num = 6;
        System.out.println("num = " + super.num);
    }
}

class ExtendsDemo2 
{
    public static void main(String[] args) 
    {
        Zi z = new Zi();
        z.show();
    }
}

此时以上程序代码在内存中的体现大致是如下图:
这里写图片描述

子父类中的函数

  1. 当子类出现和父类一模一样的函数时,当子类对象调用该函数时,会运行子类函数的内容,如同父类的函数被覆盖一样,这种情况是函数的另一种特性:重写(覆盖);
  2. 父类中的私有方法不可以被覆盖;
  3. 在子类覆盖方法中,继续使用被覆盖的方法可以通过super.函数名获取。

首先观察如下代码,并试着运行一下,你会得到什么结果呢?

class Fu
{
    void show()
    {
        System.out.println("fu show run");
    }
}

class Zi extends Fu
{
    void show1()
    {
        System.out.println("zi show run");
    }
}

class ExtendsDemo3 
{
    public static void main(String[] args) 
    {
        Zi z = new Zi();
        z.show();
        z.show1();
    }
}

知道运行后的结果,我们应该可以画出这样一个内存调用图来:
这里写图片描述
现在我们还是回到对子父类中的函数的讨论中,当子类继承父类,沿袭了父类的功能,子类虽然具备了该功能,但是功能的内容要和父类不一致时,没有必要定义新功能,而是使用覆盖特性,保留父类的功能定义,并重写功能内容。

class Fu {
    void show() {
        System.out.println("fu show");
    }

    void speak() {
        System.out.println("vb");
    }
}
class Zi extends Fu {

    void speak() {
        System.out.println("java");
    }

    public void show() {
        System.out.println("zi show");
    }
}

class ExtendsDemo3 {
    public static void main(String[] args) {
        Zi z = new Zi();
        z.speak();
    }
}

使用函数覆盖这一特性时,要特别注意如下两点:

  1. 子类方法覆盖父类方法,必须要保证权限大于等于父类权限;
  2. 静态只能覆盖静态,或者被静态覆盖。

子父类中的构造函数

在对子类对象进行初始化时,父类的构造函数也会运行,那是因为子类的构造函数默认第一行有一条隐式的语句:super(),它访问父类中空参数的构造函数,而且子类中所有的构造函数默认第一行都是super()。那么为什么子类对象初始化一定要去访问父类中的构造函数呢? 因为父类中的数据,子类可以直接获取,所以子类对象在建立时,需要先查看父类是如何对这些数据进行初始化的,所以子类在对象初始化时,要先访问一下父类中的构造函数。如果要访问父类中指定的构造函数,可以通过手动定义super语句的方式来指定。我们应该特别注意以下两点:

  1. 当父类中没有空参数构造函数时,子类需要通过显示定义super语句指定要访问的父类中的构造函数;
  2. 用来调用父类构造函数的super语句在子类构造函数中必须定义在第一行,因为父类的初始化要先完成。

这里就会产生一个问题:this和super都是用于调用构造函数,它们可以同时存在吗?答案是不可以,因为它们只能定义在第一行,所以说有了this语句,就没有super语句,有了super语句,就没有this语句了。
试着运行如下程序代码,你会得到什么结果呢?

class Fu { // extends object
    int num;
    Fu() {
        num = 60;
        System.out.println("fu run");
    }
    Fu(int x) {
        System.out.println("fu..."+x);
    }
}
class Zi extends Fu {
    Zi() {
        System.out.println("zi run");
    }
    Zi(int x) {
        this(); // 此构造函数没有super语句
        System.out.println("zi...."+x);
    }
}
class ExtendsDemo4 {
    public static void main(String[] args) {
        Zi z = new Zi(0);
        System.out.println(z.num);
    }
}

输出结果:

fu run
zi run
zi….0
60

从上述例子的运行结果,可以知道子类的实例化过程,即子类的所有的构造函数,默认都会访问父类中空参数的构造函数。因为子类中每一个构造函数的第一行都有一个隐式super();当父类中没有空参数的构造函数时,子类必须手动通过super语句形式来指定要访问的父类中的构造函数;当然,子类的构造函数第一行也可以手动指定this语句来访问本类中的构造函数,子类中至少会有一个构造函数会访问父类中的构造函数。

继承的注意事项

  1. 子类不能从父类继承私有成员(成员方法和成员变量),但是子类的对象是包括子类所不能从父类中继承的私有成员的。其实这也体现了继承的另一个弊端:打破了封装性;
  2. 子类不能继承父类的构造方法,但是可以通过super关键字去访问父类构造方法;
  3. 千万不要为了获取其他类的功能,简化代码而继承,必须是类与类之间有所属关系才可以继承,所属关系是:is a。

什么时候定义继承呢?

当事物之间存在所属(is a)关系时,可以通过继承来体现这个关系。例,xxx是yyy的一种,用代码体现就是:xxx extends yyy。
类与类之间除了有继承关系外,还有聚集(聚合、组合)的关系,所属关系是:has a。

  • 聚合:(举例)球员与球队的关系;
  • 组合:事物的联系程度更紧密,(举例)人的心脏和手。

final关键字

final:最终,作为一个修饰符。

  1. 可以修饰类、函数、变量;
  2. 被final修饰的类不可以被继承。为了避免被继承,被子类覆写功能;
  3. 被final修饰的方法不可以被覆写;
  4. 被final修饰的变量是一个常量,只能被赋值一次,而且后面需要跟一个值,不然编译失败!既可以修饰成员变量,又可以修饰局部变量。当在描述事物时,一些数据的出现,值是固定的,那么这时为了增强阅读性,都给这些值起个名字,方便于阅读,而这个值不需要改变,所以用final修饰,作为常量,常量的书写规范,所有字母都大写,如果由多个单词组成,单词间通过_连接;
  5. 内部类定义在类中的局部位置上时,只能访问该局部被fianl修饰的局部变量。

    class Demo {
        final int x = 3;
        // 全局常量
        public static final double MY_PI = 3.14; // 相当于加了一个锁
    
        final void show1() { // 被final修饰的方法不可以被覆写
    
        }
        void show2() {
            final int y = 4;
            // y = 4; // 被final修饰的变量是一个常量,只能被赋值一次,之后不允许被修改
            System.out.println(3.14);
        }
    }
    class SubDemo extends Demo {
        // void show1() {
    
        // }
    }

猜你喜欢

转载自blog.csdn.net/yerenyuan_pku/article/details/81953722