本教程出自于白师傅、本节关键词: 继承, 父类, 子类, 祖先类, 派生类, this 与 super, 覆盖, 异常, 等值判断

继承

关于”继承”的概念、为什么要继承、继承的好处等一些基础性的知识在本 Blog 中的另一文章“面向对象起步 — 封装、继承、多态”已有讲述, 因此此处不再赘述, 本文仅对如何在 Java 中实现继承进行阐述……

我们沿袭前面的例子, 由 Pig 类派生出一个子类 SmallPig (小猪):

com.bailey.study.animal 包内新建一个类, 并使用 extends 关键词实现继承, 代码如下:

如上图所示, 当添加了代码 extends Pig 后 Eclipse 提示了一个错误, 大意是: 在父类 Pig 中未定义默认构造函数 Pig(), 因此必须定义一个明确的构造函数.

还记得上节最后讲述构造函数的时候我们列出了几点需要注意的, 其中第 (2) 点当时可能看不明白, 现在就来一起解释一下:

继承事实上是在父类的基础上进行 “扩展 ( extend )”. 当实例化子类的时候, Java 会先去实例化其父类, 因此, 若父类中没有那个不带任何参数的缺省 (默认) 构造函数, 那么子类在实例化的时候就必须明确要通过哪一个父类构造函数去实例化父类, 否则…… 爹都生不出来, 儿子怎么出来?

呵呵~ 那咋办呢?

看上图…… Eclipse 其实已经给出了2个建议 ( quick fix, 可以直接点击, 之后 Eclipse 会帮你把代码写好…)

下面的代码是经人工修改后, 正确代码:

1
2
3
4
5
6
7
8
9
10
package com.bailey.study.animal;

public class SmallPig extends Pig {
public SmallPig(String name) {
this(name, "雌");
}
public SmallPig(String name, String sex) {
super(name, sex);
}
}

注意, 上述两个 SmallPig 类的构造函数均必须通过某种方式去调用父类的构造函数 (第 5 行 和 第 8 行).

其中:

第 8 行通过 super 关键词直接调用了父类中定义的构造函数: Pig(String name, String sex).

第 5 行则通过 this 关键词调用了当前类中第 7~9 行定义的构造函数 ( 间接地调用父类构造函数).

试着按住 Ctrl 键, 用鼠标点击第 5 行的 this …… 看到效果了吧 (点击函数名, Eclipse会跳转到实际执行的函数的声明位置, 点击变量, 则会跳转到变量声明位置)

上面是继承之后, 子类中必须解决的一个问题.

覆盖(Override)

下面来看另一个概念: 覆盖.

显然, SmallPig 恐怕还不能吃 “米饭”, 得吃 “奶” !

因此, 我们为 SmallPig 重新定义一下 eat 方法, 将如下代码添加到SmallPig类中:

1
2
3
4
5
6
7
@Override                       // @Override 注解告诉编译器注意校验"下面的方法是覆盖版本"
public void eat(String food) {
if (!"奶".equals(food)) { // 判断食物是否是奶
throw new RuntimeException("小猪只能吃奶!"); // 不是"奶", 抛出异常
}
super.eat(food); // 调用父类eat方法
}

如上面代码所示, 简单来说, 在子类中重新定义祖先类中已有的方法就叫做覆盖 (或重写).

注意和第 (1) 节中讲到的重载对比一下

重载发生在同一个类的 “内部”, 而覆盖(重写)发生成祖先类和子类之间

先试试效果吧…… 什么? 怎么试? 好吗, 最后帮你一次…… 代如下:

复制不了…… 呵呵, 我故意的! 防止懒人偷代码! 什么都不想动手, 只是傻看的话永远学不会……

怎么样, 试了吗? 控制台 ( Console ) 是不是出现了异常信息, 并列出了函数调用栈的信息:

第一次见到这个东东, 简单解释一下……

上图这段输出的大意是: 在线程 “main” 中出现了异常 ( Exception ), 这个异常是 RuntimeException (运行时异常), 后面那几个汉字不用解释了吧~

第 2 行陈述了异常是在执行到哪一行抛出的 ( SmallPig.java 中的 16 行, 也就是上面代码的第 5 行, 鼠标点击一下带下划线的部分试试… )

第 3 行表达的意思与第 2 行类似: 在执行到HelloJava.java中第 10 行出现异常……

如果我们从下往上读, 是不是就是程序实际执行时的调用顺序~

也许现在最吸引你注意的是 “异常(Exception)” 这个新鲜玩意是什么鬼, 或是上面出现的几行看不太懂的代码 ( 第3, 5行 ), 但先把它放下, 我们后面还会详细介绍.

现在花10分钟 ( Maybe… 20 分钟 ), 设置断点, 单步跟踪程序的执行过程, 并仔细琢磨一下 p.eat("奶")p.eat("奶", true) 是如何一步步执行的?

也许, 你还应该花点时间改变一下 Pig 类中那些属性和方法的可访问性修饰符, 看看效果……

也许…… 你还应该在Pig类中使用一下 final 修饰符试试…… ( 把 final 放在类声明部分和 eat 方法声明部分试试 )

再次强调, 本教程不能只是看… 必须边看, 边实验, 边思考, 你才能看得懂!

OK, 是不是越来越复杂了, 呵呵~ 确保前述内容没有太大的问题之后, 继续往下看……

虽然作为一头人品正常的猪, 生活就是”吃”和”睡”, 这是多少人向往的生活啊…… 但在下私心里估摸着, 猪儿的童年想必也是极喜欢玩耍的吧, 或许……平日间偶尔玩一下也是极好的~    

呵呵, 来吧, 给小猪类加个”玩”的方法…… 代码如下:

1
2
3
4
public void play(String toy) {
// 把 Pig 类中的 name 属性改为 protected 修饰就不报错了
System.out.println(name + "玩了会" + toy);
}

意思都明白吧…… 如果我们执行”小花”的play方法, 例如: p.play("球"); 那应该输出: 小花玩了会球

OK, 继续, 修改 HelloJava.java 的代码, 变成如下样式:

咦~ 好多错…… 别急, 我们一起来细细琢磨……

(1) 先把第 13~33 行注释掉 ( 选中 13~33 行, 按 “ Ctrl + / “ , 呵呵, 又学了一招~ 如果你不嫌麻烦, 删除也行 ), 运行一下试试…… 显然, 这没问题……

(2) 把 13~15 行加上…… 先看 13 行, 有意思吧, SmallPig 可以变成 Pig…… 废话~ 小猪当然也是猪了…… 这个故事告诉我们: 子类对象可以非常顺畅地转化为父类(祖先)的实例; 再看 15 行, 不用运行, Eclipse已经告诉你有问题了…… 既然 SmallPig 已经变成 Pig ( p2 )了, 那自然就不再会有play方法了, 因为 Pig 中压根就没定义过play方法. 这个故事告诉我们: 子类对象变成父类(祖先)实例后, 子类中新增的属性和方法将不再可用. 严格来说是不可视, 并未真正消失.

(3) 把 15 行注释掉, 加上 17 ~ 19 行. 嘿嘿~ 猪 p2 被”强制转换”成小猪了, 这是允许的. 并且, 在第 19 行它又可以玩球了…… 运行试试…… 这个故事又告诉我们: (a) 类型是可以强制转换的, 语法见代码; (b) 子类对象变成父类(祖先)实例后, 新增属性和方法会暂时不可用, 但再次转回子类后, 那些属性/方法就又可用了.

(4) 21 行是一条华丽丽的分隔线…… 歇会儿~ 喘口气, 想明白了继续……

(5) 加上 23 ~ 25 行, 25 行出错了, 这个错误比较低级, 自己应该想得明白, 我就不侮辱你的智商了……

(6) 注释掉 25 行, 加上 27~29 行, 又出错! 为什么? 因为…… 父类对象不能”顺畅地”直接转换为子类的实例 ( 与 13 行对比一下…… ). 虽然我们可以硬生生地拿着q2play (第 29 行), 但 27 行编译都过不了, 那就别提运行了……

(7) 注释掉 27~29 行, 加上 31~33 行…… 呵呵, “强制转换”真的很牛吧~ 第 31 行一切正常, 编译也是可以通过的. 但是…… 运行试试…… 会在 33 行抛出异常 ( 虽然编译没错, 这叫运行时异常 ). 这个故事告诉我们: “强制转换”真的很强, 它能让编译器闭嘴, 但是…… 猪就是猪, 它绝对不可能变成小猪, 即使变成了小猪, 也是个半残废 (不会”play”).

不要怀疑自己的智商, 彻底把你绕晕一直是我追求的目标…… OMG~ 谁扔的鸡蛋……

Just Relax ! 慢慢想, 不能完全想明白也不要紧, 能想明白多少算多少. 多练习, 也许有一天你就会突然明白了, 现在实现看不懂上面那段 (1) ~ (7) 就当没看见就是了.


不知不觉中, 我们已经把面向对象三大特性 (封装、继承、多态) 最基本的实现形式学完了, 小朋友们是否学会了呢? 如果喜欢记得叫上爸爸妈妈一起来顶贴哦~

按照惯例, 给点温馨提示:

(1) Java中无论是原生的类 ( 如: String, Integer ), 还是你自己定义的类 ( 如: Pig, SmallPig ), 它们都有一个共同的祖先: Object, 这是所有”类”的家谱中的”根”. ( 如果在声明类时没有extends…, 那 Java 会自己帮你加上 extends Object).

自己抽空看看 Object 类里被预先定义了些什么方法和属性吧…

(2) 正因为 (1) 中所述, 有时我们要将两个”不相关”类的对象作为参数传递给一个函数时, 该函数的参数类型可选用Object. 但这只是权宜之计, 如果你的代码中真出现这样情况, “可能” 意示着那个函数设计得有问题 ( 违背了函数功能独立性原则, 因为你把两类不相关的参数传给同一个函数处理 ). 当然, 也不是说绝对不能使用 Object 作为形参, 所以说 “可能” 设计有问题.

(3) 判断两个变量是否”相等”时, 千万注意: 对于引用类型 ( 对象 ) , 比较运算符 “==” 判定的是这两个变量是否是 “同一个” ( 在内存中就是同一个空间 ), 而非判定两个变量的”值”是否”相等”. 同理, 运算符 “!=” 类似……

对于引用类型, 若要判定”值”是否”相等”, 一般使用equals方法. 当然, 对于我们自己定义的类, 必要时应覆盖Object类中已定义的equals方法.

对于非引用类型 ( 可简单理解为 Java 预定义的那些类型名以小写字母开头的类型, 如: int, boolean, char, long, double… ) 不存在前述问题. 可运行下面的代码, 比较一下就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.bailey.study.test;

public class Test {
public static void main(String[] args) {
String a = "1";
String b = new String("1");

if (a == b) {
System.out.println("==");
} else {
System.out.println("!=");
}

if (a.equals(b)) {
System.out.println("equal");
} else {
System.out.println("not equal");
}
}
}