1.多态
一个简单的规则可以帮助您确定继承是否是数据的正确设计。“ is–a”规则指出,子类的每个对象都是超类的对象。例如,每个经理都是一名雇员。
因此,使Manager类成为Employee类的子类是有意义的。
自然,事实并非如此-并非每个员工都是经理。
制定“是”规则的另一种方法是 替代原则。
例如,您可以将子类对象分配给超类变量。
Employee e;
e = new Employee(. . .); // Employee object expected
e = new Manager(. . .); // OK, Manager can be used as well
在Java编程语言中,对象变量是 多态的。Employee类型的变量可以引用Employee类型的对象或Employee类的任何子类的对象(例如Manager,Executive)
Manager boss = new Manager(. . .);
Employee[] staff = new Employee[3];
staff[0] = boss;
在这种情况下,变量staff [0]和boss指的是同一对象。但是,staff [0]被编译器视为仅Employee对象。
boss.setBonus(5000); // OK
staff[0].setBonus(5000); // ERROR
staff [0]的声明类型为Employee,而setBonus方法不是Employee类的方法。
但是,不能将超类引用分配给子类变量。例如,进行转让是不合法的
Manager m = staff[i]; // ERROR
如果此分配成功完成,并且m引用了不是Managet类的Employee对象,若调用m.setBonus(......),则会发生运行时错误。
注意:在Java中,子类引用的数组可以转换为超类引用的数组,而无需强制转换。例如,考虑以下Manager数组:
Manager[] managers = new Manager[10];
It is legal to convert this array to an Employee[] array:
Employee[] staff = managers; // OK
请记住,经理和员工是对同一数组的引用。现在考虑语句staff [0] = new Employee(“ Harry Hacker”,....);
编译器会愉快地允许此分配。但是Employee[0]和Manager[0]
是相同的参考,因此似乎我们设法将仅一名雇员走私到了管理层中。那将是非常糟糕的—调用managers [0] .setBonus(1000)会尝试访问不存在的实例字段,并且会破坏相邻的内存。
为确保不会发生此类损坏,所有数组都记住创建它们所使用的元素类型,并且它们监视仅将兼容的引用存储在其中。例如,创建为new Manager[10]的数组
记得这是一系列管理人员。尝试存储Employee引用会导致ArrayStoreException。
2.方法调用
准确了解方法调用如何应用于对象非常重要。
假设我们调用x.f(args),并将隐式参数x声明为C类的对象。这是发生的情况:
编译器查看对象的声明类型和方法名称。请注意,可能有多种方法,所有方法都具有相同的名称f,但具有不同的参数类型。例如,可能存在方法f(int)和方法f(String)。编译器枚举C类中称为f的所有方法,以及C超类中称为f的所有可访问方法。(无法访问超类的私有方法。)
现在,编译器知道要调用的方法的所有可能候选者。
接下来,编译器确定方法调用中提供的参数的类型。如果在所有称为f的方法中,有一个唯一的方法,其参数类型与所提供的参数最匹配,则选择该方法进行调用。此过程称为 重载解析。例如,在调用x.f(“ Hello”)中,编译器选择f(String)而不是f(int)。由于类型转换(从int到double,从Manager到Employee,等等),这种情况可能会变得复杂。如果编译器无法找到具有匹配参数类型的任何方法,或者如果在应用转换后多个方法都匹配,则编译器将报告错误。
注意:请记住,方法的名称和参数类型列表称为方法的 签名。例如,f(int)和f(String)是两个名称相同但签名不同的方法。如果在子类中定义一个与超类方法具有相同签名的方法,则将覆盖超类方法。
返回类型不是签名的一部分。但是,重写方法时,需要保持返回类型兼容。子类可以将返回类型更改为原始类型的子类型。例如,假设Employee类有一个方法
public Employee getBuddy(){...}
经理永远不会希望有一个低下的员工作为伙伴。为了反映这一事实,Manager子类可以将此方法重写为
public Manager getBuddy(){。。。} //确定更改返回类型
我们说这两个getBuddy方法具有 协变返回类型。
如果该方法是private, static, final或构造函数,则编译器会确切知道要调用哪个方法。这称为 静态绑定。否则,要调用的方法取决于隐式参数的实际类型,并且必须在运行时使用动态绑定。在我们的示例中,编译器将生成一条指令以动态绑定调用f(String)。
当程序运行并使用动态绑定调用方法时,虚拟机必须调用适合x所引用对象的实际类型的方法版本 。假设实际类型为D,它是C的子类。如果类D定义了方法f(String),则将调用该方法。如果不是,则在D的超类中搜索方法f(String),依此类推。
每次调用方法时执行此搜索将很耗时。相反,虚拟机为每个类预先计算一个方法表,该表列出了所有方法签名和要调用的实际方法。实际调用方法时,虚拟机仅进行表查找。在我们的示例中,虚拟机在类D的方法表中查询并查找该方法以调用f(String)。
该方法可以是D.f(String)或X.f(String),其中X是D的某些超类。如果调用为super.f(param),则编译器查询隐式参数超类的方法表。
让我们在上一篇文章中的调用e.getSalary()中详细查看此过程。e的声明类型为Employee。Employee类只有一个方法,称为getSalary,没有方法参数。因此,在这种情况下,我们不必担心重载解析。
getSalary方法不是private, static或final,因此它是动态绑定的。虚拟机为Employee和Manager类生成方法表。
Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
Manager:
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
setBonus(double) -> Manager.setBonus(double)
3.final类
有时,您想阻止某人形成您的某个类的子类。无法扩展的类称为 final类,您可以在类的定义中使用final修饰符来表明这一点。例如,假设我们要防止其他人继承执行类。只需使用final修饰符声明该类,如下所示:
public final class Executive extends Manager
{
. . .
}
您还可以在final类中标记特定的方法。如果这样做,则没有子类可以覆盖该方法。(final类中的所有方法都是自动final的。)例如:
public class Employee
{
. . .
public final String getName()
{
return name;
}
. . .
}
注意:请记住,字段也可以声明为final。构造对象后,无法更改最终字段。但是,如果将一个类声明为final,则仅方法(而不是字段)自动为final。
在Java的早期,一些程序员使用final关键字希望避免动态绑定的开销。如果一个方法没有被覆盖并且很短,那么编译器可以优化调用方法,即一个名为inlining的过程 。例如,内联调用e.getName()会将其替换为字段访问e.name。这是一个值得改善的改进-CPU讨厌分支,因为它会干扰他们在处理当前指令时预取指令的策略。但是,如果可以在另一个类中重写getName,则编译器无法内联它,因为它无法知道重写的代码可以做什么。
幸运的是,虚拟机中的即时编译器可以比传统编译器做得更好。它确切知道哪些类扩展了给定的类,并且可以检查是否有任何类实际上覆盖了给定的方法。
如果一个方法很短,经常被调用并且实际上没有被重写,则即时编译器可以内联它。
4.强制转换
您可能需要将对象引用从一个类转换为另一个类。要实际转换对象引用,请使用与转换数字表达式相似的语法。用括号括住目标类名称,并将其放在要转换的对象引用之前。例如:
Manager boss = (Manager) staff[0];
您要进行强制转换的原因只有一个-在暂时忘记其实际类型后使用其最大容量的对象。
例如,在ManagerTest类中,staff数组必须是Employee对象的数组,因为它的 某些元素是常规员工。我们需要将数组的管理元素强制转换回Manager,以访问其任何新变量。(请注意,在第一部分的示例代码中,我们做出了特殊的努力来避免强制转换。在将其存储在数组中之前,我们使用Manager对象初始化了boss变量。我们需要正确的类型来设置manager的奖励)
如您所知,在Java中,每个变量都有一个类型。类型描述了变量所引用的对象类型以及它可以做什么。例如,staff [i]引用一个Employee对象(因此它也可以引用一个Manager对象)。
当您将值存储在变量中时,编译器会检查您是否承诺不太多。如果将子类引用分配给超类变量,那么您的承诺就更少了,编译器将简单地允许您这样做。如果将超类引用分配给子类变量,那么您承诺更多。然后,您必须使用强制转换,以便可以在运行时检查您的诺言。
如果您尝试抛弃继承链并“撒谎”会发生什么?
关于一个对象包含什么?
Manager boss = (Manager) staff[1]; // ERROR
程序运行时,Java运行时系统会注意到违约并生成ClassCastException。如果没有捕获到异常,则程序将终止。因此,在尝试转换之前,先找出转换是否会成功是一种很好的编程习惯。只需使用instanceof运算符。例如:
if (staff[1] instanceof Manager)
{
boss = (Manager) staff[1];
. . .
}
总结:
•您只能在继承层次结构中进行强制转换。
•在从超类转换为子类之前,请使用instanceof进行检查。
5.抽象类
随着继承层次结构的上移,类将变得更加通用,并且可能更加抽象。在某些时候,祖先类变得 如此笼统,以至于您将其更多地视为其他类的基础,而不是具有要使用的特定实例的类。例如,考虑我们的Employee类层次结构的扩展。员工是一个人,学生也是。让我们扩展我们的班级层次结构,以包括“人”和“学生”类。图5.2
显示了这些类之间的继承关系。
为什么要花这么高的抽象水平呢?有一些对每个人都有意义的属性,例如名称。学生和雇员都有名字,而引入一个公共的超类可以使我们在继承层次结构中将getName方法分解到更高的层次。
现在让我们添加另一个方法getDescription,其目的是返回该人的简短描述,例如
年薪为$ 50,000.00的员工
计算机科学专业的学生
对于Employee和Student类,很容易实现此方法。但是,除名称外,Person类对人员一无所知。
public abstract class Person
{
. . .
public abstract String getDescription();
}
除了抽象方法之外,抽象类还可以具有字段和具体方法。例如,Person类存储人员的姓名,并具有返回该姓名的具体方法。
public abstract class Person
{
private String name;
public Person(String name)
{
this.name = name;
}
public abstract String getDescription();
public String getName()
{
return name;
}
}
扩展抽象类时,有两种选择。
您可以使某些或所有抽象方法保持未定义状态。那么您还必须将子类标记为抽象。或者,您可以定义所有方法,并且子类不再是抽象的。
例如,我们将定义一个Student类,该类扩展了抽象的Person类并实现getDescription方法。Student类的方法都不是抽象的,因此不需要将其声明为抽象类。
尽管没有抽象方法,但类甚至可以声明为抽象类。
抽象类无法实例化。也就是说,如果将一个类声明为抽象的,则不能创建该类的对象。例如,表达式new Person(“ Vince Vu”)
是一个错误。但是,您可以创建具体子类的对象。
请注意,您仍然可以创建 抽象类的对象变量,但是此类变量必须引用非抽象子类的对象。例如:Person p = new Student(“ Vince Vu”,“ Economics”); 这里p是抽象类型Person的变量,它引用非抽象子类Student的实例。
让我们定义一个扩展抽象类Person的具体子类Student:
public class Student extends Person
{
private String major;
public Student(String name, String major)
{
super(name);
this.major = major;
}
public String getDescription()
{
return "a student majoring in " + major;
}
}
6.protected 访问
若允许子类方法访问超类字段,您将类功能声明为。例如protected,如果超类Employee将hiredDay字段声明为protected而不是private,则Manager方法可以直接访问它。
以下是Java中四个访问控制修饰符的摘要:
1.仅在class可访问(private)。
2.全部范围可访问(public)
3.可在包和所有子类中访问(protected)。
4.软件包中可访问的(默认),不需要修饰符。