《JAVA核心技术卷》第五章 继承
- 5.1 类、超类和子类
- 5.1.1 定义子类
- 5.1.2 覆盖方法
- 5.1.3 子类构造器
- 多态
- 5.1.4 继承层次
- 5.1.5 多态
- 5.1.6 理解方法调用
- 5.1.7 阻止继承:final类和方法
- 5.1.8 强制类型转换
- 5.1.9 抽象类
- 5.1.10 受保护访问
- 5.2 所有类的超类
- 5.2.1 equals方法
- 5.2.2 相等测试与继承
- 5.2.3 hashCode方法
- 5.2.4 toString方法
- 5.3 泛型数组列表
- 5.3.1 访问数组列表元素
- 5.4 对象包装器与自动装箱
- 5.7 反射
- 5.7.1 Class类
- 5.7.2 捕获异常
- 5.7.3 利用反射分析类的能力
- 5.7.4 在运行时使用反射分析对象
- 5.7.5 使用反射编写泛型数组代码
- 5.7.6 调用任意方法
- 5.8 继承的设计技巧
5.1 类、超类和子类
5.1.1 定义子类
关键字extends表示继承,表明正在构造的新类派生于一个已存在的类。已经存在的类称为超类、基类或父类;新类称为子类、派生类或孩子类。子类比超类拥有的功能更加丰富。
public class Manager extends Employee{
添加方法和域
}
在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。
5.1.2 覆盖方法
Java中super与this的异同
在子类中可以增加域、增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。
5.1.3 子类构造器
使用super调用构造器的语句必须是子类构造器的第一条语句。如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其它构造器,则Java编译器将报告错误。
多态
一个对象变量可以指示多种实际类型的现象被称为多态。在运行时能够自动地选择调用哪个方法的现象称为动态绑定。
若自定义一个Employee类:
public class Employee{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day){
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public LocalDate getHireDay(){
return hireDay;
}
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
}
定义一个Manager类,继承自Employee类:
public class Manager extends Employee{
private double bonus;
public Manager(String name, double salary, int year, int month, int day){
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary(){
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b){
bonus = b;
}
}
多态见下面代码中的变量e,尽管e声明为Employee类型,但实际上e既可以引用Employee类型的对象,也可以引用Manager类型的对象。当e引用Employee对象时,e.getSalary( )调用的是Employee类中的getSalary方法;当e引用Manager对象时,e.getSalary( )调用的是Manager类中的getSalary方法。虚拟机知道e实际引用的对象类型,因此能够正确的调用相应的方法。
//程序清单5-1
public class ManagerTest{
public static void main(String[] args){
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
//此处的e是多态
for(Employee e : staff){
System.out.println("name=" + e.getName() + ", salary=" + e.getSalary());
}
}
}
5.1.4 继承层次
Java不支持多继承
5.1.5 多态
置换法则:程序中出现超类对象的任何地方都可以用子类对象置换。然而,不能将一个超类的引用赋给子类变量。
从程序清单5-1中可以看出,变量staff[0]与boss引用同一个对象。编译器将staff[0]看成Employee对象,这意味着可以这样调用:
boss.setBonus(5000); //OK
但不能这样调用:
staff[0].setBonus(5000); //Error
是因为staff[0]中声明的类型是Employee,而setBonus不是Employee类的方法。
5.1.6 理解方法调用
- 编译器查看对象的声明类型和方法名,获得所有可能被调用的候选方法。
- 编译器将查看调用方法时提供的参数类型,获得需要调用的方法名字和参数类型。
- 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public。
5.1.7 阻止继承:final类和方法
在定义类时使用final修饰符就表明这个类是final类,不允许被扩展。
在定义方法时使用final修饰符就表明子类不能覆盖这个方法。(final类中的所有方法自动地成为final方法,但是域并未自动成为final域)。
此处书上提及了内联,懒狗决定暂时不去管它ᕙ(`▿´)ᕗ 我只是一个面向面试学习的懒狗罢了
5.1.8 强制类型转换
- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前,应该使用instanceof进行检查。
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
5.1.9 抽象类
包含一个或多个抽象方法的类本身必须被声明为抽象的。除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
扩展抽象类可以有两种选择,一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
类即使不含抽象方法,也可以将类声明为抽象类。
!!!!抽象类不能被实例化。但是可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。
问:是否可以省去超类中的抽象方法,而仅在子类中定义实现对应的方法呢?
答:不能。因为在Java中,编译器只允许调用在类中声明的方法。如果省去超类中的抽象方法,就不能通过抽象类的对象变量来调用该省去的方法了。
5.1.10 受保护访问
若希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,则需要将这些方法或域声明为protected。
下面归纳一下Java用于控制可见性的4个访问修饰符:
- 仅本类可见——private
- 对所有类可见——public
- 对本包和所有子类可见——protected
- 对本包可见——默认,不需要修饰符
5.2 所有类的超类
5.2.1 equals方法
在Object类中,这个方法将判断两个对象是否具有相同的引用。(对多数类来说,这种判断并没有什么意义。)然而,经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。
在子类中定义equals方法时,首先调用超类的equals。
5.2.2 相等测试与继承
相等性测试的特点:
- 自反性:对于任何非空引用x,x.equals(x)应该返回true。
- 对称性:对于任何引用x和y,x.equals(y)和y.equals(x)都应该返回true。
- 传递性:对于任何引用x y z,如果x.equals(y)返回true,y.equals(x)返回true,则x.equals(z)也应该返回true。
- 一致性:如果x、y没有发生变化,反复调用x.equals(y)应返回同样结果。
- 对于任意非空引用x,x.equals(null)应返回false。
- 如果x.equals(y)返回true,则必须有x.hashCode() == y.hashCode()。
就对称性来说,当参数不属于同一个类的时候需要十分注意。比如:e.equals(m)
这里的e是一个Employee对象,m是一个Manager对象,并且两个对象具有相同的姓名、薪水和雇佣日期。
如果Employee类中的equals方法用instanceof来实现,则e.equals(m)
将返回true。由于相等性测试需要满足对称性,这就意味着反过来m.equals(e)
也需要返回true——这限制了Manager类中equals方法的实现:因为Employee类中不包含经理特有的那一部分信息(比如bonus),所以若要使m.equals(e)
返回true,Manager类中的equals方法就不能考虑经理特有的那部分信息。
再来考虑若二者的equals方法都用getClass来实现:由于e和m都属于不同的类,所以e.equals(m)
和m.equals(e)
都返回false,对称性得到了满足。
所以,在继承关系中的相等性测试,应该根据语义分两种情况考虑:
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
- 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。
对于 1 的情况:比如两个对象所对应的名字,薪水和雇佣日期相等而奖金不相等,则认为它们是不同的。(equals方法中在进行getClass比较之后再比较对应的域)
对于 2 的情况,假设使用name作为相等的检测标准,并且这个相等的概念适用于所有子类,就可以使用instanceof进行检测,并且应该将超类的equals方法声明为final。
下面给出编写一个完美的equals方法的建议:
- 显示参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
- 检测this与otherObject是否引用同一个对象:
if(this == otherObject) return true;
- 检测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;
- 现在开始对所有需要比较的域进行比较。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。
return field1 == other.field1
&& Objects.equals(field2, other.field2)
&& . . .;
如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。
tips:
对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。
关于P170页上方的警告的理解
//代码1
public class Foo(){
public boolean equals(Foo foo){
return true;
}
}
Object中的源码如下:(我竟然也有去看源码的一天)
//代码2
public boolean equals(Object obj) {
return (this == obj);
}
我们的意图是覆盖Object中的equals方法,但因为我们指定了一个类型为Foo而不是Object类型的参数,所以我们实际上提供了重载的Object的equals方法,而不是覆盖。所以当需要调用Foo中equals方法的时候若传入参数是Object类对象则仍然是调用object中的方法,即比较二者是否为同一引用,并没有达到我们想象中覆盖的目的。
5.2.3 hashCode方法
- 每个对象都有一个默认的散列码,其值为对象的存储地址。
- 如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。
- Equals必须与hashCode的定义一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。
(关于散列表的内容见第9章)
5.2.4 toString方法
getClass().getName()
:获得类名的字符串
Object类的toString方法:(用来打印输出对象所属的类名和散列码)
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
若想要打印数组,不能用数组的toString方法,因为数组的toString方法继承了object类的toString方法。修正的方式是调用静态方法Arrays.toString。若要想打印多维数组,则需要调用Arrays.deepToString方法。
5.3 泛型数组列表
ArrayList是一个采用类型参数的泛型类。
//ArrayList中几个常用的方法
//在数组列表的尾部添加一个元素,永远返回true。
boolean add(E obj)
//返回存储在数组列表中的当前元素数量(这个值小于或等于数组列表的容量)
int size()
//确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素
void ensureCapacity(int capacity)
//将数组列表的存储容量削减到当前尺寸,垃圾回收器将回收多余的存储空间
void trimToSize()
5.3.1 访问数组列表元素
//设置数组列表指定位置的元素值,这个操作将覆盖这个位置的原有内容
void set(int index, E obj)
//获得指定位置的元素值
E get(int index)
//向后移动元素,以便插入元素
void add(int index, E obj)
//删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回
E remove(int index)
(可能看源码看得有点上瘾,由于比较之意第三个方法add,不太相信难道真的还会将后面的元素向后挪嘛,于是去看了源码,是我孤陋寡闻了)
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
// 将元素element添加到顺序表index处
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
// 如果顺序表已满,则需要扩容
if((s = size) == (elementData = this.elementData).length) {
// 对当前顺序表扩容
elementData = grow();
}
// 移动元素
System.arraycopy(elementData, index, elementData, index + 1, s - index);
// 插入元素
elementData[index] = element;
size = s + 1;
}
注意set方法和add方法的区别!!!使用add方法为数组添加新元素,而不要使用set方法,它只能替换数组中已经存在的元素内容。
将ArrayList中的元素拷贝到一个数组中:
ArrayList<X> list = new ArrayList<>();
. . . //向list中插入元素
X[] a = new X[list.size()];
list.toArray(a);
5.4 对象包装器与自动装箱
所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。
自动装箱:list.add(3)
自动地变换成list.add(Integer.valueOf(3))
自动拆箱:int n = list.get(i)
翻译成int n = list.get(i).intValue( )
装箱和拆箱是编译器认可的,而不是虚拟机。 编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
Integer类中包含的一些重要方法:
//以int形式返回Integer对象的值
int intValue()
//以一个新String对象的形式返回给定数值i的十进制表示
static String toString(int i)
//返回数值i的基于给定radix参数进制的表示
static String toString(int i, int radix)
//返回字符串表示的整型数值,给定字符串表示的是十进制的整数(第一种方法)
//或者是radix参数进制的整数(第二种方法)
static int parseInt(String s)
static int parseInt(String s, int radix)
//返回用s表示的整型数值进行初始化后的一个新Integer对象,给定字符串表示的是十进制(第一种方法)
//或者是radix参数进制的整数(第二种方法)
static Integer valueOf(String s)
static Integer valueOf(String s, int radix)
注意:parseInt返回的是int值,而valueOf返回的是Integer对象。
5.7 反射
能够分析类能力的程序称为反射。
5.7.1 Class类
一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但int.class是一个Class类型的对象。
虚拟机为每个类型管理一个Class对象。因此可以利用==运算符实现两个类对象比较的操作。例如,if(e.getClass == Employee.class) . . .
newInstance()
方法:
newInstance方法用来动态地创建一个类的实例。它调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。
例如:e.getClass().newInstance()
forName()
方法:获得类名对应的Class对象
将以上二者配合起来使用,可以根据存储在字符串中的类名创建一个对象:
String s = "java.util.Random";
Object m = Class.forName(s).newInstance();
5.7.2 捕获异常
具体异常处理机制见第七章。
抛出异常比终止程序要灵活得多,这是因为可以提供一个“捕获”异常的处理器对异常情况进行处理。
实现最简单的处理器:将可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后在catch子句中提供处理器代码。
try{
statements that might throw exceptions
}
catch(Exception e){
handler action
}
5.7.3 利用反射分析类的能力
在java.lang.reflect包中有三个类:Field、Method和Constructor分别用于描述类的域、方法和构造器。
//java.lang.Class 1.0
//返回一个包含Field对象的数组,这些对象记录了这个类或其超类的公有域。
Field[] getField()
//返回包含Field对象的数组,这些对下个记录了这个类的全部域。
Field[] getDeclaredFields()
//返回包含Method对象的数组:
//getMethods将返回所有的公有方法,包括从超类继承来的公有方法;
//getDeclaredMethods返回这个类或接口的全部方法,但不包括由超类继承的方法。
Method[] getMethods()
Method[] getDeclaredMethods()
//返回包含Constructor对象的数组,其中包含了Class对象所描述的类的所有公有构造器(getConstructors)
//或所有构造器(getDeclaredConstructors)
Constructor[] getConstructors()
Constructor[] getDeclaredConstructors()
//java.lang.reflect.Constructor
//返回一个用于描述类中定义的构造器、方法或域的Class对象
Class getDeclaringClass()
//返回一个用于描述方法抛出的异常类型的Class对象数组
Class[] getExceptionTypes()
//返回一个用于描述构造器、方法、域的修饰符的整型数值。
int getModifiers()
//返回一个用于描述构造器、方法或域名的字符串
String getName()
//返回一个用于描述参数类型的Class对象数组
Class[] getParameterTypes()
//返回一个用于描述返回类型的Class对象
Class getReturnType()
5.7.4 在运行时使用反射分析对象
5.7.5 使用反射编写泛型数组代码
5.7.6 调用任意方法
以上三节我是实在看不下去了。。。
5.8 继承的设计技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现“ is-a ”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态,而非类型信息
- 不要过多地使用反射
反射机制对于编写系统程序来说极其实用,但是通常不适用于编写应用程序。