多态和向下转型问题

4
我正在阅读一本关于Java的书,目前正在学习多态性主题以及如何向下转型(downcast)一个引用变量。然而,我对理解向下转型的概念感到困惑。下面是我正在跟随的示例的UML图。

UML

对于所有BasePlusCommissionEmployee对象,它们的基本工资将增加10%。其他Employee子类按照正常方式处理。 PayrollSystemTest包含运行应用程序的主方法。

// Fig. 10.9: PayrollSystemTest.java
// Employee hierarchy test program.

public class PayrollSystemTest
{
    public static void main(String[] args)
    {
        // create subclass objects
        SalariedEmployee salariedEmployee =
            new SalariedEmployee("John", "Smith", "111-11-1111", 800.00);
        HourlyEmployee hourlyEmployee =
            new HourlyEmployee("Karen", "Price", "222-22-2222", 16.75, 40.0);
        CommissionEmployee commissionEmployee =
            new CommissionEmployee(
            "Sue", "Jones", "333-33-3333", 10000, .06);
        BasePlusCommissionEmployee basePlusCommissionEmployee =
            new BasePlusCommissionEmployee(
            "Bob", "Lewis", "444-44-4444", 5000, .04, 300);

        System.out.println("Employee processed individually:");

        System.out.printf("%n%s%n%s: $%,.2f%n%n",
            salariedEmployee, "earned", salariedEmployee.earnings());
        System.out.printf("%s%n%s: $%,.2f%n%n",
            hourlyEmployee, "earned", hourlyEmployee.earnings());
        System.out.printf("%s%n%s: $%,.2f%n%n",
            commissionEmployee, "earned", commissionEmployee.earnings());
        System.out.printf("%s%n%s: $%,.2f%n%n",
            basePlusCommissionEmployee,
            "earned", basePlusCommissionEmployee.earnings());

        // create four-element Employee array
        Employee[] employees = new Employee[4];

        // initialize array with Employees
        employees[0] = salariedEmployee;
        employees[1] = hourlyEmployee;
        employees[2] = commissionEmployee;
        employees[3] = basePlusCommissionEmployee;

        System.out.printf("Employees processed polymorphically:%n%n");

        // generically process each element in array employees
        for (Employee currentEmployee : employees)
        {
            System.out.println(currentEmployee); // invokes toString

            // determine whether element is a BasePlusCommissionEmployee
            if (currentEmployee instanceof BasePlusCommissionEmployee)
            {
                // downcast Employee reference to
                // BasePlusCommissionEmployee reference
                BasePlusCommissionEmployee employee =
                    (BasePlusCommissionEmployee) currentEmployee;

                employee.setBaseSalary(1.10 * employee.getBaseSalary());

                System.out.printf(
                    "new base salary with 10%% increase is: $%,.2f%n",
                    employee.getBaseSalary());
            } // end if

            System.out.printf(
                "earned $%,.2f%n%n", currentEmployee.earnings());
        }  // end for

        // get type name of each object in employees array
        for (int j = 0; j < employees.length; j++)
            System.out.printf("Employee %d is a %s%n", j,
                employees[j].getClass().getName());
    } // end main
} // end class PayrollSystemTest

该书进一步解释了增强的for循环是如何迭代数组employees并使用Employee变量currentEmployee调用toStringearnings方法的,而这个变量在每次迭代时都被赋值为数组中不同的Employee的引用。因此,输出结果说明了每个类的特定方法都会被调用,并且根据对象类型在执行时进行解析。
为了在当前的Employee对象上调用BasePlusCommissionEmployeegetBaseSalarysetBaseSalary方法,需要使用条件语句检查对象引用是否为BasePlusCommissionEmployee对象,并使用instanceof运算符进行判断,如果条件为真,则必须将对象从Employee类型向下转换为BasePlusCommissionEmployee类型才能调用上述方法。
这让我感到困惑,因为我们能够访问子类的toString方法,但必须向下转型对象才能使用其他方法,即getBaseSalarysetBaseSalary。为什么会这样呢?
3个回答

3
因为toString()方法是在Object类中定义的,因此在每个类中都可以使用。基本工资的getter和setter仅在BasePlusCommissionEmployee中可用,因此您无法通过Employee引用调用它(如果它引用不同类型会发生什么?)。
这个例子不是你在实际代码中会看到的。使用instanceof来确定要做什么是不好的风格。

你说的很有道理。顺便问一下,为什么你会认为使用 instanceof 是一个不好的习惯? - Scorpiorian83
instanceof 是一种不好的编程实践,因为它通常可以被应用多态性的代码所替代。请参考 GhostCat 的回答。换句话说,代码 if (obj instanceof ClassA) { doA(); } else if (obj instanceof ClassB) { doB(); } 最好通过在 ClassA 和 ClassB 的超类上定义方法 doStuff(),并在每个类中重写它以执行正确的操作,然后用 obj.doStuff() 替换上述代码。 - Paul Brinkley

2
当您想在实例上调用方法时,可以调用的方法取决于多种因素,包括实例的声明类型、方法的修饰符以及您从何处调用它们。在您的示例中,应该关注的是实例的声明类型。
例如,当您声明以下内容时:
String s = new String("string");

您可以从String类中调用可访问的方法。
例如:
s.toString();
s.trim();
etc...

在你的情况下,当你声明这样一个变量时:
BasePlusCommissionEmployee basePlusCommissionEmployee =
            new BasePlusCommissionEmployee(
            "Bob", "Lewis", "444-44-4444", 5000, .04, 300);
Employee currentEmployee = basePlusCommissionEmployee;

您可以这样做:

basePlusCommissionEmployee.getBaseSalary(),因为 BasePlusCommissionEmployee 声明的类型提供了此方法。

您也可以这样做:

basePlusCommissionEmployee.toString()currentEmployee.toString(),因为这两种类型(EmployeeBasePlusCommissionEmployee)都提供了 toString() 方法。该方法是 Object 类的公共方法,所有类都继承自 Object 类,因此这些类都有 toString() 方法。

但您不能这样做:

currentEmployee.getBaseSalary(),因为 Employee 声明的类型不提供此方法。

为了规避此问题,您可以将基类向下转换为目标子类:

Employee currentEmployee = basePlusCommissionEmployee;
((BasePlusCommissionEmployee)currentEmployee).getBaseSalary();

感谢David抽出宝贵的时间为我解释。我终于明白了其中的原因,但是对于你向我展示的最后一行代码,我还是第一次看到。请问这是什么意思?这种情况是否有术语描述?谢谢。 - Scorpiorian83
不客气。很好,你理解了 :) 最后一行是一个快捷方式,当你想将一个对象转换为调用它的方法时,但你不需要将转换的结果存储在中间变量中。 可能是全局括号让你感到困扰。它允许在BasePlusCommissionEmployee类型的实例上应用getBaseSalary()方法。如果你只写(BasePlusCommissionEmployee) currentEmployee.getBaseSalary(),你会有一个编译错误,因为getBaseSalary()将被应用于currentEmployee变量。 - davidxxx

1
值得注意的是,这实际上是一个相当糟糕的例子。
Downcast 往往是设计不良的指示;而这个例子很好地证明了这一规则。
那个薪资系统不应该需要这样的 "instanceof" 检查;然后为特定的子类执行一些特定的计算。
这就是使用多态的整个意义:这些不同的类 BasePlusCommissionEmployee 和 CommissionEmployee 应该各自包含正确计算正确工资的方法。
在这里要维护的事情是 TDA(告诉不问)原则。
好的 OO 是关于告诉某个对象 "做这件事";而不是询问对象关于某些事情,然后基于该对象的内部状态(或本质,在这种情况下)做出外部决策!
对于任何有兴趣了解如何真正解决这个问题的人,我建议看一下 Robert Martin 的 "敏捷原则"。那本书描述了一个真实世界的薪资系统的设计/实现...

非常感谢您的知识分享,我也会去了解敏捷原则并好好研究。谢谢! - Scorpiorian83
@Scorpiorian83 提示:你可以免费找到那本书的C#版本...而且源代码在那里有点不太重要(UML图表肯定是与语言无关的)...这就是我首先阅读的内容。 - GhostCat
非常感谢,我刚刚下载完那本书。 :D - Scorpiorian83

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接