Java入门——类、超类和子类(二)

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.软件包中可访问的(默认),不需要修饰符。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容