第四章 类的继承
上一章我们谈到了如何将现实中的概念映射为程序中的概念,我们谈了类以及类之间的组合,现实中的概念间还有一种非常重要的关系,就是 分类。分类有个根,然后往下不断细化,形成一个层次分类体系,这种例子是非常多的。
-
在自然世界中,生物有动物和植物,动物有不同的科目,食肉动物、食草动物、杂食动物等,食肉动物有狼、豹、虎等,这些又细分为不同的种类。
-
打开电商网站,在显著位置一般都有分类列表,比如家用电器、服装,服装有女装、男装,男装有衬衫、牛仔裤等。
计算机程序经常使用类之间的 继承关系来表示对象之间的分类关系。在继承关系中,有 父类和子类,比如动物类Animal和狗类Dog,Animal是父类,Dog是子类。父类也叫 基类,子类也叫 派生类。父类、子类是相对的,一个类B可能是类A的子类,但又是类C的父类。
之所以叫继承,是因为子类继承了父类的属性和行为,父类有的属性和行为子类都有。但子类可以增加子类特有的属性和行为,某些父类有的行为,子类的实现方式可能与父类也不完全一样。
使用继承一方面可以复用代码,公共的属性和行为可以放到父类中,而子类只需要关注子类特有的就可以了;另一方面,不同子类的对象可以更为方便地被统一处理。
本章详细介绍继承。我们先介绍继承的基本概念,然后详述继承的一些细节,理解了继承的用法之后,我们探讨继承实现的基本原理,最后讨论继承的注意事项,解释为什么说继承是把双刃剑,以及如何正确地使用继承。
4.1 基本概念
本节介绍Java中继承的基本概念,在Java中,所有类都有一个父类Object, 我们先来看这个类,然后主要通过图形处理中的一些简单例子来介绍继承的基本概念。
4.1.1 根父类Object
在Java中,即使没有声明父类,也有一个隐含的父类,这个父类叫Object。Object没有定义属性,但定义了一些方法,如图4-1所示。
本节我们会介绍toString()方法,其他方法我们会在后续章节中逐步介绍。toString()方法的目的是返回一个对象的文本描述,这哦方法可以直接被所有类使用。
比如,我们上一章介绍的Point类,可以这样使用toString方法:
Point p = new Point(2, 3);
System.out.println(p.toString());
输出类似这样:
Point@76f9aa66
这个是什么意思呢?@之前是类名,@之后的内容是什么呢?我们来看下toString()方法的代码:
public String toString(){
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
getClass().getName()返回当前对象的类名,hashCode()方法一个对象的哈希值,哈希我们在后续章节进一步介绍,这里可以理解为是一个整数,这个整数默认情况下,通常是对象的内存地址值,Integer.toHextString(hashCode()) 返回这个哈希值的十六进制表示。
为什么要这样写呢?写类名是可以理解的,表示对象的类型,而写哈希值则是不得已的,因为Object类并不知道具体对象的属性,不知道怎么用文本描述,但又需要区分不同对象,只能是写一个哈希值。
但子类是知道自己的属性的,子类可以 重写 父类的方法,以反应自己的不同实现。所谓重写,就是定义和父类一样的方法,并重新实现。
4.1.2 方法重写
上一章,我们介绍了一些图形处理类,其中有Point类,这次我们重写其toString()方法,如代码清单4-1所示
代码清单4-1 Point类:重写toString()方法
public class Point{
private int x;
private int y;
public Point(int x, int y){
this.x = x;
this.y = y;
}
public double distance(Point point){
return Match.sqrt(Math.pow(this.x-point.getX(), 2) + Math.pow(this.y-point.getY(), 2));
}
public int getX(){
return x;
}
public int getY(){
return y;
}
@Override
public String toString(){
return "(" + x + ","+y+")";
}
}
toString()方法前面有一个@Override,这表示toString()这个方法时重写的父类的方法,重写后的方法返回Point的x和y坐标的值。重写后,将调用子类的实现。比如,如下代码的输出就变成了(2,3)。
Point p = new Point(2, 3);
System.out.println(p.toString());
4.1.3 图形类继承体系
接下来,我们以一些图形处理中的例子来进一步解释。先来看一些图形的例子,如图4-2所示。
4.1.4小结
本节介绍了继承和多态的基本概念。
- 每个类有且只有一个父类,没有声明父类的,其父类为Object, 子类继承了父类非private的属性和方法,可以增加自己的属性的方法,以及重写父类的方法实现。
- new过程中,父类先进行初始化,可通过super调用父类相应的构造方法,没有使用super的情况下,调用父类的默认构造方法。
- 子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法。
- 子类对象可以赋值给父类引用变量,这叫多态;实际执行调用的是子类实现,这叫动态绑定。
继承和多态的基本概念是比较简单的,子类继承父类,自动拥有父类的属性和行为,并可扩展属性和行为,同时,可重写父类的方法以修改行为。但关于继承,还有很多细节,我们下一节继续讨论。
4.2 继承的细节
本节探讨继续的一些细节,具体包括:
- 构造方法
- 重名与静态绑定
- 重载和重写
- 父子类型转换
- 继承访问权限(protected)
- 可见性重写
- 防止继承(final)
下面我们逐个介绍。
4.2.1 构造方法
4.2.2 重名与静态绑定
4.1节我们提到,子类可以重写父类非private的方法,当调用的时候,会动态绑定,执行子类的方法。那实例变量、静态方法和静态变量呢?它们可以重名吗?如果重名,访问的是哪一个呢?
重名是可以的,重名后实际上有两个变量和方法。private变量和方法只能在类内访问,访问的也永远是当前类的,即:在子类中访问的是子类的;在父类中访问的是父类的,它们只是碰巧名字一样而已,没有任何关系。
public变量和方法,则要看如何访问它。在类内,访问的是当前类的,但子类可以通过super.明确指定访问父类的。在类外,则要看访问变量的静态类型:静态类型是父类,则访问父类的变量的方法;静态类型是子类,则访问的是子类的变量和方法。我们来看个例子,这是基类代码:
public class Base{
public static String s = "static_base";
public String m = "base";
public static void staticTest(){
System.out.println("base static: " + s);
}
}
定义了一个public静态变量s, 一个public实例变量m, 一个静态方法staticTest。这是子类代码:
public class Child extends Base{
public static String s = "child_base";
public String m = "child";
public static void staticTest(){
System.out.println("child static: " + s);
}
}
子类定义了和父类重名的变量和方法。对于一个子类对象,它就有了两份变量和方法,在子类内部访问的时候,访问的是子类的,或者说,子类变量和方法隐藏了父类对应的变量和方法,下面看一下外部访问的代码:
public static void main(String[] args){
Child c = new Child();
Base b = c;
System.out.println(b.s);
System.out.println(b.m);
b.staticTest();
System.out.println(c.s);
System.out.println(c.m);
c.staticTest();
}
以上代码创建了一个子类对象,然后将对象分别赋值给了子类引用对象c和父类引用变量b,然后通过b和c分别引用变量和方法。这里需要说明的是,静态变量和静态方法一般通过类名直接访问,但也可以通过类的对象访问。程序输出为:
static_base
base
base static: static_base
child_base
child
child_static: child_base
当通过b(静态类型Base)访问时,访问的是Base的变量和方法,当通过c(静态类型Child)访问时,访问的是Child的变量和方法,这称之为 静态绑定,即访问绑定到变量的静态类型。静态绑定在程序编译阶段即可决定,而动态绑定则要等到程序运行时。实例变量、静态变量、静态方法、private方法,都是静态绑定的。