Java
Java
目录
类的重用 - 继承(inheritance)
继承 - 类继承(类、超类和子类)
一些概念
- 已存在的类:称为超类(superclass)、基类(base class)或父类(parent class)
- 新类:称为子类(subclass)、派生类(derived class)或孩子类(child class)
- 术语习惯:Java通用术语 超类和子类,C++通常用术语 父类和子类
- 继承层次
- 在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)
- 通常,一个祖先类可以拥有多个子孙继承链
- is-a
- 有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则
- “is-a”规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。 例如,可以将一个子类的对象赋给超类变量
定义子类
java (在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承)
public class Manager extends Employee { // ... }
C++
class A : public APerent {}
多继承(不支持)
Java不支持多继承
多态(替换原则)
在Java程序设计语言中,对象变量是多态的。 一个父类变量既可以引用一个父类对象,也可以引用一个该类的任何一个子类对象
在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。
可能引起的bug:注意要规避以下写法:
Manager[] managers = new Manager[10]; // 经理列表
Employee[] staff = managers; // 普通雇员列表
staff[0] = new Employee("Harry Hacker");// 不报错,但危险
// 在这里,staff[0]与manager[0]引用的是同一个对象,也就是说staff[0]经理列表中居然混杂了一个普通雇员
// 经验:所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中
继承 - 方法继承
覆盖方法、选择方法版本
继承(inheritance):子类自动地继承了父类中的方法
覆盖(Override):提供一个新的方法来覆盖(Override)超类中的原方法
如果希望调用超类而不是子类的方法
Java:使用特定的关键字super解决这个问题
super.getSalary()
C++:使用
::
父类名::getSalary()
注意:有些人认为super与this引用是类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字
注意:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public
子类构造器(super关键字)
C++
A::A():PerentA(){}
// 一般使用成员初始化列表语法(效率高)来调用基类构造函数
// 缺省时:
// 如果省略基类的构造函数,则程序自动使用默认的基类构造函数
Java
public Manager(String name, double salarym, int year, int month, int day)
{
super(name, salary, year, month, day);
// 使用super调用构造器的语句必须是子类构造器的【第一条】语句
// 这里的关键字super具有不同的含义
// 是“调用超类Employee中含有n、s、year、month和day参数的构造器”的简写形式
bonus = 0;
}
// 缺省时:
// 如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器
// 如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。
动态性
动态绑定
- 一个对象变量(例如,变量e)可以指示多种实际类型的现象被称为多态(polymorphism)
- 在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)
- C:默认是静态绑定
- Java:不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征,可以将它标记为final
动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类Executive,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译
方法调用的原理
理论解析
调用过程的详细描述:
1)编译器查看对象的声明类型和方法名 编译器获得所有可能被调用的候选方法
假设调用
x.f(param)
,且隐式参数x声明为C类的对象 需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String) 编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。2)编译器查看调用方法时提供的参数类型 重载解析(overloading resolution) 编译器获得需要调用的方法名字和参数类型
如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution) 例如,对于调用 x.f(“Hello”) 来说,编译器将会挑选 f(String) ,而不是 f(int) 。由于允许类型转换(int可以转换成double,Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。
3)静态绑定或动态绑定(static binding / dynamic binding)
如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。
与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。例如编译器采用**动态绑定(dynamic binding)**的方式生成一条调用 f(String) 的指令。
4)当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
假设x的实际类型是D,它是C类的子类。如果D类定义了方法 f(String) ,就直接调用它;否则,将在D类的超类中寻找 f(String) ,以此类推。
每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索D类的方法表,以便寻找与调用 f(Sting) 相匹配的方法。这个方法既有可能是 D.f(String) ,也有可能是 X.f(String) ,这里的X是D的超类。这里需要提醒一点,如果调用 super.f(param) ,编译器将对隐式参数超类的方法表进行搜索。
实战举例
class Employee
{
public String getSalary() {}
}
class Manager extends Employee
{
// ...
}
Employee e;
e = new Employee( , , );
e = new Manager( , , );
在运行时,调用 e.getSalary() 的解析过程为:
1)首先,虚拟机提取e的实际类型的方法表。既可能是Employee、Manager的方法表,也可能是Employee类的其他子类的方法表。
2)接下来,虚拟机搜索定义getSalary签名的类。此时,虚拟机已经知道应该调用哪个方法。
3)最后,虚拟机调用方法。
关闭动态性(final)
多态性的默认
- Java:默认所有方法具有多态性,方法默认为动态绑定
- C++:默认所有方法不具有多态性,方法默认为静态绑定
- 所以除了禁止类或类方法被继承或重写外,Java中使用final还有一个功能是关闭动态绑定 在早期的Java中,有些程序员为了避免动态绑定带来的系统开销而使用final关键字 内联优化:并且如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联(inlining)
继承 - 其他
阻止继承:final类和方法
Java
作用1:不允许类被继承 例如,假设希望阻止人们定义Executive类的子类,就可以在定义这个类的时候,使用final修饰符声明
public final class Executive extends Manager { ... }
作用2:不允许类方法被复写 类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)
public class Emplyee { ... public final String getName() { return name; } ... }
C++11,final关键字
作用1:不允许类被继承
struct Basel final{}; // 不让别人继承自己 struct Derived1:Base1 {}; // 此时会报错
作用2:不允许类方法被复写
struct Base2{ virtual void f() final; // 不允许被复写 }; struct Derived2:Base2{ void f(); }
用途:将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。例如在Java中:
- Calendar类中的getTime和setTime方法都声明为final 这表明Calendar类的设计者负责实现Date类与日历状态之间的转换,而不允许子类处理这些问题
- String类也是final类 这意味着不允许任何人定义String的子类。 换言之,如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。
方法/域的访问权限(保护访问)
保护访问:
- 人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为protected
- 在实际应用中,要谨慎使用protected属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护域,由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下,如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员。这违背了OOP提倡的数据封装原则。
- 示例:Object类中的clone方法
与C++不同
Java
// 【四个访问修饰符】 priveate // 仅对本类可见 public // 对所有类可见 protected // 对本包和所有子类可见 默认 // 对本包可见
C++
- 基本同上
总结
- Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。这与C++中的保护机制稍有不同
- Java中的protected概念要比C++中的安全性差
其他
this和super的小总结
- 关键字this有两个用途
- 一是引用隐式参数
- 二是调用该类其他的构造器
- super关键字也有两个用途
- 一是调用超类的方法
- 二是调用超类的构造器
- 在调用构造器的时候,这两个关键字的使用方式很相似。 调用构造器的语句只能作为另一个构造器的第一条语句出现。 构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。
【功能扩展】继承
【功能扩展】继承 x 转换,强制类型转换
double x = 3.405;
int nx = (int) x;
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss; // 代换原则
Manager boss = (Manager) staff[0];
应该养成这样一个良好的程序设计习惯:在进行类型转换之前,先查看一下是否能够成功地转换
在一般情况下,应该尽量少用类型转换和instanceof运算符
if (staff[1] instanceof Manager) // 查看是否能成功类型转换
{
boss = (Manager) staff[1];
...
}
与C++不同
类型转换 Java使用的类型转换语法来源于C语言,但处理过程却有些像C++的dynamic_cast操作。以下两种写法等价
Java
Manager boss = (Manager) staff[1]; // Java
C++
Manager* boss = dynamic_cast<Manager*>(staff[1]); // C++
判断是否能转换
Java:当类型转换失败时,抛出一个异常(有点像C++中的引用(reference)转换)
if (staff[1] instanceof Manager) // Java,需要将instanceof运算符和类型转换组合起来使用 { boss = (Manager) staff[1]; ... }
C++:当类型转换失败时,生成一个null对象
Manager* boss = dynamic_cast<Manager*>(staff[1]); // C++,直接转换 if (boss != NULL) ...