Java基础知识之继承
本文内容参考《Java核心技术 卷Ⅰ》
类、超类和子类
定义子类
在Java中,通常使用extends
关键字表示继承。
1 | public class Manager extends Employee { |
extends
表明正在构造的新类派生于一个已存在的类。已存在的类被称为超类、基类或父类;新类被称为子类、派生类或孩子类。子类会自动继承超类的字段和方法,不过子类不能直接访问或使用超类的私有字段和方法。定义子类时,只需指出子类与超类的不同之处。
覆盖方法
超类中的一些方法在子类中不适用,我们可以将该方法进行覆盖,假设超类Employee
有一个方法getSalary()
,我们在Manager
中也写一个和超类相同方法签名的方法
1 | public class Manager extends Employee { |
此时我们如果使用Manager
的对象使用getSalary()
,调用的是子类的方法,而不是超类的方法。如果想要在子类中调用与超类同名的方法,可以向如下这样使用super
关键字
1 | public class Manager extends Employee { |
在覆盖一个方法时,子类的方法不能低于超类方法的可见性。如果超类方法使用的是public
,子类方法也必须是public
。
子类构造器
由于子类不能访问超类的私有字段,所以必须通过一个构造器来初始化私有字段,我们可以利用super
语法调用该构造器。使用super
调用构造器必须是子类构造器的第一条语句。
1 | public Manager(String name, double salary, int year, int month, int day) { |
如果子类构造器没有显式地调用超类的构造器,将自动地调用超类的无参数构造器。如果超类没有无参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,Java编译器会报错。
假设现在Manager
类的getSalary
方法会自动将奖金添加到工资里
1 | // 创建一个经理,并设置奖金 |
执行程序后会输出
1 | Carl Cracker 85000.0 |
可以看到staff[1]
和staff[2]
只输出了基本薪水,因为它们是Employee
对象,而staff[0]
是Manager
对象,所以他输出了基本薪水+奖金。
需要注意,在循环中尽管e
被声明为Employee
类型,但实际上e
既可以引用Employee
类型的对象,也可以调用Manager
类型的对象。当e
引用Employee
对象时,e.getSalary()
调用的是Employee
类中的getSalary
方法;当e
引用Manager
对象时,e.getSalary()
调用的是Manager
类中的getSalary
方法。虚拟机知道e
的实际引用对象类型,因此能正确调用相应的方法。
一个对象变量可以指示多种实际类型的现象称为多态。在运行时能够自动地选择合适的方法,称为动态绑定。
多态
可以将子类的对象赋给超类变量,但不能把超类的引用赋给子类变量。
1 | Employee e; |
理解方法调用
假设要调用x.f(args)
,隐式参数x
声明为类C
的一个对象。下面是调用过程的详细描述:
-
编译器査看对象的声明类型和方法名。需要注意的是:有可能存在多个名字为
f
,但参数类型不一样的方法。例如,可能存在方法f(int)
和方法f(String)
。编译器将会 一 一 列举所有C
类中名为f
的方法和其超类中访问属性为public
且名为f
的方法(超类的私有方法不可访问)。至此, 编译器已获得所有可能被调用的候选方法。
-
接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为
f
的方法中存在一个与提供的参数类型完全匹配,就选择个方法。这个过程被称为重载解析。例如,对于调用x.f("Hello")
来说, 编译器将会挑选f(String)
,而不是f(int)
。由于允许类型转换(int
可以转换成double
,Manager
可以转换成Employee
,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法, 或者发现经过类型转换后有多个方法与之匹配, 就会报错。 -
如果是
private
方法、static
方法、final
方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在我们列举的示例中,编译器采用动态绑定的方式生成一条调用f(String)
的指令。 -
当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与
x
所引用对象的实际类型最合适的那个类的方法。假设x
的实际类型是D
,它是C
类的子类。如果D
类定义了方法f(String)
,就直接调用它;否则,将在D
类的超类中寻找f(String)
,以此类推。每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。 这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索D类的方法表,以便寻找与调用
f(Sting)
相匹配的方法。这个方法既有可能是D.f(String)
,也有可能是X.f(String)
,这里的X
是D
的超类。这里需要提醒一点,如果调用super.f(param)
,编译器将对隐式参数超类的方法表进行搜索。
动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类Executive
,并且变量e
有可能引用这个类的对象,我们不需要对包含调用e.getSalary()
的代码进行重新编译。如果e
恰好引用一个Executive
类的对象,就会自动地调用Executive.getSalary()
方法。
阻止继承:final类和方法
不允许拓展的类称为final
类。假设希望阻止人们派生Executive
类的子类,可以如下声明
1 | public final class Executive extends Manager { |
类中的某个特定方法也可以被声明为final
。如果这样做,子类就不能覆盖这个方法。例如
1 | public class Employee { |
如果一个类被声明为final
,其中的方法会自动声明为final
,而字段不会。
强制类型转换
进行强制转换的唯一原因:要在暂时忽视对象的实际类型之后使用使用对象的全部功能。
需要注意:
- 只能在继承层次内进行强制类型转换。
- 在将超类强制转换成子类之前,应该使用
instanceof
进行检查。
抽象类
自上而下在类的继承层次结构中上移,位于上层的类更具有一般性。人们只将它作为派生其他类的基类,而不是用来构造特定的实例。可以利用abstract
关键字将类定义为抽象类
1 | public abstract class Person { |
包含一个或多个抽象方法的类本身必须被声明为抽象的。抽象类还能包含字段和具体方法。抽象方法充当占位方法的角色,它们在子类中具体实现。
受保护访问
- 仅对本类可见 —— private。
- 对外部完全可见 —— public。
- 对本包和所有子类可见 —— protected。
- 对本包可见 —— 默认,不需要修饰符。
泛型数组列表
声明数组列表
声明和构造一个保存Employee
对象的数组列表
1 | ArrayList<Employee> staff = new ArrayList<Employee>(); |
可以省去右边的泛型参数
1 | ArrayList<Employee> staff = new ArrayList<>(); |
访问数组列表元素
我们不能用[]
语法访问或修改数组元素,而要使用get
或set
方法。假如要设置第i
个元素,可以使用
1 | staff.set(i, harry); |
它等价于对数组a
的元素进行赋值
1 | a[i] = harry; |
对象包装器与自动装箱
有时需要将int
这样的基本类型转为对象,所有基本类型都有与之对应的类。Integer
类对应的基本类型是int
,通常这些类被称为包装器。包装器类是不可变的,而且不能够派生他们的子类。加入要定义一个整型数组列表,不能够在尖括号中用基本类型,也就是说不能写成ArrayList<int>
的形式,我们可以这样声明
1 | ArrayList<Integer> a = new ArrayList<>(); |
向a中添加元素
1 | a.add(3); |
它将自动变成
1 | a.add(Interger.valueOf(3)); |
这种变换被称为自动装箱。相反地,如果将Integer
对象赋给一个int
值时,将会自动拆箱。
反射
能够分析类能力的程序称为反射。反射机制可以用来:
- 在运行时分析类的能力。
- 在运行时检查对象,例如,编写一个适用于所有类的
toString
方法。 - 实现泛型数组操作代码。
- 利用
Method
对象,这个对象很像C++
中的函数指针。
Class类
Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。Class
类保存了这些信息。
Object
类中的getClass()
方法会返回一个Class
类型的实例
1 | Employee e; |
Class
对象会描述一个特定类的属性,最常用的方法是getName()
,它将返回类的名字
1 | System.out.println(e.getClass().getName() + " " + e.getName()); |
如果e
是员工,会输出:Employee Harry Hacker。
如果e
是经理,会输出:Manager Harry Hacker。
如果类在一个包里,包的名字也作为类名的一部分。
还可以使用静态方法forName()
获得类名对应的Class
对象。
1 | String className = "java.util.Random"; |
还有一种获取类对象的方法是利用T.class
1 | Class cl = Random.class |
虚拟机为每一个类型管理一个唯一的Class
对象。因此,可以利用==
运算符实现两个类对象的比较
1 | if (e.getClass() == Employee.class) ... |
如果有一个Class
类型的对象。可以用它构造类的实例。调用getConstructor()
方法将得到一个Constructor
类型的对象,然后使用newInstance()
方法来构造一个实例
1 | Class cl = forName("java.util.Random"); |
利用反射分析类的能力
检查类的结构是反射机制最重要的内容。
在java.lang.reflect
包中有三个类Field
、Method
和Constructor
分别用于描述类的字段、方法和构造器。这三个类都有getName()
的方法,用来返回字段、方法或构造器的名称。Field
类有一个getType()
方法,用来返回字段类型的一个对象,这个对象类型是Class
。Method
和Constructor
类有报告参数类型的方法,Method
类还有报告返回类型的方法。这三个类都有一个getModifiers()
方法,用不同的整数描述所使用的修饰符。
Class
类中的getFields()
、getMethods()
和getConstructor()
方法分别返回这个类中的公共字段、方法和构造器的数组,其中包括超类的公共成员。getDeclareFields()
、getDeclareMethods()
和getDeclareConstructors()
方法返回类中声明的全部字段、方法和构造器的数组,其中包含私有成员、包成员和受保护的成员,但不包括超类的成员。
继承的设计技巧
- 将公共操作和字段放在超类中。
- 不要使用受保护的字段。
- 使用继承实现"is-a"的关系。
- 除非继承的所有方法都有意义,否则不要使用继承。
- 在覆盖方法时,不要改变预期的行为。
- 使用多态,而不要使用类型信息。
- 不要滥用反射。