为什么我们在Java中将父引用分配给子对象?


79

我在问一个很简单的问题,但是对此我有些困惑。

假设我有一堂课Parent

public class Parent {

    int name;
}

并开设另一堂课Child

public class Child extends Parent{

    int salary;
}

最后是我的Main.java类

public class Main {

    public static void main(String[] args)
    {
        Parent parent = new Child();
        parent.name= "abcd";
    }
}

如果我做一个子对象

Child child = new Child():

然后child对象可以访问两个name and salary变量。

我的问题是:

Parent parent = new Child();

提供name对Parent类的唯一变量的访问。那么这条线的确切用途是什么?

 Parent parent = new Child();

而且当它使用动态多态性时,为什么这样做后子类的变量不可用

Parent parent = new Child();

1
在此处阅读动物示例:en.wikipedia.org/wiki/…–
mrswadge

在您的示例中,它实际上并没有帮助。但是请考虑您有一种对父代引用执行某些操作的方法。例如,在Main类中的某个位置。它需要一个父对象引用并对其进行填充。即使只接受父对象,也可以接受Parent的子类,例如Child对象。此外,请查看动态绑定以获得更多有趣的内容。
AndreiBârsan2012年

这是name正确的类型吗?
罗曼C

Answers:


52

首先,要澄清术语:我们正在将Child对象分配给type变量ParentParent是正好是的子类型的对象的引用Parent,一个Child

它仅在更复杂的示例中有用。想象一下,您将添加getEmployeeDetails到类Parent中:

public String getEmployeeDetails() {
    return "Name: " + name;
}

我们可以重写该方法Child以提供更多详细信息:

@Override
public String getEmployeeDetails() {
    return "Name: " + name + " Salary: " + salary;
}

现在,您可以编写一行代码,获取所有可用细节,无论对象是aParent还是Child

parent.getEmployeeDetails();

如下代码:

Parent parent = new Parent();
parent.name = 1;
Child child = new Child();
child.name = 2;
child.salary = 2000;
Parent[] employees = new Parent[] { parent, child };
for (Parent employee : employees) {
    employee.getEmployeeDetails();
}

将导致输出:

Name: 1
Name: 2 Salary: 2000

我们将Child用作Parent。它具有Child班级特有的特殊行为,但是当我们打电话时,getEmployeeDetails()我们可以忽略差异,而专注于方式ParentChild相似之处。这称为亚型多态性

您更新的问题询问为什么Child.salaryChild对象存储在Parent引用中时无法访问。答案是“多态性”和“静态类型”的交集。因为Java在编译时是静态类型的,所以您会从编译器获得某些保证,但是您必须遵循规则进行交换,否则代码将无法编译。在此,相关保证是,子类型(例如Child)的每个实例都可以用作其超类型(例如Parent)的实例。例如,可以保证在访问时employee.getEmployeeDetailsemployee.name在可以分配给employee类型变量的任何非空对象上定义方法或字段时Parent。为了保证这一点,编译器Parent在决定您可以访问的内容时仅考虑该静态类型(基本上是变量引用的类型)。因此,您无法访问在对象的运行时类型上定义的任何成员Child

当您真正想使用a时ChildParent这是一个易于使用的限制,您的代码将可用Parent于其所有子类型。如果不可接受,请创建引用的类型Child


22

编译程序时,基类的引用变量将获得内存,并且编译器将检查该类中的所有方法。因此,它检查所有基类方法,但不检查子类方法。现在,在运行时创建对象时,只有选中的方法可以运行。如果子类中的方法被覆盖,则函数将运行。子类的其他功能未运行,因为编译器在编译时尚未识别它们。


1
这个答案很有道理!谢谢
user7098526

12

它允许您通过公共父接口访问所有子类。这对于运行所有子类上可用的通用操作很有用。需要一个更好的例子:

public class Shape
{
  private int x, y;
  public void draw();
}

public class Rectangle extends Shape
{ 
  public void draw();
  public void doRectangleAction();
}

现在,如果您有:

List<Shape> myShapes = new ArrayList<Shape>();

您可以将列表中的每个项目都引用为Shape,而不必担心它是Rectangle还是其他类型,例如Circle。您可以一视同仁;您可以全部绘制。您无法调用doRectangleAction,因为您不知道Shape是否真的是矩形。

这是您在以通用方式处理对象与专门处理对象之间进行的交易。

确实,我认为您需要阅读有关OOP的更多信息。一本好书应该会有所帮助:http : //www.amazon.com/Design-Patterns-Explained-Perspective-Object-Oriented/dp/0201715945


2
在引用的示例中,您使用的是“抽象类”还是“接口”?因为简单的类不能有未实现的方法。
HQuser

1
@HQuser-很好发现,似乎想在问题的背景下解释有关多态的更多信息,但是肯定需要修改。
Vivek

11

如果将父类型分配给子类,则表示您同意使用父类的通用功能。

它使您可以自由地从不同的子类实现中进行抽象。因此,您只能使用父功能。

但是,这种分配称为上播。

Parent parent = new Child();  

相反是垂头丧气。

Child child = (Child)parent;

因此,如果您创建实例Child并将其向下转换,则Parent可以使用type属性name。如果您创建的实例,则Parent可以与前面的情况相同,但不能使用,salary因为中没有这样的属性Parent。返回salary仅在向下转换为时可以使用的前一种情况Child

有更多详细说明


因此,我可以做Parent parent = new Parent()。为什么要这样做?请帮我。
Narendra Pal

是的,但仍然无法解释为什么要这么做。
约翰·瓦茨

为什么要这样做取决于您要做什么。
罗曼C

如果分配Parent,则可以使用Parent如果分配(如果可以)Child,则可以使用Child
罗曼C

1
子女=(子女)父母;这将导致ClassCastException。您不能将父对象向下转换为子引用变量。
穆罕默德·萨勒曼·法鲁克

7

这很简单。

Parent parent = new Child();

在这种情况下,对象的类型为Parent。蚂蚁Parent只有一个属性。是name

Child child = new Child();

在这种情况下,对象的类型为Child。蚂蚁Child有两个属性。他们是namesalary

事实是,不需要在声明时立即初始化非最终字段。通常,这是在运行时完成的,因为通常您无法确切知道所需的实现。例如,假设您有一个以类Transport为首的类层次结构。和三个子类:CarHelicopterBoat。还有另一种Tour具有field的类Transport。那是:

class Tour {
   Transport transport;
}  

只要用户尚未预订行程且未选择特定的交通工具,就无法初始化此字段。第一次

其次,假设所有这些类都必须具有一个方法,go()但是具有不同的实现。您可以默认在超类中定义一个基本实现,Transport并在每个子类中拥有自己的唯一实现。通过这种初始化,Transport tran; tran = new Car();您可以调用该方法tran.go()并获得结果,而不必担心特定的实现。它将从特定的子类调用重写的方法。

此外,您可以在使用超类实例的任何地方使用子类实例。例如,您想提供租用交通工具的机会。如果不使用多态,则必须为每种情况编写很多方法:rentCar(Car car)rentBoat(Boat boat)依此类推。同时,多态允许您创建一种通用方法rent(Transport transport)。您可以传入的任何子类的it对象Transport。另外,如果随着时间的流逝,您的逻辑会增加,并且您需要在层次结构中创建另一个类?使用多态时,您无需更改任何内容。只需扩展类Transport并将新类传递给方法即可:

public class Airplane extends Transport {
    //implementation
}

rent(new Airplane())。而new Airplane().go()在第二种情况下。


我知道,但是我的问题是为什么要这样做?如果我们可以做Parent p = new Parent()以获得父引用。
Narendra Pal

这是多态性。非常有用 多态使您的程序更加灵活和可扩展。这到底有什么用?请参阅更新后的答案中的示例。
卡佩尔奇克2012年

1
运输的典范...容易理解。谢谢!
1919年

2

当您有几种实现时,就会发生这种情况。让我解释。假设您有几种排序算法,并且您想在运行时选择要实现的排序算法,或者要给其他人添加其实现的功能。为了解决这个问题,您通常创建一个抽象类(父类),并使用不同的实现(子类)。如果您写:

Child c = new Child();

您将实现绑定到Child类,并且无法再对其进行更改。否则,如果您使用:

Parent p = new Child();

只要Child扩展了Parent,您将来就可以在不修改代码的情况下对其进行更改。

使用接口可以完成相同的事情:父级不再是类,而是Java接口。

通常,您可以在DAO模式中使用此方法,如果您要使用几种依赖数据库的实现。您可以看一下FactoryPatter或AbstractFactory Pattern。希望这可以帮到你。


1

假设您希望有一个Parent类实例数组,以及一组扩展Parent的子类Child1,Child2,Child3。在某些情况下,您只对父类实现感兴趣,这是更通用的,而不关心子类引入的更具体的内容


1

我认为,对于面向对象编程(OOP)的新手来说,以上所有解释都有些过于技术化。几年前,我花了一些时间来解决这个问题(作为Jr Java Developer),我真的不明白为什么我们使用父类或接口来隐藏我们实际上在幕后调用的实际类。

  1. 这样做的直接原因是隐藏复杂性,以使呼叫者不需要经常进行更改(以外行的方式被砍掉和顶住)。这很有道理,特别是如果您的目标是避免创建错误。而且,修改代码的次数越多,您越有可能让其中的一些爬上您的代码。另一方面,如果您只是扩展代码,那么您将很难发现bug,因为您一次只专注于一件事,而您的旧代码不会更改或仅更改一点。假设您有一个简单的应用程序,该应用程序允许医疗行业的员工创建配置文件。为简单起见,假设我们只有全科医生,外科医生和护士(当然,实际上,还有许多更具体的职业)。对于每个职业,您想存储一些常规信息,以及一些专门针对该专业人员的信息。例如,外科医生可以将通用字段(例如firstName,lastName,yearsOfExperience)作为通用字段,但也可以将特定字段(例如,存储在列表实例变量中的专业化字段,如具有类似于“骨外科”,“眼外科”的内容的列表)存储。护士没有任何要求,但可能拥有他们熟悉的清单程序,GeneralPractioners将拥有自己的详细信息。因此,您如何保存详细资料的配置文件。但是,您不希望您的ProfileManager类了解这些差异,因为随着您的应用程序扩展其功能以覆盖更多医学专业(例如,物理治疗师,心脏病专家,肿瘤学家等),它们不可避免地会随着时间的推移而变化和增加。您想要ProfileManger要做的就是说save(),无论它要保存谁的个人资料。因此,通常的做法是将其隐藏在“接口”,“抽象类”或“父类”的后面(如果您计划允许创建一名普通医务人员)。在这种情况下,让我们选择一个Parent类,并将其称为MedicalEmployee。在幕后,它可以引用上述扩展它的任何特定类。当ProfileManager调用myMedicalEmployee.save()时,save()方法将被多态(从结构上)解析为最初用于创建配置文件的正确类类型,例如Nurse,并在其中调用save()方法。类。或家长班(如果您计划允许创建普通医疗人员)。在这种情况下,让我们选择一个Parent类,并将其称为MedicalEmployee。在幕后,它可以引用上述扩展它的任何特定类。当ProfileManager调用myMedicalEmployee.save()时,save()方法将被多态(从结构上)解析为最初用于创建配置文件的正确类类型,例如Nurse,并在其中调用save()方法。类。或家长班(如果您计划允许创建普通医疗人员)。在这种情况下,让我们选择一个Parent类,并将其称为MedicalEmployee。在幕后,它可以引用上述扩展它的任何特定类。当ProfileManager调用myMedicalEmployee.save()时,save()方法将被多态(从结构上)解析为最初用于创建配置文件的正确类类型,例如Nurse,并在其中调用save()方法。类。

  2. 在许多情况下,您实际上并不知道在运行时需要哪种实现。从上面的示例中,您不知道全科医生,外科医生或护士是否会创建配置文件。但是,您知道无论完成什么情况都需要保存该概要文件。MedicalEmployee.profile()正是这样做的。每种特定类型的MedicalEmployee(通用从业者,外科医生,护士,

  3. 上面(1)和(2)的结果是,您现在可以添加新的医学专业,在每个新类中实现save(),从而覆盖MedicalEmployee中的save()方法,而无需在以下位置修改ProfileManager所有。


1

我知道这是一个非常老的话题,但是我曾经遇到过同样的疑问。

因此,的概念Parent parent = new Child();与java中的早期和晚期绑定有关。

私有,静态和最终方法的绑定发生在编译时,因为它们不能被覆盖,并且常规方法调用和重载方法是早期绑定的示例。

考虑示例:

class Vehicle
{
    int value = 100;
    void start() {
        System.out.println("Vehicle Started");
    }

    static void stop() {
        System.out.println("Vehicle Stopped");
    }
}

class Car extends Vehicle {

    int value = 1000;

    @Override
    void start() {
        System.out.println("Car Started");
    }

    static void stop() {
        System.out.println("Car Stopped");
    }

    public static void main(String args[]) {

        // Car extends Vehicle
        Vehicle vehicle = new Car();
        System.out.println(vehicle.value);
        vehicle.start();
        vehicle.stop();
    }
}

输出:100

汽车启动

车辆停止

发生这种情况是因为stop()它是静态方法,不能被覆盖。因此,子类中的绑定stop()在编译时发生,并且start()是非静态的。因此,有关对象类型的信息仅在运行时可用(后期绑定),因此将start()调用Car类的方法。

同样在这段代码中,由于变量初始化不会受到后期绑定的约束,因此也vehicle.value将我们100作为输出。方法重写是Java支持运行时多态性的方法之一

  • 当通过超类引用调用重写的方法时,Java将基于调用发生时所引用的对象的类型来确定要执行该方法的版本(超类/子类)。因此,该确定是在运行时进行的。
  • 在运行时,取决于所引用对象的类型(而不是引用变量的类型)来确定将执行哪个版本的重写方法

我希望这个答案在哪里Parent parent = new Child();很重要,以及为什么您不能使用上面的引用访问子类变量。


-3

您将parent声明为Parent,因此java将仅提供Parent类的方法和属性。

Child child = new Child();

应该管用。要么

Parent child = new Child();
((Child)child).salary = 1;

1
当然可以。OP要求提供Parent parent = new Child();有用/有意义的上下文。
巴兹(Baz)2012年

2
其解释与其他答案相同,那么为什么要投票反对呢?我不说我不工作,我说的是如果您说Child c = new Parant(),那么您有Parent而不是Child的实例,因此您没有属性薪水。如果您希望属性的薪水,你需要创建儿童的实例或投父到子....
Thargor
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.