第五章 继承
本章内容
- 类、超类和子类
- Object:所有类的超类
- 泛型数组列表
- 对象包装器与自动装箱
- 参数数量可变的方法
- 枚举类
- 反射
- 继承的设计技巧
继承已存在的类就是复用这些类的方法和域,还可以在原有的基础上增加一些新的方法和域,来满足新的需求。
反射是指在程序运行期间发现更多的类以及属性的能力。这个特性的功能很强大(暂时并不明白),但是入门只需要了解一下便可。
5.1 类、超类和子类
子类与父类的关系是“is a”关系。
5.1.1 定义子类
关键字extends
表示继承
public class Manager extends Employee {
// 添加方法和域
}
extends
表明正在构造的新类派生于一个已存在的类。已存在的类被称为超类(superclass)、基类(base class)或父类(parent class),新类被称为子类(subclass)。
在子类中定义的方法,超类并不能使用它,但是在子类中可以使用超类的方法,虽然并没有显式地定义。
同样,子类也继承了超类的域。但超类的私有域(private
作为修饰符)只能通过超类的访问器进行访问。
5.1.2 覆盖方法
有些超类的方法并不适用于子类,所以需要提供一个新的方法来覆盖(Override)超类中的这个方法。
假如你在覆盖了某一个超类方法后,发现有调用它的需求,可以使用特定的关键词super
来调用超类的同名方法。
super
和this
似乎很像,但本质上this
是一个对象的引用,而super
只是一个指示编译器调用超类方法的特殊关键字。
5.1.3 子类构造器
因为子类无法直接访问超类的私有域,所以在构造一个实例时,只能通过super(args1, arg2...)
调用超类的构造方法来初始化域。
如果子类的构造器没有显式地调用超类的构造器,编译器会自动调用超类默认的编译器super()
,如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式调用超类其他的构造器,编译器就会报错。
调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器
可以用超类的对象变量引用一个子类对象
Superclass super = new Subclass(args);
用超类指代子类非常有实用价值。例如所有数值类型的类都继承于Number
,那么我只需要用一个Number
类就能指代所有数值类型的对象。
5.1.4 继承层次
由一个公共超类派生出的所有类的集合被称为继承层次(inheritance hierarchy)。从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。
5.1.5 多态
一个用来判断是否应该设计为继承关系的简单规则。“is-a” 规则,它表明子类的每个对象也是超类的对象。另一种表述法是置换法则。表明了程序中出现超类对象的任何地方都可以用子类对象置换。
在Java中,对象变量是多态的,一个类变量既可以引用本类对象,也可以引用任何一个由它派生出的任何一个子类对象。
5.1.6 理解方法调用
假设要调用x.f(args)
,编译器会一一列举在本类中以及超类中访问属性为public
且名为f
的方法,接下来编译器会查看调用方法时提供的参数类型,如果找到一个与之参数类型完全匹配,便执行相应方法。如果编译器没有找到对应的方法,或者发现了多个方法与之匹配,就会报告一个错误。
如果是private
方法、static
方法、`final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这种调用方式称之为静态绑定 (static binding)。与之对应的是,调用的方法依赖于隐式参数的实际类型,并在运行时实现动态绑定。
当程序运行时,虚拟机会预先为每个类创建一个方法表(method table),列出所有方法的签名和实际调用的方法。虚拟机会搜索本类和超类的方法表进行匹配。
5.1.7 阻止继承:final类和方法
- final类不允许被扩展,不能被继承
- final方法不能被覆盖,且final类中的方法自动成为final方法
- final域在构造对象后,不允许改变它们的值
在设计类层次时,仔细考虑该将哪些类和方法声明为final
如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程被称为内联(inlining)
在进行类型转换之前,先查看一下是否能够成功地转换
if (staff[1] instanceof Manager) {
boss = (Manager) staff[1];
}
要点:
- 只能在继承层次内进行类型转换
- 再将超类转换成子类之前,应该使用
instanceof
进行检查
5.2 Object: 所有类的超类
5.2.1 equals方法
在Object类中,这个方法判断两个对象是否具有相同的引用
使用a.equals(b)
时需要小心a
为null
的情况,因此可以使用Objects.equals(a, b)
,当两个参数都为null
时,将会返回true
,其中一个参数为null
时会返回false
,如果两个参数都不为null
,则调用a.equals(b)
。
5.2.2 相等测试与继承
很多人喜欢用
if (!(instance instanceof class)) return false;
检测相等,但是随之而来的一个问题是,当隐式和显示的参数不属于一个类时,例如sub.equals(parent)
时上面的语句也许会返回true
,但反过来parent.eauqls(sub)
绝对会返回false
,这样就会违背对称性。
两种情况:
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用
getClass()
进行检测。 - 如果由超类决定相等的概念,那么就可以使用
instanceof
进行检测,这样可以在不同子类的对象之间进行相等的比较。
编写一个equals()
方法的建议:
- 显式参数命名为
otherObject
,稍后需要将它转换成另一个叫做other
的变量。 - 检测
this
和otherObject
是否引用同一个变量。 - 检测
otherObject
是否为null
,如果为null
,则返回false
,这项检测很有必要。 - 比较
this
和otherObject
是否属于同一个类,如果equals
的语义在每个子类中有所改变,就使用getClass()
检测;如果所有的子类都拥有统一的语义,就是用instanceof
检测。 - 将
otherObject
转换为相应的类类型变量。
6.现在开始对所有需要比较的域进行比较。用==
比较基本类型域,使用Objects.equals()
比较对象域。
在重写equals()
时,可能会出现错误:public boolean equals(Class other)
,因为声明的显式参数类型不是Object
,结果并没有覆盖超类的equals()
,为了避免发生类型错误,可以使用@Override
对覆盖超类的方法进行标记。
5.2.3 hashCode方法
散列码(hash code)是由对象导出的一个整型值。如果重写equals()
就必须重写hashCode()
。
null安全的方法:Objects.hashCode()
组合散列值: Objects.hash(Object obj1, Object obj2...)
基本类型: Integer/Long/Float/Double/.../Boolean.hashCode()
数组: Arrays.hashCode()
5.2.4 toString方法
数组需要调用Arrays.toString(arr)
进行打印,多维数组调用Arrays.deepToString(arr)
进行打印
toString()
是一种非常有用的调试工具,可以使用户获得一些关于对象状态的必要信息,建议为自定义的每一个类增加toString
方法,十分有用。
5.3 泛型数组列表
使用ArrayList
解决数组无法动态更改大小的问题
ArrayList
是一个使用类型参数(type parameter)的泛型类(generic class)。
例如:
ArrayList<String> strings = new ArrayList<String>();
//Java SE 7之后,可以省去右边的类型参数
ArrayList<String> strings = new ArrayList<>();
使用add
方法将元素添加到数组列表中。
如果能估计出数组可能存储的元素数量,可以调用ensureCapacity
方法,也可以把初始容量传递给构造器。
size
方法会返回数组列表中包含的实际元素数目。
当确定ArrayList的大小不会再发生变化时,可以使用trimToSize
方法将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器将回收多余的存储空间。
5.3.1 访问元素
通过get/set
方法实现访问或改变数组元素的操作:
strings.set(0, "first");
strings.get(0);
add
方法可以带索引,以便在ArrayList的中键插入元素。
通过remove
方法,将指定索引的元素删除。
通过toArray
方法将数组元素拷贝到一个数组中。
ArrayList
类实现了Iterable
接口,所以可以使用for-each
循环遍历所有元素:
for (String aStr : strings) {
System.out.print(aStr + " ");
}
5.3.2 类型化与原始数组列表的兼容性
一般来说,和遗留代码中的没有使用类型参数的ArrayList打交道,都会得到一些来自编译器的警告,只要确认这些警告并不会带来什么实质性的影响就行。如果能确保不会造成严重的后果,可以用@SuppressWarning("unchecked")
标注来标记这个变量能够接收类型转换。
public class BookDB{
public ArrayList find(String Word){ ... }
}
标记使用如下所示:
@SuppressWarning("unchecked")
ArrayList<String> result = (ArrayList<String>) bookDB.find("Java");
5.4 对象包装器与自动装箱
所有的基本类型都有一个与之对应的类
基本类型 | 包装器 |
---|---|
int | Integer |
char | Character |
double | Double |
boolean | Boolean |
… | … |
这些类被称为包装器(wrapper),对象包装器类时不可变的,而且是final类。
假设想定义一个int
型数组列表,但泛型的类型参数不能是基本类型,我们就可以先声明一个Integer
对象的数组列表:
ArrayList<Integer> list = new ArrayList<>();
现在就需要引入一个很有用的特性:
list.add(3);
//自动变换成
list.add(Integer.valueOf(3));
这种变换被称为自动装箱(autoboxing)。
相反,当将一个Integer
对象赋给一个int
值时,将会自动的拆箱:
int n = list.get(i);
//翻译成
int n = list.get(i).intValue();
==
运算符检测对象时会检测是否指向同一个存储区域,所以==
并不适用于对象包装器。在比较两个包装器对象时需要使用equals
方法。
装箱和拆箱是编译器认可的,而不是虚拟机。
5.5 参数数量可变的方法
可变参数的代码格式:Type...values
例如:
public static double max(double...values) {
double largest = Double.NEGATIVE_INFINITY;
for (double d : values) if (d > largest) largest = d;
return d;
}
允许将一个数组传递给可变参数方法的最后一个参数。可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。
可以试着把main
方法声明为:
public static void main(String...args){ ... }
5.6 枚举类
定义枚举类型:
public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};
实际上,这个声明定义的类型是一个类,它刚好有4个实例。
在比较两个枚举类型的值时,永远不需要调用equals
,直接使用==
即可。如果需要,可以在枚举类型中添加一些构造器、方法和域。
public enum Size{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation) {this.abbreviation = abbreviation; }
public String getAbbreviation() {return abbreviation; }
}
所有枚举类型都是Enum类的子类,都继承了Enum类中的许多方法。
toString
: 这个方法能够返回枚举常量名。valueOf
: 是一个静态方法,下面的语句将s
设置成Size.SMALL
。
Size s = Enum.valueOf(Size.class, "SMALL");
5.7 反射
浏览。。。
5.8 继承的设计技巧
- 将公共操作和域放在超类。
- 不要使用受保护的域。
- 第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问,从而破坏了封装性。
- 第二, 在同一个包中的所有类都能访问proteced域,而不管它是否为这个类的子类。
- 使用继承实现"is-a"关系。
不要滥用继承,只有当类与类之间是明确的"is-a"关系时,再使用继承来派生类,否则容易得不偿失。 - 除非所有继承的方法都有意义, 否则不要使用继承。
例如想编写一个Holiday
类,派生于GregorianCalendar
类,但是在GregorianCalendar
类中有一个add
方法可以将假日转换成非假日,会导致Holiday
类没有意义,因此这个例子不太适合继承。
如果拓展LocalDate
类就不会出现这个问题,因为这个类是不可变的。 - 在覆盖方法时,不要改变预期的行为。
置换原则不仅应用于语法,也可以应用于行为。在覆盖一个方法的时候,不应该毫无理由地改变行为的内涵。例如可以重写Holiday
类中add
方法修正原方法的问题,或什么都不做,或抛出异常,或继续到下一个假日。但是这些都违反了置换原则。关键在于,在重写子类中的方法时,不要偏离最初的设计想法。置换原则不仅应用于语法,也可以应用于行为。在覆盖一个方法的时候,不应该毫无理由地改变行为的内涵。例如可以重写Holiday
类中add
方法修正原方法的问题,或什么都不做,或抛出异常,或继续到下一个假日。但是这些都违反了置换原则。关键在于,在重写子类中的方法时,不要偏离最初的设计想法。 - 使用多态,而非类型信息。 使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
- 不要过多地使用反射。 反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性地程序,这种功能对于编写系统程序来说及其使用,但是通常不适于编写应用程序。