Java 继承与多态

Java 继承与多态

继承的概念

面向对象的三个基本特征是:封装继承多态

  • 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式;
  • 继承:子类继承父类的成员,实现代码重用;
  • 多态:子类重写父类的方法,实现接口重用;

继承是类与类之间的关系,继承可以理解为一个类从另一个类获取方法和属性的过程;
Java 中类的继承使用 extends 关键字;Java 只支持单继承,不支持多继承;

在 Java 中,类与类之间只能单继承,但是一个类可以同时实现多个接口,并且接口与接口之间可以多继承;

被继承的类称为父类或基类,继承的类称为子类或派生类
派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能;

以下是两种典型的使用继承的场景:
1) 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能;
2) 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承;可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员;

比如,我们定义了一个基类 People,并且由基类派生出 Student 类:

Java 和 C++ 一样,派生类构造函数必须调用直接基类中的构造函数;
如果不显式的在派生类构造函数中调用直接基类构造函数,那么默认调用直接基类的无参构造函数,即super()
如果直接基类中没有无参构造函数,那么编译失败,所以建议在派生类的构造函数中都显式的调用直接基类构造函数!

对于需要 override 的派生类成员函数,建议都加上@Override伪代码,提示编译器检查 Override 的正确性;

Java 中没有 public、protected、private 继承方式之分,默认都是 public 继承,即不改变基类成员在派生类中的访问性;

哪些成员可以被继承

  • public、protected 成员函数和成员变量可以被继承;
  • private 成员函数不能被继承;private 成员变量可以被继承,但是在派生类中不可见;
  • 构造函数不能被继承;
  • final 类不能被继承;

static成员:与对象无关的函数或变量,存储在全局数据区,相当于访问性被限制的全局函数、全局变量;
final类:不能被派生,并且 final 类的成员函数都隐式的声明为了 final 成员,但是成员变量不会;
final方法:不能被重写;final属性:不能被修改,只读;final局部变量:不能被修改,只读;

super关键字

super 关键字与 this 类似,this 用来表示当前类的实例,super 用来表示父类;
super 用在子类中,通过.来获取父类的成员变量和方法;super 也可以用在子类的子类中,Java 能自动向上层类追溯;

super 关键字的功能:

  • 调用名字被屏蔽的基类成员;
  • 调用直接基类的构造方法(在派生类构造函数中调用,必须位于第一行,一个构造函数中只能有一个 super 语句);

例子:

多态和动态绑定

在 Java 中,父类的引用可以引用父类的实例,也可以引用子类的实例;
多态存在的三个必要条件:继承重写父类引用变量引用子类对象

绑定、静态绑定、动态绑定的概念:
1) 绑定
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来;
对 Java 来说,绑定分为静态绑定动态绑定;或者叫做前期绑定和后期绑定;

2) 静态绑定
在程序执行前方法已经被绑定,针对 Java 简单的可以理解为程序编译期的绑定;
在 Java 中,被 final、static、private 修饰的方法和构造方法是静态绑定的;

3) 动态绑定
在程序运行期间根据具体对象的类型进行方法绑定;
Java 提供了一些机制(类似 C++ 中虚函数表),可在运行期间判断对象的类型,并分别调用适当的方法;
也就是说,编译器此时依然不知道对象的实际类型以及要调用的方法主体,但方法调用机制能自己去调查,找到正确的方法主体;

例子:

instanceof运算符

多态性带来了一个问题,就是如何判断一个变量所实际引用的对象的类型;
C++ 使用RTTI运行时类型识别,Java 使用 instanceof 操作符;

instanceof 的语法为obj instanceof ClassName,运算结果为一个 bool 值;

向上转型和向下转型

在继承链中,我们将子类向父类转换称为“向上转型”,将父类向子类转换称为“向下转型”;

向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预;

1) 向上转型:
很多时候,我们会将变量定义为父类的类型,却引用子类的对象,这个过程就是向上转型;
程序运行时通过动态绑定来实现对子类方法的调用,也就是多态性(动态多态);

2) 向下转型:
然而有些时候为了完成某些父类没有的功能,我们需要将向上转型后的子类对象再转成子类,调用子类的方法,这就是向下转型;

注意:不能直接将父类的对象强制转换为子类类型,只能将向上转型后的子类对象再次转换为子类类型;

例子:

因为向下转型存在风险,所以在接收到父类的一个引用时,请务必使用 instanceof 运算符来判断该对象是否是你所要的子类;

static、final

static 关键字
static 可用于修饰成员变量和成员函数,不能修饰局部变量(C/C++ 可以)

  • 静态成员函数只能调用类的静态成员;
  • 静态成员一般通过类来访问,也可以通过对象来访问(不推荐);
  • 静态成员函数中不存在当前对象,因而不能使用 this,当然也不能使用 super;

final 关键字
在 Java 中,声明类、变量和方法时,可使用关键字 final 来修饰;
final 所修饰的数据具有“终态”的特征,表示“最终的”意思;具体规定如下:

  • final 修饰的类不能被继承;
  • final 修饰的方法不能被子类重写;
  • final 修饰的成员变量必须在声明的同时进行赋值,或者在构造函数中进行赋值;
  • final 修饰的局部变量即成为常量,只能赋值一次(可在定义的同时进行赋值,也可以在定义之后进行一次赋值);
  • final 修饰的引用类型变量不能指向其它对象;但可以改变对象的内容;

Object类

Java 中的 Object 类是所有类(包括数组)的父类,它提供了以下 11 个方法:

hashCode() 和 equals()
在重写 equals() 方法的同时,hashCode() 方法也必须被重写(同时编译器也会给出警告)。反之则没有要求。
为什么存在这样一个规定呢?为了支持所有基于散列的集合(包括 HashMap、HashSet、Hashtable)的正常运行

默认的 hashCode() 和 equals() 的实现:

  • int hashCode():根据对象的内存地址计算的一个散列值,相同的地址返回的散列值是一样的
  • boolean equals(Object obj):在方法内部,它仅仅返回this == obj的结果,比较的是内存地址

以 HashMap 为例,HashMap 检索一个 key 的步骤为:

  1. 根据 key.hashCode() 确定存储桶(即一个链表)
  2. 然后使用 key.equals(otherKey) 查找正确的元素

如果只重写了 equals() 方法,那么两个你认为相等的元素(equals() 判断)在第一步中就很有可能不会进入同一个存储桶,除非它们是同一个引用。
如果只重写了 hashCode() 方法,那么两个你认为相等的元素(hashCode() 判断)即使进入了同一个存储桶,也不一定在第二步中找到正确的位置,除非它们是同一个引用。

只重写 hashCode() 方法:

只重写 equals() 方法:

重写 hashCode() 和 equals() 方法:

clone() 方法
请参考 - java.lang.Cloneable 接口、浅拷贝、深拷贝