1.类、超类和子类
定义子类
关键字extends表示继承。
public class Manager extends Employee {
添加方法和域
}
关键字extends表明正在构建的新类派生于一个已存在的类。已存在的类被称为超类、基类或父类。新类称为子类、派生类或孩子类。Java程序员更喜欢用超类和子类。
子类比超类拥有的功能更加丰富,Manager比员工多一份奖金。
在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。
覆盖方法
超类中的方法对于子类并不一定适用(Manager的getSalary方法应该返回薪水和奖金的总和)。需要提供新的方法来覆盖(override)超类中的这个方法:
public class Manager extends Employee {
private double bonus;
@Override
public double getSalary() {
return salary+ bonus;
}
}
然而这个方法并不能运行。子类不能直接访问超类的私有域。
public double getSalary() {
double baseSalary = getSalary();
return baseSalary + bonus;
}
仍不能运行,因为子类也有一个getSalary方法(就是正在实现的这个方法),所以这条语句会导致无限次地调用自己,直到整个程序崩溃。
为此,需要使用特定的super解决这个问题:
@Override
public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
super并不像this一样,super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
在子类中可以增加域、增加方法或覆盖超类的方法,然而不能删除继承的任何域和方法。
子类构造器
public Manager(String name, double salary, int year, int month, int day) {
super(name, salary, year, month, day);
bonus = 0;
}
这里的super具有不同的含义,语句super(name, salary, year, month, day)是调用超类中含有name,salary,year,day参数的构造器的简写形式。由于子类不能访问超类的私有域,所以必须利用超类构造器对这部分私有域进行初始化。super调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式地调用超类构造器,则将自动调用超类默认构造器。如果超类没有不带参数的构造器,并且在子类构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。
this的两个用途:
1.引用隐式参数
2.调用该类其他的构造器
super的两个用途:
1.调用超类方法
2.调用超类构造器
两个关键字在调用构造器时候,有一个共同点,调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。
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);
for (Employee e : staff) {
System.out.println(e.getName() + " " + e.getSalary());
}
//Carl Cracker 85000.0
//Harry Hacker 50000.0
//Tommy Tester 40000.0
}
只有staff[0]计算了奖金加薪水,e.getSalary()调用能够确定应该执行哪个getSalary方法。尽管e声明为Employee类型,但实际上e既可以引用到Employee类型的对象,也可以引用Manager类型的对象。虚拟机知道e实际引用的对象类型,因此能够正确地调用响应的方法。
一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。
在运行时能够自动地选择调用哪个方法的现象被称为动态绑定(dynamic binding)。
继承层次
继承不仅限于一个层次。Java不支持多继承,有关多多继承的功能Java通过接口实现。
由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。
多态
继承关系就是is-a规则,is-a规则的另一种表述法是置换法则。
在Java中,对象变量是多态的。一个Employee对象既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类对象。
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
这个例子中,变量staff[0]与boss引用同一个对象。但编译器将staff[0]看成Employee对象。这意味着可以调用boss.setBonus(5000),但不能调用staff[0].setBonus(5000)。
然而,不能将一个超类的引用赋给子类变量。
Manager m = staff[i]; // error
不是所有的雇员都是经理。
多态的条件:
1.要有继承
2.要有重写
3.父类引用指向子类对象
在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。
Manager[] managers = new Manager[10];
Employee[] staff = managers;
staff[0] = new Employee(...);
编译器接纳这个赋值操作,但staff[0]与manager[0]引用的是同一个对象,似乎我们把一个普通雇员擅自归入经理行列中了。当调用manager[0].setBonus(1000)的时候,将会导致调用一个不存在的域,进而搅乱相邻存储空间的内容。
为了确保不发生这类错误,所有数组都要牢记他们的类型,并负责监督仅将类型兼容的引用存储到数组中。例如new manager[10]创建了一个经理数组,如果试图存储一个Employee类型的引用就会发生ArrayStoreException异常。
理解方法调用
x.f(args),隐式参数x声明为C的一个对象。
1.编译器查看对象的声明类型和方法名,编译器会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。
2.编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法,这个过程被称为重载解析(overloading resolution)。由于允许参数类型转换(int转为double),如果编译器没有找到与参数类型匹配的方法,或者经过转换后有多个方法与之匹配,就会报告错误。
3.如果是private、static、final方法或者构造器,那个编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定(static binding)。与之相对应的是,调用的方法依赖于隐式参数的实际类型,并且运行时实现动态绑定。
4.当采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
假设x的实际类型是D,它是C的子类。如果D类定义了方法f,就直接调用它,否则在D的超类在寻找f,依次类推。
虚拟机预先为每个类创建一个方法表(method table),列出所有的签名和实际调用的方法。如果调用super.f(param),编译器将对隐式参数超类的方法表进行搜索。
动态绑定有一个重要特性:无需对现存的代码进行修改,就可以对程序进行扩展。
阻止继承:final类和方法
不允许扩展的类被称为final类。
public final class Executive extends Manager {
...
}
类中特定方法也可以被声明为final。子类就不能覆盖这个方法(final类中的所有方法自动的成为final方法,但不包括域)。
将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。
强制类型转换
进行类型转换的唯一原因是:在暂时忽略对象的实际类型之后,使用对象的全部功能。
Manager boss1 = (Manager) staff[0];
Manager boss2 = (Manager) staff[1]; // error
第二句转换会产生一个ClassCastException异常。
应该养成一个良好的程序设计习惯,在转换之前先查看是否能够成功地转换:
if (staff[1] instanceof Manager) {
Manager boss2 = (Manager) staff[1];
}
x如果为null,x instanceof C不会产生异常,只是会返回false。因为null没有引用任何对象,当然也不会引用C类型的对象。
1.只能在继承层内进行类型转换。
2.在将超类转换成子类之前,应该使用instanceof进行检查。
抽象类
如果自上而下在类的继承层次结果中上移,位于上层的类更具有通用性,甚至可能更加抽象。我们只将它作为派生其他类的基类,而不作为想使用的特定实例类。可以使用abstract关键字,这样就不需要实现基类的方法了。
包含一个或多个抽象方法的类本身必须被声明为抽象的。
public abstract class Person {
public abstract String getDescription();
}
除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
public abstract class Person {
private String name;
public Person(String name) {
this.name = name;
}
public abstract String getDescription();
public String getName() {
return name;
}
}
类即使不含抽象方法,也可以声明为抽象类。
抽象类不能实例化。
new Person("Wu");
是错误的,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象:
Person p = new Student("Wu", "Economics");
受保护访问
可以将Employee中的hireDay声明为proteced,而不是私有域,Manager中的方法就可以直接访问它,但Manager类中的方法只能够访问Manager对象中的hireDay域,而不能访问其他Employee对象中的这个域。这种限制有助于避免滥用受保护机制,使得子类只能获得访问受保护域的权利。在实际应用中,要谨慎使用protected属性。
受保护方法更具有实际意义。如果需要限制某个方法的使用,就可以将它声明为protected。这表明子类得到信任,可以正确地使用这个方法,而其他类则不行。这种方法一个最好的示例就是Object中的clone方法。
2.Object:所有类的超类
Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来的。
在Java中,只有基本类型(primitive types)不是对象。
所有数组类型,不过是对象数组还是基本类型的数组都扩展了Object类。
equals类
在Object类中,这个方法将判断两个对象是否具有相同的引用。
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) {
return true;
}
if (otherObject == null) {
return false;
}
if (getClass() != otherObject.getClass()) {
return false;
}
Employee other = (Employee) otherObject;
return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
}
getClass方法将返回一个对象所属的类。
由于null值调用equals方法会出现NullPointerException异常,所以这里使用Objects.equals方法,如果两个参数都为null,返回true;如果其中一个参数为null,返回false;均不为null,调用a.equals(b)。
在子类定义equals方法时,首先调用超类的equals,如果检查失败,对象就不可能相等。
@Override
public boolean equals(Object otherObject) {
if (!super.equals(otherObject)) {
return false;
}
Manager other = (Manager) otherObject;
return bonus == other.bonus;
}
相等测试与继承
很多人喜欢使用instanceof进行检查类是否匹配。这样不但没有解决otherObject是子类的情况,并还还有可能招致一些麻烦。
java中,instanceof运算符的前一个操作符是一个引用变量,后一个操作数通常是一个类(可以是接口),用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是返回true,否则返回false。
Person p = new Student("Wu", "Economics");
System.out.println(p instanceof Object);
System.out.println(p instanceof Student);
// true
// true
Java语言规范要求equals方法具有下面的特性:
1.自反性 x.equals(x) = true
2.对称性
3.传递性
4.一致性,如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
5.对于任意非空引用x.equals(null)应该返回false。
一个完美编写equals方法的建议:
1.显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量
2.检测otherObject与this是否引用同一个对象
if (this == otherObject) {
return true;
}
3.检测otherObject是否为null
if (otherObject == null) {
return false;
}
4.比较this与otherObject是否属于同一个类,使用getClass或instanceof检测
●如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检查
●如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同的子类的对象之间进行相等比较。
5.将otherObject转换为相应的类类型变量
ClassName other = (ClassName)otherObject;
6.对所有需要比较的域进行比较
对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。
hashCode方法
散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的,如果x和y是两个不同的对象,x.hashCode()与y.hashCode()基本上不会相同。
String类源码的hashCode方法:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
String s = "OK";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("OK");
StringBuilder st = new StringBuilder(t);
System.out.println(t.hashCode() + " " + st.hashCode());
// 2524 356573597
// 2524 1735600054
sb和st有着不同的散列码,因为在StringBuilder类中没有定义hashCode方法,它的散列码是由Object类默认hashCode方法导出的对象存储地址。
hashCode方法应该返回一个整数数值(负数也可),并合理地自核实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。
@Override
public int hashCode() {
return 7 * Objects.hashCode(name)
+ 11 * Double.hashCode(salary)
+ 13 * Objects.hashCode(hireDay);
}
最好使用null安全方法Objects.hashCode,如果参数为null,这个方法返回0。
还有更好的做法,组合多个散列值时,可使用Objects.hash:
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。
如果存在数组类型的域,可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码有数组元素的散列码组成。
toString方法
toString方法,返回表示对象值的字符串。
绝大多数toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。
@Override
public String toString() {
return getClass().getName() + "[name=" + name +
"salary: " + salary
+ ",hireDay=" + hireDay
+ "]";
}
最好通过getClass().getName()获得类名的字符串,而不要讲类名硬加在toString方法中。
这样的好处是,子类只要调用super.toString方法就可以了
@Override
public String toString () {
return super.toString()
+ "[bonus=" + bonus +
"]";
}
随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符 “+”连接起来,Java编译就会自动调用toString方法。
在调用x.toString()的地方可以用 "" + x代替,与toString不同的是,如果x是基础类型,这句话也可以执行。
如果x是任意对象,调用System.out.println(x),println方法就会直接地调用x.toString方法,并打印出得到的字符串。
Object类定义了toString方法,用来打印输出对象所属的类名和散列码。
System.out.println(System.out);
// java.io.PrintStream@14ae5a5
PrintStream类的设计者没有覆盖toString方法。
但是素组继承了Object类的toString方法,数组类型将按照旧的格式打印:
int[] numbers = { 2, 3, 5, 7 };
String ss = "" + numbers;
System.out.println(ss);
System.out.println(Arrays.toString(numbers));
// [I@7f31245a
// [2, 3, 5, 7]
[I表明是一个整型数组,修正的方式是调用静态方法Arrays.toString方法,多维数组则需要调用Arrays.deepToString方法。