📜  用子类引用子类对象 vs 父类引用

📅  最后修改于: 2020-03-23 01:42:49             🧑  作者: Mango

先决条件: 继承
在Java中,所有非静态方法都基于基础对象的运行时类型,而不是指向该对象的引用的类型。因此,在对象的声明中使用哪种类型都没有关系,其行为将相同。

如何引用子类对象

有两种方法可以引用子类对象。两者都具有相对于其他的一些优点/缺点。声明在编译时可见的方法上可见。

  1. 第一种方法(使用超类引用进行引用):超类的引用变量可用于引用从该超类派生的任何子类对象。如果方法存在于超类中,但被子类重写,则它将被执行。
  2. 第二种方法(使用子类引用进行引用):子类引用可用于引用其对象。

考虑一个说明这两种方法的示例。

// Java展示引用子类
// 基类
class Bicycle
{
    // Bicycle类有两个fields
    public int gear;
    public int speed;
    // Bicycle构造器
    public Bicycle(int gear, int speed)
    {
        this.gear = gear;
        this.speed = speed;
    }
    // Bicycle三个方法
    public void applyBrake(int decrement)
    {
        speed -= decrement;
    }
    public void speedUp(int increment)
    {
        speed += increment;
    }
    // toString()打印Bicycle信息
    public String toString()
    {
        return("齿轮数 "+gear
                +"\n"
                + "速度 "+speed);
    }
}
// 测试代码
class MountainBike extends Bicycle
{
    // MountainBike子类增加了一个field
    public int seatHeight;
    // MountainBike子类构造函数
    public MountainBike(int gear,int speed,
                        int startHeight)
    {
        // 调用基类构造器
        super(gear, speed);
        seatHeight = startHeight;
    }
    // MountainBike增加了一个方法
    public void setHeight(int newValue)
    {
        seatHeight = newValue;
    }
    // 重写toString()打印更多信息
    @Override
    public String toString()
    {
        return (super.toString()+
                "\n座位高度 "+seatHeight);
    }
}
// 测试代码
public class Test
{
    public static void main(String args[])
    {
        // 使用子类引用
        // 方法1
        Bicycle mb2 = new MountainBike(4, 200, 20);
        // 使用子类引用
        // 方法2
        MountainBike mb1 = new MountainBike(3, 100, 25);
        System.out.println("第一个座位高度 "
                                            + mb1.seatHeight);
        
        System.out.println(mb1.toString());
        System.out.println(mb2.toString());
        /* 如下语句不可用,因为Bicycle
        没有定义变量seatHeight.
        // System.out.println("第二个座位高度 is "
                                                + mb2.seatHeight); */
        /* 如下语句不可用,因为Bicycle
       没有定义设定座位高度setHeight()方法.
        mb2.setHeight(21);*/
    }
}

输出:

第一个座位高度 25
齿轮数 3
速度 100
座位高度 25
齿轮数 4
车速 200
座位高度 20

以上程序说明:

  • 创建了MountainBike类的对象,该类通过使用子类引用“ mb1″进行引用。使用此引用,我们将可以访问超类或子类定义的对象的两个部分(方法和变量)。请参阅下图以了解清楚。
    MountainBike mb1 = new MountainBike(3, 100, 25);

  • 现在,我们再次创建MountainBike类的对象,但是这次使用超级类Bicycle引用’mb2’对其进行引用。使用此引用,我们将有机会给那些超类中定义的对象(方法和变量)。
    Bicycle mb2 = new MountainBike(4, 200, 20);

  • 由于引用“ mb1″可以访问字段“ seatHeight”,因此我们在控制台上打印此内容。
    System.out.println("seat height of first bicycle  is " + mb1.seatHeight);
    
  • 如果超类中存在方法,但被子类重写,并且创建了子类的对象,则无论我们使用什么引用(子类或超类),它将始终是要执行的子类中的被重写方法。因此,以下两个语句将调用MountainBike类的toString()方法。
    System.out.println(mb1.toString());
    System.out.println(mb2.toString());
  • 由于’mb2’的引用类型为Bicycle,因此在下面的语句中将得到编译时错误。
    System.out.println("seat height of second bicycle  is " + mb2.seatHeight);
    
  • 同样,’mb2’所做的引用是Bicycle类型的,因此在下面的语句中我们将得到编译时错误。
    mb2.setHeight(21);

    使用类型转换

    在上面的示例中,我们已经看到,通过使用Bicycle类型的引用’mb2’,我们无法调用特定于子类的方法或访问子类字段。使用Java中的类型转换可以解决此问题。例如,我们可以声明另一个引用,例如MountainBike类型的“ mb3″,并使用类型转换将其分配给“ mb2″。

    // 声明MountainBike引用
    MountainBike mb3;
    // 适用类型转换,把mb3赋给mb2.
     mb3 = (MountainBike)mb2;

    因此,现在以下语句有效。

    f(double): 6.3
    f(double): 6.6

    而不是假定的输出:

    f(int): 6
    f(double): 6.6

    重载不适用于C++编程语言中的派生类。编译器查看派生类的范围,找到单个函数“ double f(double)”并调用它。它永远不会干扰基类的(封装)范围。在C++中,没有作用域之间的重载,派生类作用域不是该一般规则的例外。

  • 现在考虑该程序的Java版本:
    class Base
    {
        public int f(int i)
        {
            System.out.print("f (int): ");
            return i+3;
        }
    }
    class Derived extends Base
    {
        public double f(double i)
        {
            System.out.print("f (double) : ");
            return i + 3.3;
        }
    }
    class myprogram3
    {
        public static void main(String args[])
        {
            Derived obj = new Derived();
            System.out.println(obj.f(3));
            System.out.println(obj.f(3.3));
        }
    }

    上面程序的输出是:

    f (int): 6
    f (double): 6.6

    因此,在Java中,重载可在与C++相反的方式进行。Java编译器根据用于调用方法的参数类型,确定要在编译时执行的重载方法的正确版本,并根据这两个类的重载方法的参数,接收调用中使用的参数的值并执行重载方法。
    最后,让我们尝试以下C#程序的输出:

    using System;
    class Base
    {
        public int f(int i)
        {
            Console.Write("f (int): ");
            return i + 3;
        }
    }
    class Derived : Base
    {
        public double f(double i)
        {
            Console.Write("f (double) : ");
            return i+3.3;
        }
    }
    class MyProgram
    {
        static void Main(string[] args)
        {
            Derived obj = new Derived();
            Console.WriteLine(obj.f(3));
            Console.WriteLine(obj.f(3.3));
            Console.ReadKey(); // write this line if you use visual studio
        }
    }

    注意:Console.ReadKey()用于暂停控制台。它类似于C / C++中的getch。
    上面程序的输出是:

    f(double) : 6.3
    f(double):  6.6

    而不是假设的输出

    f(int) : 6
    f(double) : 6.6

    原因与C++程序中说明的相同。像C++一样,在父类和派生类之间没有重载解析。在C#中,没有作用域之间的重载,派生类作用域也不是该一般规则的例外。这与C++相同,因为C#语言的创建者Anders hejlsberg认为,C#的设计与C++更加接近。