5.8多态

上面我们了解了向上转型,即一个对象变量可以引用本类及子类的对象实例,这种现象称为多态(polymorphism)。多态究竟有什么用呢?我们先学习一个知识点。

5.8.1方法重写

前面我们学习类与对象的时候,学习过方法重载(overload),方法重载指的是在同一个类,存在多个方法名相同,但是方法签名不同的现象。

方法重写是啥呢?我们先看一个问题。Gun类有单发的方法:

public void shoot() {
System.out.println("单发");
}

对于狙击枪AWM来说,射击的子弹是7.62mm子弹,不想只输出“单发”两个字,而是想输出“发射7.62mm子弹”。那么怎么办呢?可以在AWM类中重新写定义一个一模一样的方法,然后重写方法体:

public void shoot() {
System.out.println("发射7.62mm子弹");
}

这种在子类中重新定义一个和超类相同方法签名的操作,称为方法重写(override)。这里需要注意一个问题,就是构造方法是不能被重写的,因为构造方法不能被继承。另外子类的方法不能低于超类方法的可见性。

方法重写要求方法签名和返回值都完全一样,有时候,我们在进行方法重写的时候,经常会出现方法名相同、参数列表也相同,但是返回值不同的现象,这时候其实就不算方法重写。不过在Java5.0之后,返回值只要是原返回类型的子类也算方法重写。其实有个好办法帮我们检查方法重写时候正确,就是在子类的覆盖方法上加上一个注解:@Override,如果重写错误,编译器会报错。关于注解后面会详细讨论。加上注解的方法如下:

@Override
public void shoot() {
System.out.println("发射7.62mm子弹");
}

当然,这个注解并不是必须的。不过笔者建议在重写的时候最好是加上。

另外,如果超类中的方法是静态方法,子类想要重写该方法,也必须定义为静态的,如果定义为成员方法,则会编译报错。反之亦然。我们可以用一个表总结一下子类中定义了一个和超类方法签名一样并且返回值也一样的情况:

超类成员方法

超类静态方法

子类成员方法

重写

编译报错

子类静态方法

编译报错

重写

现在,我们来总结一下方法重写:

  • 方法签名必须相同
  • 方法返回值相同或者为其子类
  • 可见性不能低于超类方法
  • 可以使用注解@Override帮助检查重写是否正确
  • 成员方法必须用成员方法重写,静态方法必须用静态方法重写

5.8.2动态绑定

在上面的例子中,AWM重写了单发方法,我们给AK47类也重写单发方法:

@Override
public void shoot() {
System.out.println("发射5.56mm子弹");
}

然后我们编写测试方法:

public class ExtendTest {
public static void main(String[] args) {
Gun gun1 = new AWM("awm", "绿色", "8倍镜");
Gun gun2 = new AK47("ak47", "黑色", "4倍镜");
gun1.shoot();// 输出结果为: 发射7.62mm子弹
gun2.shoot();// 输出结果为: 发射5.56mm子弹
}
}

我们发现,gun1和gun2都是Gun类型变量,但是最终调用shoot()方法的结果不一样。当实际引用的是AWM类型对象,则调用AWM的shoot方法,实际引用的AK47类型对象,则调用AK47的shoot方法。这种在运行时能够自动选择调用哪个方法的现象称为动态绑定(dynamic binding)。

5.8.3多态有什么用

我们了解了方法重写和动态绑定,那么多态有什么用处呢?下面我们用实际例子来演示。我们现在拥有了4个枪类:Gun、AWM、AK47、Gatling。现在我们编写一个玩家类,我们假设玩家只能拿一把枪,但是玩家可以随时更换枪支。我们设计一个玩家类如下:

根据设计,我们编写代码如下:

public class Player {
private Gun gun; public void changeGun(Gun gun) {
this.gun = gun;
} public void shoot() {
this.gun.shoot();
}
}

然后我们编写测试类:

public class ExtendTest {
public static void main(String[] args) {
Gun gun1 = new AWM("awm", "绿色", "8倍镜");
Gun gun2 = new AK47("ak47", "黑色", "4倍镜");
Player player = new Player();
player.changeGun(gun1);
player.shoot();// 输出 发射7.62mm子弹
player.changeGun(gun2);
player.shoot();// 输出 发射5.56mm子弹
}
}

我们看到,对于玩家类,不需要和枪支具体的子类打交道,只需要持有超类Gun对象即可。假如新加了别的枪,只需要编写新的枪类继承Gun类,重写shoot方法,然后调用Player类的changeGun方法即可。新增枪完全不需要修改Player类的代码。

多态的强大之处就在此,可以很方便的扩展子类,而不需要修改基类代码和一些调用者的类。一般编写框架和一个项目的核心代码,经常会利用继承和多态的特性,这个等你们经验丰富了,有机会进入一个项目的框架小组,编写框架代码的时候就会充分体会到多态的强大。

5.9final阻止继承

我们又一次看到了final关键字。前面我们学习类和对象的时候,知道用final修饰的属性将不能被修改。当时我们提到过,如果用final修饰类类型的属性时,必须保证该类也是final的。

当我们用final来修饰一个类的时候,那么这个类就不能被继承了,不过该类是可以继承其他类的。例如java.lang.String类就是一个final类:

public final class String  

另外,我们还可以用final修饰方法,用final修饰的方法则不能被重写了。例如我们把Gun类中的getColor()方法定义为final的:

public final String getColor() {
return this.color;
}

5.10Object类

前面介绍继承层次的时候,提到过顶级超类java.lang.Object。如果某个类没有显示的使用extends关键字,则该类是继承自Object。事实上,在Java中,除了基本数据类型不是对象,其他都是对象,包括数组。因此数组也是继承自Ojbect类的。这里需要注意的是,即使是基本数据类型的数组,也是继承自Object。因此我们可以把一个数组赋值给一个Object类型的变量:

Object obj;
int[] a = new int[] { 1, 2, 3 };
obj = a;

Object类中定义了许多有用的方法,都会被我们继承下来,因此我们有必要熟悉一下。这里我们主要介绍3个方法:equals方法、hashCode方法和toString方法。还有一些其他方法我们留在后面讨论。下表列出这3个方法的说明:

方法

说明

equals

比较两个对象是否相等,如果相等返回true,否则返回false

hashCode

返回该对象的hash值

toString

以字符串形式返回该对象的有关信息

5.10.1equals方法

我们在学习String类的时候,就接触过equals方法了。equals方法是用来比较两个对象时候相等。不过String类的equals方法是重写过的。因为Object的equals方法很简单,仅仅判断两个对象的引用是否相等(即两个对象变量内存中的值),实际上和等号(==)没有区别。但是其实大多数情况下,这种判断没有意义。例如对于String类来说,如果仅仅判断对象引用是否相等,那么“Java大失叔”和“Java大失叔”很有可能将不相等。更有意义的判断可能是两个对象的状态完全一致(即所有属性值都一致)。这就要求我们如果有需要,一般都要重写equals方法。

Java语言规范对equals方法其实是有要求的,需要满足下面几个特性:

  • 自反性:对于任何非空引用x,x.equals(x)应该返回true
  • 对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true
  • 传递性:对于任何引用x、y和z。如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true
  • 一致性:如果x和y引用的对象没有发生改变,反复调用x.equals(y)返回值不变
  • 对于任何非空引用x,xequals(null)应该返回false

根据上面的规范,假如我们认为2个Gun对象名字和颜色都一样,则2把枪是相等的,可以如下编写代码:

 1 @Override
2 public boolean equals(Object otherObj) {
3 if (this == otherObj) {
4 return true;
5 }
6 if (otherObj == null) {
7 return false;
8 }
9 if (this.getClass() != otherObj.getClass()) {
10 return false;
11 }
12 Gun other = (Gun) otherObj;
13 return this.name.equals(other.name) && this.color.equals(other.color);
14 }

注意,第9行用到了Object类的getClass方法,这个方法返回的是一个对象所属的类,比如一个AWM对象调用getClass方法返回是AWM类,一个Gun对象调用getClass方法返回的是Gun类。因此下面代码返回的结果将是false:

Gun gun1 = new Gun("awm", "绿色");
Gun gun2 = new AWM("awm", "绿色", "4倍镜");
System.out.println(gun1.equals(gun2));

如果只在Gun中重写了equals方法,而AWM中不重写的话,那么对于2把同样颜色、但是倍镜不同的AWM来说,将是不合理的。因此我们还需要在AWM中重写equals方法:

@Override
public boolean equals(Object otherObj) {
if (!super.equals(otherObj)) {
return false;
}
AWM other = (AWM) otherObj;
return this.gunsight.equals(other.gunsight);
}

注意第3句代码,首先调用超类的equals方法,如果检测失败,表示2个对象不可能相等了,直接返回false即可。如果超类equals方法通过,往下只需要比较AWM特有的属性倍镜即可。

上面的equals方法编写几乎很完美,可以作为equals方法编写的模板,但是稍微有一点点瑕疵,就是Gun类equals方法第9行使用getClass方法来判断2个对象是否属于同一个类,对于某些不需要区分的那么严格的情况下,稍显严格。假如我们不需要关心一把枪是狙击器还是步枪,只要看名字和颜色一样,就认为相等(这个例子不是很合理,纯粹为了演示),那么就可以用instanceof关键字来判断,这样Gun的equals方法可以修改如下:

@Override
public boolean equals(Object otherObj) {
if (this == otherObj) {
return true;
}
if (otherObj == null) {
return false;
}
// if (this.getClass() != otherObj.getClass()) {
// return false;
// }
if (!(otherObj instanceof Gun)) {
return false;
}
Gun other = (Gun) otherObj;
return this.name.equals(other.name) && this.color.equals(other.color);
}

当然这时候AWM就不需要重写equals方法了。这样修改以后,上面的测试代码将返回true。通过这个例子,大家应该看出getClass和instanceof的区别了:

  • instanceof比较宽松,对于x instanceof y,只要x是y的类型,或者是y的子类型,就返回true。
  • getClass是严格的判断,不考虑继承的类型。

最后,我们可以总结一下equals方法编写的一个相对完美的模板:

  1. 使用==检测this和otherObj是否引用同一个对象,如果是直接返回true。
  2. 检测otherObj是否为null,如果是直接返回false。
  3. 比较this和otherObj是否属于同一个类;这里要仔细思考一下是使用getClass方法还是instanceof。如果不需要区分子类,就使用instanceof,同时子类不要重写equals方法。如果子类需要重新定义,就使用getClass方法
  4. 把otherObj转换为本类的类型
  5. 一般情况下,进行属性状态的比较;使用==比较基本数据类型的属性,使用equals方法比较类类型的属性。
  6. 如果是子类,第一句话调用super.equals(otherObj)。

5.10.2hashCode方法

hash code叫做散列码,是由对象导出的一个整型值。Object类的的hashCode方法是一个本地方法:

public native int hashCode(); 

实际上返回的就是对象的内存地址。如果对象x和y是不同的对象,那么x.hashCode()和y.hashCode()基本是不相同的。

如果一个类重写了equals方法,一般情况下,必须重写hashCode方法,以便让equals与hashCode的定义是一致的:如果x.equas(y)返回true,那么x.hashCode()和y.hashCode()也要返回同样的值。比如String类重写了equals方法,那么它也重写了hashCode方法:

public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value; for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

我们可以看出,String的散列码是通过字符串中的字符加上一些算法导出的,我们看代码:

String s1 = "Java大失叔";
String s2 = "Java大失叔";
System.out.println(s1.equals(s2));// 返回true
System.out.println(s1.hashCode() == s2.hashCode());// 返回true

只要2个字符串equals相等,那么hashCode方法返回值也是相等的。

另外需要注意的是,散列码可以返回负数,我们尽量要合理的组织散列码,以便让不同的对象产生的散列码相对均匀。这里给出一个编写hashCode方法的建议:

  1. 初始化一个整型变量,例如h=17
  2. 然后选取equals方法中用到的所有属性,针对每个属性计算一个hash值,乘以h
    • 对于对象类型的属性x,直接调用x.hashCode()计算hash值
    • 对于基本数据类型的属性y,可以用包装器包装成对应的对象类型Y,然后调用Y.hashCode()计算hash值
    • 对于数组类型的属性,可以调用java.util.Arrays.hashCode()方法计算hash值
  3. 最后把各个属性计算后的值相加作为最后的hash值返回

上面提到包装器类,因为基本数据类型不是对象,为了面向对象,Java对每一个基本数据类型都提供了一个包装器类,具体我们在后面会介绍。我们按照这个建议,给Gun类重写hashCode方法,因为Gun类equals方法参与的属性都是String,因此比较简单:

@Override
public int hashCode() {
return 17 * this.name.hashCode() + 17 * this.color.hashCode();
}

对于Gun的子类AWM,重写hashCode方法可以如下:

@Override
public int hashCode() {
return super.hashCode() + 31 * this.gunsight.hashCode();
}

5.10.3toString方法

我们经常会用System.out.println()来进行打印。如果我们把一个对象x传入到该方法中,那么println方法就会直接调用x.toString()方法。例如:

Gun gun = new Gun("awm", "绿色");
System.out.println(gun);// 打印:com.javadss.javase.ch05.Gun@12742ea

另外我们用一个字符串通过操作符“+”和一个对象x连接起来,那么编译器也会自动调用x.toString()方法。例如:

Gun gun = new Gun("awm", "绿色");
System.out.println("这是大失叔儿子的枪:" + gun);// 打印:这是大失叔儿子的枪:com.javadss.javase.ch05.Gun@12742ea

我们看到,默认的Object类的toString()方法只返回对象所属类名和散列码,如果我们想用toString方法进行调试,就有必要重写toString方法,返回对象的状态的相关信息。一种比较常见的重写toString方法的格式为:类名[属性值列表],比如我们可以在Gun类中重写toString方法如下:

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getName());
sb.append("[name=").append(this.name).append(",");
sb.append("color=").append(this.color).append("]");
return sb.toString();
}

这样重写以后,对于下面代码,打印将会友好的多:

Gun gun = new Gun("awm", "绿色");
System.out.println(gun);// 打印:com.javadss.javase.ch05.Gun[name=awm,color=绿色]

我们注意重写的toString方法的第4句,我们使用了this.getClass().getName()方法,这样在子类中将会动态输出子类所属的类,这也是多态的一种应用。例如打印AWM:

AWM gun = new AWM("awm", "绿色", "4倍镜");
System.out.println(gun);// 打印:com.javadss.javase.ch05.AWM[name=awm,color=绿色]

当然,如果AWM不重写toString方法的话,那么输出将不会体现倍镜属性的状态,因此我们最好给子类也重写toString方法,子类重写的时候,可以充分利用超类中已经重写的部分:

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString());
sb.append("[gunsight=").append(this.gunsight).append("]");
return sb.toString();
}

我们可以直接调用super.toString(),然后加上自身特有的属性值即可,这样输出值将会变为:

com.javadss.javase.ch05.AWM[name=awm,color=绿色][gunsight=4倍镜]

事实上,类库中很多类都重写了toString方法,不过对于数组来说,却提供了一种不是很友好的输出,例如:

double[] ds = { 1.0d, 2.0d };
System.out.println(ds);// 打印:[D@15db9742

前缀[D表示这是一个double数组,后面是哈希值。如果我们想获得友好的输出,可以使用java.util.Arrays类的toString方法:

double[] ds = { 1.0d, 2.0d };
System.out.println(Arrays.toString(ds));// 打印:[1.0, 2.0]

如果是多维数组,可以调用Arrays.deepToString方法:

double[][] ds = { { 1.0d, 2.0d }, { 3.0d, 4.0d } };
System.out.println(Arrays.deepToString(ds));// 打印:[[1.0, 2.0], [3.0, 4.0]]

笔者非常建议如果时间、条件允许,最好对每一个重要的自定义类都重写toString方法,这将会对你和调用你代码的人有很大帮助。

《Java从入门到失业》第五章:继承与多态(5.8-5.10):多态与Object类的更多相关文章

  1. 《Java从入门到失业》第一章:计算机基础知识(一):二进制和十六进制

    0 前言 最近7年来的高强度工作和不规律的饮食作息,压得我有些喘不过气,身体也陆续报警.2018年下半年的一场病,让我意识到了这个问题的严重性,于是开始强制自己有规律饮食和作息,并辅以健身锻炼,不到2 ...

  2. 《Java从入门到失业》第二章:Java环境(一):Java SE安装

    从这一章开始,终于我们可以开始正式进入Java世界了.前面我们提到过,Java分三个版本,我们这里只讨论Java SE. 2.1Java SE安装 所谓工欲善其事,必先利其器.第一步,我们当然是要下载 ...

  3. 《Java从入门到失业》第一章:计算机基础知识(三):程序语言简介

    1.3程序语言简介 我们经常会听到一些名词:低级语言.高级语言.编译型.解释型.面向过程.面向对象等.这些到底是啥意思呢?在正式进入Java世界前,笔者也尝试简单的聊一聊这块东西. 1.3.1低级语言 ...

  4. 《Java从入门到失业》第二章:Java环境(四):IDE集成环境

    2.4IDE集成环境 在掌握了编写.编译和运行Java程序的基本步骤以后,你肯定就在想,这太麻烦了,有没有更好的工具?当然有了,那就是IDE.IDE就是专业的集成开发环境(Integrated Dev ...

  5. 《Java从入门到失业》第二章:Java环境(三):Java命令行工具

    2.3Java命令行工具 2.3.1编译运行 到了这里,是不是开始膨胀了,想写一段代码来秀一下?好吧,满足你!国际惯例,我们写一段HelloWorld.我们在某个目录下记事本,编写一段代码如下: 保存 ...

  6. 《Java从入门到失业》第二章:Java环境(二):JDK、JRE、JVM

    2.2JDK.JRE.JVM 在JDK的安装目录中,我们发现有一个目录jre(其实如果是下一步下一步安装的,在和JDK安装目录同级目录下,还会有一个jre目录).初学Java的同学,有时候搞不清楚这3 ...

  7. 《Java从入门到失业》第一章:计算机基础知识(二):计算机组成及基本原理

    1.2计算机组成及基本原理 1.2.1硬件组成 这里说的计算机主要指微型计算机,俗称电脑.一般我们见到的有台式机.笔记本等,另外智能手机.平板也算.有了一台计算机,我们就能做很多事情了,比如我在写这篇 ...

  8. 《Java从入门到失业》第四章:类和对象(4.5):包

    4.5包 前面我们已经听过包(package)这个概念了,比如String类在java.lang包下,Arrays类在java.util包下.那么为什么要引入包的概念呢?我们思考一个问题:java类库 ...

  9. ArcGIS for Desktop入门教程_第五章_ArcCatalog使用 - ArcGIS知乎-新一代ArcGIS问答社区

    原文:ArcGIS for Desktop入门教程_第五章_ArcCatalog使用 - ArcGIS知乎-新一代ArcGIS问答社区 1 ArcCatalog使用 1.1 GIS数据 地理信息系统, ...

  10. D3.js的v5版本入门教程(第五章)—— 选择、插入、删除元素

    D3.js的v5版本入门教程(第五章) 1.选择元素 现在我们已经知道,d3.js中选择元素的函数有select()和selectAll(),下面来详细讲解一下 假设我们的<body>中有 ...

随机推荐

  1. Go语言使用swagger生成接口文档

    swagger介绍 Swagger本质上是一种用于描述使用JSON表示的RESTful API的接口描述语言.Swagger与一组开源软件工具一起使用,以设计.构建.记录和使用RESTful Web服 ...

  2. leetcode刷题-67二进制求和

    题目 给你两个二进制字符串,返回它们的和(用二进制表示). 输入为 非空 字符串且只包含数字 1 和 0. 示例 1: 输入: a = "11", b = "1" ...

  3. PHP 类的构造方法 __construct()

    1. 构造方法简介 构造方法 __construct() 是一种类结构特有的特殊方法,该方法由系统规定好 实例化一个类时:先调用该方法,再返回类的对象 构造方法也是普通方法,不同之处就是在实例化类时会 ...

  4. Fragment时长统计那些事

    注:本文同步发布于微信公众号:stringwu的互联网杂谈 frament时长统计那些事 页面停留时长作为应用统计的北极星指标里的重要指标之一,统计用户在某个页面的停留时长则变得很重要.而Fragme ...

  5. 2020年 .NET ORM 完整比较、助力选择

    .NET ORM 前言 为什么要写这篇文章? 希望针对 SEO 优化搜索引擎,让更多中国人知道并且使用.目前百度搜索 .NET ORM 全是 sqlsugar,我个人是无语的,每每一个人进群第一件事就 ...

  6. Mybatis如何执行Select语句,你真的知道吗?

    持续原创输出,点击上方蓝字关注我吧 作者:不才陈某 博客:https://chenjiabing666.github.io 前言 本篇文章是Myabtis源码分析的第三篇,前两篇分别介绍了Mybati ...

  7. python中一次性input3个整数,并用空格隔开怎么表示

    a,b,c=map(int,input('请输入3个整数用空格隔开:').split(' ')) map的使用方法:map(函数名,循环体)

  8. mongodb查询语句与sql语句对比

    左边是mongodb查询语句,右边是sql语句.对照着用,挺方便. db.users.find() select * from users db.users.find({"age" ...

  9. defer implement for C/C++ using GCC/Clang extension

    前述: go 中defer 给出了一种,延时调用的方式来释放资源.但是对于C/C++去没有内置的这种属性.对于经常手动管理内存的C/C++有其是C程序员这种特性显得无比重要.这里给出了一种基于GCC/ ...

  10. 学习 | css3基本动画之demo篇

    移动端使用的框架是zepto,但是zepto的内置对象没有传统的animate这个方法,效果都是需要css3来实现的,zepto也不支持fadeIn和fadeOut等一些基本的动画,基于这一现状,我自 ...