第5章 继承
继承(inheritance)的基本思想是,可以基于已有的类创建新的类。继承已存在的类就是复用(继承)这些类的方法,而且可以增加一些新的方法和字段,使新类能够适应新的情况。
反射(reflection)是指在程序运行期间更多地了解类及其属性的能力。
5.1 类、超类和子类
-
Java中,使用关键字extends表示继承。public class Manager extends Employee { // added methods and fields } -
这个已存在的类称为超类
(superclass)、基类(base class)或父类(parent class),新类称为子类(subclass)、派生类(derived class)或孩子类(child clsaa)。 -
应该将最一般的方法放在超类中,将更特殊的方法放在子类中。在子类中可以增加字段、增加方法或覆盖超类的方法,不过继承绝对不会删除任何字段或者方法。
-
子类不能直接访问超类的私有字段,必须使用关键字
super来调用超类的公共接口。 -
子类可以提供一个新的方法来覆盖超类中同名的方法:
public double getSalary () { double baseSalary = super.getSalary(); return baseSalary + bonus; } -
子类构造器:
public Manager (String name, double salary, int year, int month, int day) { super(name, salary, year, month, day); bonus = 0; }使用
super调用构造器的语句必须是子类构造器的第一条语句。 -
如果子类的构造器没有显式的调用超类的构造器,将自动的调用超类的无参数构造器。
-
关键字
this的两种含义:- 指示隐式参数的引用
- 调用该类的其他构造器
关键字
super的两种含义:- 调用超类的方法
- 调用超类的构造器
-
一个对象变量(例如,变量
e)可以指示多种实际类型的现象称为多态(polymorphism)。在运行时能够自动地选择适当的方法,称为动态绑定(dynamic binding)。Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15); boss.setBonus(5000); var staff = new Employee[3]; staff[0] = boss; staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1); staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15); for (Employee e : staff) System.out.println(e.getName + " " + e.getSalary); -
继承并不仅限于一个层次。由一个公共超类派生出来的所有类的集合称为继承层次
(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链(inheritance chain)。 -
"is-a"规则的另一种表述是替换原则,它指出程序中出现超类对象的任何地方都可以使用子类对象替换。在Java程序设计语言中,对象变量是多态的。不过,不能将超类的引用赋给子类变量。 -
假设要调用
x.f(args),隐式参数x声明为类C的一个对象。方法调用的过程如下:- 编译器查看对象的声明类型和方法名。
- 编译器要确定方法调用中提供的参数类型。
- 如果是
private方法、static方法、final方法或者构造器,那么编译器将可以准确的知道应该调用哪个方法。这称为静态绑定(static binding)。 - 程序运行并且采用动态绑定方法时,虚拟机必须调用与
x所引用对象的实际类型对应的那个方法。
-
动态绑定有一个非常重要的特性:无须对现有的代码进行修改就可以对程序进行扩展。
-
运行子类将覆盖方法的返回类型修改为原返回类型的子类型。
-
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。
-
不允许被扩展的类被称为
final类。public final class Executive extends Manager { ... } -
如果类中的某个特定方法声明为
final,那么子类就不能覆盖这个方法。 -
如果将一个类声明为
final,其中的方法会自动地成为final,字段不会。 -
进行强制类型转换地唯一原因是:要在暂时忽视对象的实际类型之后使用对象的全部功能。
-
将一个子类的引用赋给一个超类变量,编译器是允许的。但是将一个超类的引用赋给一个子类变量时,就承诺过多了,必须进行强制类型转换。
-
强制类型转换的原则:
- 只能在继承层次内进行强制类型转换
- 在将超类强制转换成子类之前,应该使用
instanceof进行检查
if (staff[1] instanceof Manager) { boss = (Manager) staff[1]; ... } -
包含一个或多个抽象方法的类本身必须被声明为抽象的。
public abstract class Person { ... public abstract String getDescription() {}; }除了抽象方法之外,抽象类还可以包含字段和具体方法。
即使不含抽象方法,也可以将类声明为抽象类。
抽象类不能实例化,即不能创建这个类的对象。
可以定义一个抽象类的对象变量,但是这个变量只能引用非抽象子类的对象。
-
抽象方法充当占位方法的角色,它们在子类中具体实现。
扩展抽象类有两种选择:
- 在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样子类也必须标记为抽象类。
- 定义全部方法,子类就不是抽象类了。
-
Java中的4个访问控制修饰符:- 仅对本类可见 ——
private - 对外部完全可见 ——
public - 对本包和所有子类可见 ——
protected - 对本包可见 —— 默认(很遗憾),不需要修饰符
- 仅对本类可见 ——
5.2 Object类:所有类的超类
-
Object类是Java中所有类的始祖,在Java中每个类都扩展了Object。 -
可以使用
Object类型的变量引用任何类型的对象。在Java中,只有基本类型不是对象,例如,数值、字符和布尔类型的值。 -
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了
Object类。Object obj = new Employee("Harry Hacker", 50000); Employee[] staff = new Employee[10]; obj = staff; // OK obj = new int[10]; // OK -
Object类中实现的equals方法可以确定两个对象引用是否相等。如果两个对象引用是相等的,这两个对象就相等。如果对象可能为null,可以使用Objects.equals方法进行判断。 -
在子类中定义
equals方法时,可以首先调用超类的equals,然后再比较子类中新增的实例字段。 -
编写一个完美的
equals方法的建议:- 显式参数命名为
otherObject,稍后需要将它强制转换成另一个名为other的变量。 - 检测
this与otherObject是否相等:if (this == otherObject) return false; - 检测
otherObject是否为null,如果是null,则返回false。if (otherObject == null) return false; - 比较
this与otherObject的类。如果equals的语义可以在子类中改变,就使用getClass检测:
如果所有的子类都有相同的相等性语义,可以使用if (getClass() != otherObject.getClass()) return false;instanceof检测:if (!(otherObject instanceof ClassName)) return false; - 将
otherObject强制转换为相应类类型的变量:ClassName other = (ClassName) otherObject; - 现在根据相等性概念的要求来比较字段。使用
==比较基本类型字段,使用Objects.equals比较对象字段。如果所有的字段都匹配,就返回true;否则返回false。return field1 == other.field1 && Objects.equals(field2, other.field2) && ...;
- 显式参数命名为
-
散列码
(hash code)是由对象导出的一个整型值。散列码是没有规律的。由于
hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值由对象的存储地址得出。字符串的散列码是由内容导出的。
-
Employee类的hashCode方法:public class Empoyee { public int hashCode() { return 7 * name.hashCode() + 11 * new Double(salary).hashCode() + 13 * hireDay.hashCode(); } ... }进阶版:
- 使用
null安全的方法Objects.hashCode,如果参数为null,这个方法会返回0。 - 使用静态方法
Double.hashCode来避免创建Double对象。
public int hashCode() { return 7 * Objects.hashCode(name) + 11 * Double.hashCode(salary) + 13 * Objects.hashCode(hireDay); }再次进阶版:
- 使用
Objects.hash方法
public int hashCode() { return Objects.hash(name, salary, hireDay); } - 使用
-
两个相等的对象要求返回相等的散列码。
-
Object类有toString方法,绝大多数(但不是全部)的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的字段值。 -
设计的子类应该定义自己的
toString方法,并加入子类的字段。 -
随处可见
toString方法的主要原因是:只要对象与一个字符串通过操作符"+"连接起来,Java编译器就会自动地调用toString方法来获得这个对象的字符串描述。 -
如果
x是一个任意对象,并调用:System.out.println(x);println方法就会简单地调用x.toString(),并打印输出得到的字符串。 -
打印数组可以调用静态方法:
Arrays.toString。打印多维数组,需要调用Arrays.deepToString静态方法。
5.3 泛型数组列表
-
ArrayList是一个有类型参数的泛型类。ArrayList类类似于数组,但在添加或删除元素时,可以自动调整大小。 -
声明数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();在
Java 10中,可以使用var关键字,以避免重复写类名:var staff = new ArrayList<Employee>();如果没有使用
var关键字,可以省去右边的类型参数:ArrayList<Employee> staff = new ArrayList<>();这称为“菱形”语法。
-
add方法可以将元素添加到数组列表中,如果内部数组已经满了,数组列表就会自动创建一个更大更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。 -
如果已经知道或能够估计除数组可能存储的元素数量,就可以在填充数组之前调用
ensureCapacity方法。 -
size方法将返回数组列表中包含的实际元素个数。 -
trimToSize方法将存储块的大小调整为保存当前元素数量所需要的存储空间,垃圾回收器将回收多余的存储空间。 -
add方法为数组列表增加新元素,get方法获取第i个元素,set方法可以替换数组中已经加入的元素,remove方法从数组列表中删除一个元素。使用toArray方法可以将数组元素拷贝到一个数组中。 -
可以使用
for each循环遍历数组列表的内容。 -
出于兼容性的考虑,编译器检查到没有发现违反规则的现象之后,就将所有的类型化数组列表转换成原始
ArrayList对象。
5.4 对象包装器与自动装箱
-
所有的基本类型都有一个与之对应的类:
IntegerL、Long、Float、Double、Short、Byte、Character、Boolean(前6个类派生于公共的超类Number),这些类称为包装器。包装器类是不可变的,一旦构造了包装器,就不允许更改包装在其中的值。同时,包装器类还是final,因此不能派生它们的子类。 -
加入定义一个整型数组列表,尖括号中的类型参数不允许是基本类型,但是可以使用包装器类。
var list = new ArrayList<Integer>(); -
自动装箱:
list.add(3);会自动地变换成:
list.add(Integer.valueOf(3)); -
当将一个
Integer对象赋给一个int值时,将会自动地拆箱:int n = list.get(i);会转换成:
int n = list.get(i).intValue(); -
自动装箱和拆箱也适用于算数表达式。
Integer n = 3; n++; -
比较两个包装器对象时要调用
equals方法。 -
在一个条件表达式中混合使用
Integer和Double类型,Integer值就会拆箱,提升为double,再装箱成Double。 -
装箱和拆箱是编译器要做的工作,而不是虚拟机。
-
使用静态方法
parseInt可以将字符串转换成整型。int x = Integer.parseInt(s);
5.5 参数数量可变的方法
-
printf方法的定义:public class PrintStream { public PrintfStream printf(String fmt, Object... args) {return format(fmt, args)}; }printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组,其中保存着所有其他参数。 -
如果一个已有方法的最后一个参数是数组,可以把它重新定义为有可变参数的方法,而不会破坏任何已有的代码。
5.6 枚举
-
一个典型的例子:
public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};这个声明定义的类型是一个类,它刚好有4个实例,不可能构造新的对象。
-
枚举的构造器总是私有的。可以省略
private修饰符。如果声明一个enum构造器为public或protected,会出现语法错误。
5.7 反射
-
能够分析类能力的程序称为反射
(reflective),反射可以用来:- 在运行时分析类的能力
- 在运行时检查对象
- 实现泛型数组操作代码
- 利用
Method对象
-
在程序运行期间,
Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。保存这些信息的类名为Class。 -
获得
Class类对象的三种方法:Object类中的getClass()方法将会返回一个Class类型的实例。Employee e; ... Class cl = e.getClass();- 可以使用静态方法
forName获得类名对应的Class对象。
无论何时使用String className = "java.util.Random"; Class cl = Class.forName(className);forName方法,都应该提供一个异常处理器。 - 如果
T是任意类型的Java类型(或void关键字),T.class将代表匹配的类对象。
一个Class cl1 = Random.class; Class cl2 = int.class; Class cl3 = Double[].class;Class对象实际上表示的是一个类型,这可能是类,也可能不是类。
Class类实际上是一个泛型类。例如,Employee.class的类型是Class<Employee>。
-
虚拟机为每个类型管理一个唯一的
Class对象。因此,可以利用==运算符实现两个类对象的比较。if (e.getClass == Employee.class) ... -
如果有一个
Class类型的对象,可以用它构造类的实例。调用getConstructor方法将得到一个Constructor类型的对象,然后使用newInstance方法来构造一个实例。String className = "java.util.Random"; // or any other name of a class with // a no-arg constructor Class cl = Class.forName(className); Object obj = cl.getConstructor().newInstance();如果这个类没有无参数的构造器,
getConstructor方法会抛出一个异常。 -
异常有两种类型:
- 检查型
(checked)异常:编译器将会检查程序员是否知道这个异常并做好准备来处理后果。 - 非检查型
(unchecked)异常:越界错误或访问null引用等。编译器并不期望程序员为这些异常提供处理器,而是应该集中精力避免这些错误的发生。
- 检查型
-
异常处理最简单的一个策略:
如果一个方法包含一条可能抛出检查型异常的语句,则在方法名上增加一个
throws子句。调用这个方法的任何方法也都需要一个throws声明。这也包括main方法。如果一个异常确实出现,main方法将终止并提供一个堆栈轨迹。public static void doSomethingWithClass(String name) throws ReflectiveOperationException { Class cl = Class.forName(name); // might throw exception do something with cl } -
在
Java中,与类有关联的文件被称为资源。Class类提供了一个很有用的服务可以查找资源文件:- 获得拥有资源的类的
Class对象,例如,ResourceTest.class。 - 有些方法,如
ImageIcon类的getImage方法,接受描述资源位置的URL。则要调用URL url = cl.getResource("about.gif"); - 否则,使用
getResourceAsStream方法得到一个输入流来读取文件中的数据。
- 获得拥有资源的类的
-
利用反射分析类的能力
在
java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的字段、方法和构造器。Field类的常用方法:getName、getType、getModifiers。Constructor类的常用方法:getName、getModifiers、getParameterTypes。Method类的常用方法:getName、getModifiers、getReturnType、getParameterTypes。
可以利用
java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整数。如isPublic、isPrivate、isFinal,也可以使用Modifier.toString方法。 -
Class类中的getFields、getConstructors、getMethods方法将分别返回这个类支持的公共字段、构造器和方法的数组,其中包括超类的公共成员。Class类的getDeclareFields、getDeclareConstructors、getDeclareMethods方法将分别返回类中声明的全部字段、构造器和方法的数组,其中包括私有成员、包成员和受保护的成员,但不包括超类的成员。 -
使用反射在运行时分析对象
如果
f是一个Field类型的对象,obj是某个包含f字段的类的对象,f.get(obj)将返回一个对象,其值为obj的当前字段值。var harry = new Employee("Harry Hacker", 50000, 10, 1, 1989); Class cl = harry.getClass(); // the class object representing Employee Field f = cl.getDeclaredField("name"); // the name field of the Employee class Object v = f.get(harry); // the value of the name field of the harry object, i.e., the String object "Harry Hacker"调用
f.set(obj, value)将把对象obj的f表示的字段设置为新值。 -
只能对可以访问的字段使用
get和set方法。反射机制的默认行为受限于Java的访问控制。 -
可以调用
Field、Method或Constructor对象的setAccessible方法覆盖Java的访问控制。setAccessible方式是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。 -
使用反射编写泛型数组代码
java.lang.reflect包中Array类的静态方法newInstance,能够构造一个新数组,调用此方法时必须提供的参数,一是数组的元素类型,二是数组的长度。数组的长度可以通过
Array类的静态方法getLength方法获得。获得新数组元素类型的步骤:
- 首先获得
a数组的类对象 - 确认它确实是一个数组
- 使用
Class类的getComponentType方法(只为表示数组的类对象定义了这个方法)确定数组的正确类型
public static Object goodCopyOf(Object a, int newLength) { Class cl = a.getClass(); if (!cl.isArray()) return null; Class componentType = cl.getComponentType(); int length = Array.getLength(a); Object newArray = Array.newInstance(componentType, length); System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength)); }这个方法可以用来扩展任意类型的数组,而不仅是对象数组。
该方法的参数应该声明为
Object类型,而不要声明为对象型数组(Object[])。整型数组类型int[]可以转换成Object,但是不能转换成对象数组。 - 首先获得
-
调用任意方法和构造器
Method类有一个invoke方法,允许调用包装在当前Method对象中的方法。invoke方法的签名为:Object invoke(Object obj, Object... args)第一个参数是隐式参数,其余的对象提供了显式参数。对于静态方法,第一个参数可以忽略,即可以将它设置为
null。invoke方法的返回类型是Object对象,必须相应地完成强制类型转换再使用。如果返回类型是基本类型,invoke方法会返回其包装器类型。获得
Method对象的方法:- 调用
getDeclareMethods方法 - 调用
Class类的getMethod方法。由于有可能存在若干个同名的方法,调用该方法时必须提供想要的方法的参数类型。getMethod方法的签名为:
Method getMethod(String name, Class... parameterTypes)可以使用类似的方法调用任意的构造器。将构造器的参数类型提供给
Class.getConstructor方法,并把参数值提供给Constructor.newInstance方法。Class cl = Random.class; // or any other class with a constructor that // accepts a long parameter Constructor cons = cl.getConstructor(long.class); Object obj = cons.newInstance(42L); - 调用
5.8 继承的设计技巧
-
将公共操作和字段放在超类中。
-
不要使用受保护的字段。
- 子类集合是无限制的,任何人都能够由超类派生出子类,然后直接访问超类的
protected字段,从而破坏了封装性。 - 同一个包中的所有类都可以访问
protected字段,不管它们是否为这个类的子类。
- 子类集合是无限制的,任何人都能够由超类派生出子类,然后直接访问超类的
-
使用继承实现
"is-a"关系。 -
除非所有继承的方法都有意义,否则不要使用继承。
-
在覆盖方法时,不要改变预期的行为。
-
使用多态,而不要使用类型信息。
-
不要滥用反射。

浙公网安备 33010602011771号