JAVA面向对象 类的继承

版权声明:本文为博主原创文章,未经博主允许可以转载,但请保留原文链接。 https://blog.csdn.net/tmdlife/article/details/52039170

本页面更新日期: 2016年07月30日

前言

继承是面向对象的三大特征之一.
也是实现软件复用的重要手段.
Java继承具有单继承的特点, 每个子类只有一个直接父类.

继承的特点

Java的继承通过extends关键字实现.
实现继承的类被称为子类.
被继承的类被称为父类.
父类和子类的关系, 是一种一般和特殊的关系.
例如水果和苹果的关系, 苹果继承了水果, 苹果是水果的子类, 水果是苹果的父类.

Java里子类继承父类的语法格式如下:

修饰符 class SubClass extends SuperClass
{
    //类定义部分
}

从上面的语法格式来看, 定义子类的语法非常简单, 只需要在原来的类定义上增加 extends SuperClass 即可.
即表明该类是 SuperClass 的子类.

为什么国内把 extends 翻译为 继承 而不是 扩展呢?
除了历史原因, 还有一点.
子类继承了父类, 也将获得父类的全部成员变量和方法.
这与我们现实中子辈从父辈那里获得一笔财富的继承关系很像.
但是, Java的子类不能继承父类的构造器.

下面写个程序示范子类继承父类.

public class Fruit
{
    public double weight;
    public void info()
    {
        System.out.println("我是一个水果! 重:" + weight + "g!");
    }
}

接下来定义该Fruit类的子类Apple

public class Apple extends Fruit
{
    public static void main(String[] args)
    {
        //创建Apple对象
        Apple a = new Apple();
        //Apple 对象本身没有 weight 成员变量
        //因为Apple 父类有 weight 成员变量, 所以也可以访问 Apple 对象的 weight 成员变量.
        a.weight = 56;
        //调用 Apple 对象的 info() 方法
        a.info();
    }
}

上面的 Apple 类只是一个空类, 它只包含了一个 main() 方法.
但程序中创建了 Apple 对象之后, 可以访问该 Apple 对象的 weight 实例变量和info()方法, 这表明 Apple 对象也具有了 weight 实例变量和 info() 方法, 这就是继承的作用.

Java类虽然只能有一个直接父类, 但它可以有无限多个间接父类.
例如:

class Fruit extends Plant{...}
class Apple extends Fruit{...}
......

上面类定义中, Fruit 是 Apple 类的父类.
Plant 类也是 Apple 类的父类.
区别是 Fruit 是 Apple 的直接父类, 而 Plant 则是 Apple 类的间接父类.

如果定义一个 Java类时, 并未显式指定这个类的直接父类.
则这个类默认继承 java.lang.Object 类.
因此可以得出, java.lang.Object 类时所有类的父类.
要么是直接父类, 要么是其间接父类.
因此, 所有的Java对象都可以调用 java.lang.Object 类所定义的实例方法.
关于这个很牛逼的类我们以后会有介绍.

重写父类的方法

子类继承了父类, 所以说子类是一个特殊的父类.
大部分时候, 子类总是以父类为基础.
额外增加新的成员变量和方法.
但有一种情况例外: 子类需要重写父类的方法.
例如鸟类都包含了飞翔的方法, 但其中的鸵鸟并不会飞, 因为鸵鸟是鸟的子类, 因此它将从鸟类中获得飞翔的方法, 但这个飞翔的方法显然不适合鸵鸟, 所以鸵鸟这个子类需要重写鸟类(父类)的方法.

下面先定义一个 Bird 类

public class Bird
{
    //Bird 类的 fly() 方法
    public void fly()
    {
        System.out.println("我在天空自由自在的飞翔...啦啦啦");
    }
}

下面定义一个 Ostrich 类, 这个类继承了 Bird 类, 同时重写 Bird 类的 fly() 方法.

public class Ostrich
{
    //重写 Bird 类的  fly() 方法
    public void fly()
    {
        System.out.println("NND, 我可飞不了, 虽然我有双翅膀, 啦啦啦");
    }
    public static void main(String[] args)
    {
        //创建 Ostrich 对象
        Ostrich os = new Ostrich();
        //执行 Ostrich 对象的 fly() 方法, 将会输出 "...飞不了..."
        os.fly();
    }
}

执行上面的程序, 将看到执行 os.fly() 时执行的不是 Bird 类的 fly() 方法.
而是执行 Ostrich 类的 fly() 方法.

这种子类包含与父类同名方法的现象称为方法重写(Override). 也被称为方法覆盖.
可以说子类重写了父类的方法, 也可以说子类覆盖了父类的方法, 都行.

方法的重写要遵循两同两小一大规则.

  • 两同: 方法名相同 / 形参列表相同
  • 两小: 子类方法返回值类型应比父类方法返回值类型小或相等. / 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等.
  • 一大: 子类方法的访问权限应比父类方法的访问权限大或相等.

尤其需要指出, 覆盖方法和被覆盖方法要么都是类方法, 要么都是实例方法.
不能一个是类方法, 一个是实例方法, 例如下面代码就会报错.

class BaseClass
{
    public static void test(){...}
}
class SubClass extends BaseClass
{
    public void test(){...}
}

当子类覆盖了父类方法后, 子类的对象将无法访问父类中被覆盖的方法.
但可以在子类方法中调用父类中被覆盖的方法.
如果需要在子类方法中调用父类中被覆盖的方法, 则可以使用super(被覆盖的是实例方法) 或者 父类类名(被覆盖的是类方法) 来作为调用者, 调用父类中被覆盖的方法.

如果父类方法具有 private 访问权限, 则该方法对其子类是隐藏的.
因此子类无法访问该方法, 也就无法重写该方法.
如果子类中定义了一个与父类 private 方法具有相同的方法名 / 相同的形参列表 / 相同的返回值类型的方法, 依然不是重写.
这只是在子类中重新定义了一个新的方法.
例如下面代码时完全正确的.

class BaseClass
{
    //test() 方法是 private 访问权限, 子类不可访问该方法
    private void test(){...}
}
class SubClass extends BaseClass
{
    //此处并不是方法重写, 所以可以增加 static 关键字
    public static void test(){...}
}

涨姿势:
方法重载和方法重写在英文中分别是 overload 和 override
重载和重写 并不是同一种东西, 虽然二者都是发生在方法之间, 并要求方法名相同之外, 并没有太大相似之处.
因为重载主要发生在同一个类的多个同名方法之间.
而重写发生在子类和父类的同名方法之间.
当然, 父类方法和子类方法之间也有可能发生重载, 因为子类会获得父类方法.
如果子类定义了一个与父类方法有相同方法名, 但参数列表不同的方法, 就会形成父类方法和子类方法的重载.

super 限定

如果需要在子类方法中调用父类被覆盖的实例方法.
则可以使用 super 限定来调用父类被覆盖的实例方法.
为上面的 Ostrich 类添加一个方法, 在这个方法中调用 Bird 类中被覆盖的 fly 方法.

public void callOverrideMethod()
{
    //在子类方法中通过 super 显式调用父类被覆盖的实例方法
    super.fly();
}

super 是 Java提供的一个关键字, super 用于限定该对象调用它从父类继承得到的实例变量或方法.
正如 this 不能出现在 static 修饰的方法中一样, super 也不能出现在 static 修饰的方法中.
static 修饰的方法是属于类的.
该方法的调用者可能是一个类, 而不是对象, 因而 super 限定也就失去了意义.

如果在构造器中使用 super
则 super 用于限定该构造器初始化的是该对象从父类继承得到的实例变量, 而不是该类自己定义的实例变量.

如果子类定义了和父类同名的实例变量.
则会发生子类实例变量隐藏父类实例变量的情形.
在正常情况下, 子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量.
无法访问到父类中被隐藏的实例变量.
在子类定义的实例方法中可以通过 super 来访问父类中被隐藏的实例变量.
如下代码所示:

class BaseClass
{
    public int a = 5;
}
public class SubClass extends BaseClass
{
    public int a = 7;
    public void accessOwner()
    {
        System.out.println(a);
    }
    public void accessBase()
    {
        //通过使用 super 来限定访问从父类继承得到的 a 的实例变量
        System.out.println(super.a);
    }
    public static void main(String[] args)
    {
        SubClass sc = new SubClass();
        sc.accessOwner(); //输出 7
        sc.accessBase(); //输出 5
    }
}

上面程序的 BaseClass 和 SubClass 中都定义了名为 a 的实例变量.
则 SubClass 的 a 实例变量将会隐藏BaseClass 的 a 实例变量.
当系统创建了 SubClass 对象时, 实际上会为 SubClass 对象分配两块内存.

  • 一块用于存储在 SubClass 类中定义的 a 实例变量.
  • 一块用于存储从 BaseClass 类继承得到的 a 实例变量.

如果子类里没有包含和父类同名的成员变量.
那么在子类实例方法中访问该成员变量时, 则无需显式使用 super 或 父类名作为调用者.
如果在某个方法中访问名为 a 的成员变量, 但没有显式指定调用者, 则系统查找 a 的顺序为:

  • 查找该方法中是否有名为 a 的局部变量.
  • 查找当前类中是否包含名为 a 的成员变量.
  • 查找 a 的直接父类中是否包含名为 a 的成员变量, 依次上溯 a 的所有父类. 直到 java.lang.Object 类.
  • 如果最终不能找到名为 a 的成员变量, 则系统出现编译错误.

如果被覆盖的是类变量, 在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量.

涨姿势:
当程序创建一个子类对象时.
系统不仅会为该类中定义的实例变量分配内存.
也会为它从父类继承得到的所有实例变量分配内存.
即使子类定义了与父类中同名的实例变量.
也就是说, 当系统创建一个 java 对象时.
如果该 java 类有两个父类(一个直接父类 A / 一个间接父类 B)
假设 A 类中定义了 2 个实例变量, B 类中定义了 3 个实例变量.
当前类中定义了 2 个实例变量, 那么这个 java 对象会保存 2 + 3 + 2 个实例变量.

因为子类中定义与父类中同名的实例变量并不会完全覆盖父类中定义的实例变量, 它只是简单的隐藏了父类中实例变量, 所以会出现如下特殊情况:

class Parent
{
    public String tag = "孙悟空";
}
class Derived extends Parent
{
    //定义一个私有的 tag 实例变量来隐藏父类的 tag 实例变量
    private String tag = "猪八戒";
}
public class HideTest
{
    public static void main(String[] args)
    {
        Derived d = new Derived();
        //程序不可访问 d 的私有变量 tag , 所以下面语句将引起编译错误
        //System.out.println(d.tag);
        //将 d 变量显式的向上转型为 Parent 后, 即可访问 tag 实例变量
        //程序将输出 孙悟空
        System.out.println(((Parent)d).tag);
    }
}

上面程序父类 Parent 定义了一个 tag 实例变量.
其子类 Derived 定义了一个 private 的 tag 实例变量.
子类中定义的这个实例变量将会隐藏父类中定义的 tag 实例变量.

程序的入口 main() 方法中先创建了一个 Derived 对象.
这个 Derived 对象将会保存两个 tag 实例变量.
一个是在 Parent 中定义的 tag 实例变量.
一个是在 Derived 类中定义的 tag 实例变量.
此时程序中包括了一个 d 变量.
它引用一个 Derived 对象, 内存中的存储示意图如下:

这里写图片描述

接着, 程序将 Derived 对象赋给 d 变量.
接着, 程序试图通过 d 来访问 tag 实例变量, 程序将提示访问权限不允许.
接着, 将 d 变量强制向上转型为 Parent 类型.
再通过它来访问 tag 实例变量是允许的.

调用父类构造器

子类不会获得父类的构造器.
但子类构造器里可以调用父类构造器的初始化代码.
类似于前面所介绍的一个构造器调用另一个重载的构造器.

在一个构造器中调用另一个重载的构造器使用 this 调用来完成.
在子类构造器中调用父类构造器使用 super 调用来完成.

看下面程序定义了 Base 类 和 Sub 类, 其中 Sub 类是 Base 类的子类.
程序在 Sub 类的构造器中使用 super 来调用 Base 类的构造器初始化代码.

class Base
{
    public double size;
    public String name;
    public Base(double size, String name)
    {
        this.size = size;
        this.name = name;
    }
}
public class Sub extends Base
{
    public String color;
    public Sub(double size, String name, String color)
    {
        //通过 super 调用来调用父类构造器的初始化过程
        super(size, name);
        this.color = color;
    }
    public static void main(String[] args)
    {
        Sub s = new Sub(5.6, "皮卡丘", "黄色");
        //输出 Sub 对象的 三个实例变量
        System.out.println(s.size + "--" + s.name + "--" + s.color);
    }
}

从上面程序中不难看出, 使用 super 调用和使用 this 调用也很像.
区别在于 super 调用的是其父类的构造器, 而 this 调用的是同一个类中重载的构造器.
因此, 使用 super 调用父类构造器也必需出现在子类构造器执行体的第一行.
所以 this 调用 和 super 调用不会同时出现.

不管是否使用 super 调用来执行父类构造器的初始化代码.
子类构造器总会调用父类构造器一次.
子类构造器调用父类构造器分如下几种情况:

  • 子类构造器执行体的第一行使用 super 显式调用父类构造器.系统将根据 super 调用里传入的实参列表调用父类对应的构造器.
  • 子类构造器执行体的第一行代码使用 this 显式调用本类中重载的构造器,系统将根据 this 调用里传入的实参列表调用本类中的另一个构造器.执行本类中另一个构造器时即会调用父类构造器.
  • 子类构造器执行体中既没有 super 调用, 也没有 this 调用, 系统将会在执行子类构造器之前, 隐式调用父类无参数的构造器.

不管上面哪种情况, 当调用子类构造器来初始化子类对象时.
父类构造器总会在子类构造器之前执行:
不仅如此, 执行父类构造器时, 系统会再次上溯执行其父类构造器……以此类推.
创建任何 Java对象, 最先执行的总是 java.lang.Object 类的构造器.

对于如下图所示的继承树.
如果创建 ClassB 的对象, 系统将先执行 java.lang.Object 类的构造器.
再执行 ClassA 类的构造器.
然后才执行 ClassB 类的构造器.
这个执行过程还是最基本的情况.
如果 ClassB 显式调用 ClassA 的构造器, 而该构造器又调用了 ClassA 类中重载的构造器, 则会看到 ClassA 两个构造器先后执行的情形.

这里写图片描述

下面程序定义了三个类, 它们之间有严格的继承关系.
通过这种继承关系来让你看看构造器之间的调用关系.

class Creature
{
    public Creature()
    {
        System.out.println("Creature 无参数的构造器");
    }
}

class Animal extends Creature
{
    public Animal(String name)
    {
        System.out.println("Animal 带一个参数的构造器," + "该动物的 name 为:" + name);
    }
    public Animal(String name, int age)
    {
        //使用 this 调用同一个重载的构造器
        this(name);
        System.out.println("Animal 带两个参数的构造器," + "其 age 为:" + age);
    }
}

public class Wolf extends Animal
{
    public Wolf()
    {
        //显式调用父类有两个参数的构造器
        super("大灰狼", 3);
        System.out.println("Wolf 无参数的构造器");
    }
    public static void main(String[] args)
    {
        new Wolf();
    }
}

上面程序的 main 方法只创建了一个 Wolf 对象.
但系统在底层完成了复杂的操作.
运行上面的程序, 看到如下运行结果:

Creature 无参数的构造器
Animal 带一个参数的构造器, 该动物的 name 为大灰狼
Animal 带两个参数的构造器, 其 age 为 3
Wolf 无参数的构造器

从上面的运行过程来看.
创建任何对象总是从该类所在继承树最顶层的类的构造器开始执行.
然后依次向下执行.
最后才执行本类的构造器.
如果某个父类通过 this 调用了 同类中重载的构造器.
就会依次执行此父类的多个构造器.

猜你喜欢

转载自blog.csdn.net/tmdlife/article/details/52039170