4.6 类的继承

Posted weststar

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4.6 类的继承相关的知识,希望对你有一定的参考价值。

一、继承的特点

??Java继承通过关键字extends关键字来实现,实现继承的类被称为子类,被继承的类称为父类,有时也成为基类、超类。父类和子类是一种特殊的关系。因为子类是一种特殊的父类,因此父类的范围比子类范围大,所以可认为父类是大类,而子类是小类。
Java里子类继承父类的语法格式:

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

注:(1)Java使用extends作为关键字,extends关键字在英文中的意思是扩展,而不是继承。该关键字很好地体现了子类和父类之间的关系:子类是父类的扩展,子类是一种特殊的父类。
(2)国内翻译为继承有一定理由:子类继承了父类的全部成员变量、方法、内部类。
(3)Java子类不能获得父类的构造器。

子类只能从被扩展的父类获取成员变量、方法、内部类,不能获取构造器和初始化块。
例子:先定义了一个Fruit类

class Fruit 
{
    //实例变量
    protected double weight;
    //类变量
    protected static String kind="水果";
    public void info()
    {
        System.out.println("我是一个水果,重:"+weight);
    }
}

再定义一个继承Fruit类的Apple类,程序如下:

public class Apple extends Fruit 
{
    public static void main(String[] args) 
    {
        //创建Apple对象
        var p=new Apple();
        //访问父类静态成员变量
        System.out.println(Apple.kind);
        //因为Fruit类有weight实例变量,因此可以通过Apple对象来访问weight成员变量
        p.weight=12;
        p.info();
    }
}
---------- 运行Java捕获输出窗 ----------
水果
我是一个水果,重:12.0

输出完成 (耗时 0 秒) - 正常终止

上面的Apple类只是一个空类,它包含一个main方法,但程序中创建Apple对象后,可以访问Apple对象的weight实例变量和info()方法,者表明Apple对象也具有weight实例变量和info()方法。
Java摒弃了C++难以理解的多继承,即每个类只能有一个直接父类。
Java只能有一个直接父类,可以有多个间接父类。

class A{...}
calss B extends A{...}
class C extends B{...}

上面的程序可以看出C的直接父类是B,间接父类有A,Object.
如果定义一个Java类时并未显示指定直接父类,这个类的默认扩展java.lang.Object。因此java.lang.Object类是所有类的父类。

二、父类方法重写

大部分子类都是以父类为基础,额外增加了新的成员变量和方法。但有时需要重写父类的方法。
例如:鸟都会飞,其中鸵鸟是一种特殊的鸟,但不会飞.

public class Bird
{
    public void fly()
    {
        System.out.println("飞~飞~飞");
    }
}

再定义一个继承鸟类的鸵鸟类

class Ostrich extends Bird 
{
    public void fly()
    {
        System.out.println("鸵鸟不会飞,只能跑");
    }
    public static void main(String[] args)
    {
        var os=new Ostrich();
        os.fly();
    }
}
---------- 运行Java捕获输出窗 ----------
鸵鸟不会飞,只能跑

输出完成 (耗时 0 秒) - 正常终止

??这种父类与子类同名方法的现象叫做方法重写(Override),也被称为方法覆盖。
注意:方法重写遵循“两同两小一大”
★“两同”:方法名相同、形参列表相同
★“两小”:子类方法的返回值类型应该比父类方法的返回值类型更小或者相等,子类方法声明抛出的异常类比父类声明抛出的异常类更小或相等。
★“一大”:子类方法的访问权限比父类更大或者相等。
被覆盖的方法和覆盖的方法要么都是类方法,要么都是实例方法。下面代码将出现编译错误:

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

当子类覆盖了父类的方法后,子类将无法访问父类中被覆盖的方法,但可以再在子类中调用父类中被覆盖的方法。如果需要在子类中调用父类中的方法,则可以使用super(被覆盖的是实例方法)或者父类方法(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
如果父类具有private修饰的方法,该方法对子类是隐藏的,因此子类无法访问该方法,也无法重写该方法。如果在子类创建一个与父类private方法相同的方法名、形参列表、返回值类型的方法,依然不是重写,只是在一个父类中定义了一个新方法。
@Override可以帮我们检查一个方法是不是重写。如:我们错误把fly的字母l写成数字1了

class Ostrich extends Bird 
{
    @Override
    public void f1y()//Ostrich.java:3: 错误: 方法不会覆盖或实现超类型的方法
    {
        System.out.println("鸵鸟不会飞,只能跑");
    }
    public static void main(String[] args)
    {
        var os=new Ostrich();
        os.fly();
    }
}

推荐重写父类方法时,使用@Override。

三、super限定

如果需要在父类调用父类被覆盖的实例方法,则可以使用super限定来调用父类被覆盖的实例方法。
例如上面的Ostrich类中去调用父类的fly()方法:

class Ostrich extends Bird 
{
    @Override
    public void fly()
    {
        System.out.println("鸵鸟不会飞,只能跑");
        
    }
    public void callOverridedMethod()
    {
        super.fly();
    }

    public static void main(String[] args)
    {
        var os=new Ostrich();
        os.fly();
        os.callOverridedMethod();
                //super.fly();错误看下面“注”
    }
}
---------- 运行Java捕获输出窗 ----------
鸵鸟不会飞,只能跑
飞~飞~飞

输出完成 (耗时 0 秒) - 正常终止

上面程序借助callOverridedMethod()方法就可以在Ostrich对象既可以调用自己重写的fly()方法,也可以调用Bird类中被覆盖的fly()方法。
注:
super是Java提供的关键字,super用于限定该对象调用它从父类得到的实例变量或方法。正如this不能出现在static修饰的方法一样,super也不能出现在static修饰的方法一样中。static修饰的方法属于类,该方法调用者可能是一个类,而不是对象,因而super失去了意义。

★如果在子类中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是自己该类自己定义的实例变量。
★如果子类定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形。子类方法里定义的方法直接访问该实例变量默认访问到子类中定义的实例变量,无法访问到父类被隐藏的实例变量。在子类定义的实例方法可以通过super来访问父类中被隐藏的实例变量。

class BaseClass 
{
    protected int a=5;//父类实例变量
}
public class SubClass extends BaseClass
{
    private int a=7;//子类中的实例变量
    public void test()
    {
        int a=9;//局部变量
        System.out.println(a);
        System.out.println(this.a);
        System.out.println(super.a);
    }
    public static void main(String[] args)
    {
        SubClass sc=new SubClass();
        sc.test();
    }
}
---------- 运行Java捕获输出窗 ----------
9
7
5

输出完成 (耗时 0 秒) - 正常终止

上面的程序BaseClass和SubClass中定义了名为a的实例变量,则SubClass的a实例变量将会隐藏BaseClass的实例变量a。当系统创建SubClass对象分配两块内存,一块用于存储SubClass类中定义的a实例变量,一块用于存储从BaseClass类继承得到的实例变量a。
如果在某个方法内访问名为a的成员变量,但没有显示指定调用者,则系统查找a的顺序为:
(1)查找该方法中是否有名为a的局部变量。
(2)查找当前类中是否有名为a的成员变量。
(3)查找直接父类中是否有名为a的成员变量,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的成员变量,则系统会出现编译错误。

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

class Parent 
{
    public String tag="疯狂Java讲义";//1
}

class Derived extends Parent
{
    //定义一个私有的tag实例变量来隐藏父类的tag实例变量
    private String tag="轻量级Java EE企业级应用实战";//2
}

public class HideTest
{
    public static void main(String[] args)
    {
        var d=new Derived();
        //程序不可访问d的私有变量tag,下面语句将出错
        //System.out.println(d.tag);//3  HideTest.java:18: 错误: tag 在 Derived 中是 private 访问控制
        //将d变量显示地向上转型为Parent后,即可访问tag实例变量
        System.out.println(((Parent)d).tag);//4
    }
}
---------- 运行Java捕获输出窗 ----------
疯狂Java讲义

输出完成 (耗时 0 秒) - 正常终止

在程序代码2处定义了一个实例变量,子类定义的这个实例变量将会隐藏父类中的tag实例变量。
程序在mian()方法中先创建了一个Derived对象,该对象将会保存两个tag实例变量,一个是在Parent类中定义的tag实例变量,一个实在Derived类中定义的tag实例变量。内存中的存储示意图:
技术图片

四、调用父类的构造器

??子类不会获取父类的构造器,但子类构造器里可以调用父类构造器初始胡代码块。
★在一个构造器中调用另外一个重载构造器使用this来完成,在子类中调用父类构造器用super调用来完成。

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(size,name);
        this.color=color;
    }

    public static void main(String[] args)
    {
        var s=new Sub(5.6,"测试对象","红色");
        System.out.println(s.size+"--"+s.name+"--"+s.color);
    }
}
---------- 运行Java捕获输出窗 ----------
5.6--测试对象--红色

输出完成 (耗时 0 秒) - 正常终止

从上面的程序可以看出,super调用和this调用的区别在于super调用的是其父类的构造器,而this调用的是同一个类中重载的构造器,因此使用super调用父类的构造器必须出现在子类构造器执行体的第一行。
子类构造器总会调用父类构造器一次。子类调用父类构造器分为几种情况:
★子类构造器执行体第一行使用super显示调用父类构造器,系统将根据super传入的实参列表调用父类对应的构造器。
★子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中的构造器时也会先调用父类构造器。
★子类构造器执行体既没有super调用,也没有this调用,系统将在执行子类构造器之前,隐式调用父类的构造器。
??不管哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会上溯执行其父类构造器······依次类推,创建任何Java对象,最想执行的总是java.lang.Object类的构造器。
如下图所示的继承树:
技术图片
如果创建Class B对象,系统将先执行java.lang.Object类的构造器,再执行Class A的构造器,然后再执行Class B的构造器。
例子:

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

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带两个参数的构造器,"+"该动物的name为:"+name+",其年龄为:"+age);
    }
}

public class Wolf extends Animal
{
    public Wolf()
    {
        //显示调用父类有两个参数的构造器
        super("灰太狼",3);
        System.out.println("Wolf无参数的构造器");
    }
    public static void main(String[] args)
    {
        new Wolf();
    }
}
---------- 运行Java捕获输出窗 ----------
Creatrue无参数的构造器
Animal带一个参数的构造器,该动物的name为:灰太狼
Animal带两个参数的构造器,该动物的name为:灰太狼,其年龄为:3
Wolf无参数的构造器

输出完成 (耗时 0 秒) - 正常终止

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

以上是关于4.6 类的继承的主要内容,如果未能解决你的问题,请参考以下文章

java 代码片段

web代码片段

Java 枚举类的基本使用

类的继承与派生

从 BeautifulSoup 4.6 中的两个 HTML 标签之间提取 HTML

类的继承