学习目标
- 类、超类和子类
- Object:所有类的超类
- 泛型数组列表
- 对象包装器与自动装箱
- 参数数量可变的方法
- 枚举类
- 反射
- 继承的设计技巧
一、类、超类和子类
这里以前文提过的Employee类为例,假设公司中有经理和员工,其待遇上存在一定的差异,但他们之间也存在许多相同的地方,例如,他们都领取薪水,不同的是,经理在完成预期任务后可以获得奖金。这种情况就要使用继承,因为需要为经理定义一个新类Manager,并增加一些新功能,但同时,每个经理也是员工,需要继承员工的方法。
(一)定义子类
可以如下继承Employee类来定义Manager类,这里使用关键字extends来继承:
public class Manager extends Employee
{
added methods and dields
}
在Java中,所有的继承都是公共继承。
关键字extends表明正在构造的新类派生于一个已存在的类。这个类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived classs)或孩子类(child class)。
通过扩展超类定义子类时,只需要指出子类与超类的不同之处。因此在设计类时,应将最一般的方法放在超类中。
(二)覆盖方法
超类中的某些方法对子类不一定适用,为此需要提供一个新的方法来覆盖(override)超类中这个方法。如Manager类中的getSalary方法应该返回薪水和奖金的总和:
public class Manager extends Employee
{
...
public double getSalary()
{
...
}
...
}
只有Employee方法能直接访问Employee类的私有字段,即Manager类的getSalary方法不能直接访问salary字段。因此要使用公共接口,调用超类中的getSalary方法,可以使用特殊的关键字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(name, salary, year, month, day) 即调用超类中带有n、s、year、month和day参数的构造器。
由于Manager类的构造器不能访问Employee类的私有字段,所以必须通过一个构造器初始化这些字段,可以利用super来构造。使用super调用构造器的语句必须是子类构造器的第一条语句。
Employee类完整代码如下:
import java.time.*;
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类的完整代码入下:
public class Manager extends Employee
{
private double bonus;
/**
* @param name the employee's name
* @param salary the salary
* @param year the hire year
* @param month the hire month
* @param day the hire day
*/
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;
}
}
可以通过以下程序展示二者在薪水计算上的区别:
/**
* This program demonstrates inheritance.
* @version 1.21 2004-02-21
* @author Cay Horstmann
*/
public class ManagerTest
{
public static void main(String[] args)
{
// construct a Manager object
var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
var staff = new Employee[3];
// fill the staff array with Manager and Employee objects
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
}
}
(四)继承层次
继承并不仅限一个层次。例如,可以由Manager类派生Executive类。由一个公共超类派生出来的所有类的集合称为继承层次(inheritance hierarchy),在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链(inheritance chain)。
(五)多态
一个对象变量可以指示多种实际类型的现象称为多态(polymorphism)。在运行时能自动地选择恰当的方法,称为动态绑定(dynamic binding)。
“is-a”规则的另一种表述是替换原则(substitution principal)。它指出程序中出现超类对象的任何地方都可以使用子类对象替换。
对象变量是多态的。一个Employee类型的变量既可以一用一个Employee类型的对象,也可以引用Employee类的任何一个子类的对象。例如,可以将子类对象赋给超类变量:
Employee e;
e = new Employee(...);
e = new Manager(...);
不过,不能将超类的引用赋给子类变量。
子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。如:
Manager[] managers = new Manager[10];
将它转换成Employee[]数组是完全合法的:
Employee[] staff = managers;
注:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。如超类方法为public,则子类方法必须为public。
(六)阻止继承:final类和方法
不允许扩展的类被称为final类。假设,希望阻止人们派生Executive类的子类,就可以在声明这个类时使用final修饰符:
public final class Executive extends Manager
{
...
}
类中某个特定方法也可以声明为final。子类就不能覆盖这个方法。
在早期Java中,有人为了避免动态绑定带来的系统开销而使用final关键字。如果一个方法没有被覆盖而且很短,编译器就能够对它进行优化处理,这个过程称为内联(inlining)。例如,内联调用e.getName() 将被替换为访问字段 e.name。
(七)强制类型转换
继承的强制类型转换遵循以下原则:
- 只能在继承层次内进行强制类型转换
- 在将超类强制转换成子类之前,应该使用instanceof进行检查
if (staff[1] instanceof Manager)
{
boss = (Manager) staff[1];
...
}
(八)抽象类
如果自下而上在类的继承层次结构中上移,位于上层的类更具有一般性,可能更加抽象。如:
public abstract String getDescription();
为提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。
public abstract class Person
{
...
public abstract String getDescription();
}
除了抽象方法外,抽象类还可以包含字段的具体方法。抽象方法充当着占位方法的角色,它们在子类中具体实现。扩展抽象类可以有两种选择。一种是在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样就必须将子类也记为抽象类;另一种做法是定义全部方法,这样一来,子类就不是抽象的了。
即使不含抽象方法,也可以将类声明为抽象类。
抽象类不能实例化。如果一个类声明为abstract,就不能创建这个类的对象。
可以定义一个抽象类的对象变量,但这个变量只能引用非抽象子类的对象。
(九)受保护访问
最好将类中的字段标记为private,而将方法标记为public。不过有时可能希望限制超类中的某个方法只允许子类访问,或者更少见的,可能希望允许子类方法访问超类的某个字段。为此,需要将这些类方法或字段声明为受保护(protected)。
在Java中,保护字段只能由同一个包中的类访问,从而避免滥用保护机制。
二、Object:所有类的超类
(一)Object类型的变量
可以使用Object类型的变量引用任何类型的对象:
Object obj = new Employee("Harry Hacker", 35000);
在Java中,只有基本类型(primitive type)不是对象。
(二)equals方法
Object类的equals对象用于检测一个对象是否等于另一个对象。
public class Employee
{
...
public boolean equals(Object otherObject){
// a quick test to see if the objects are identical
if (this == otherObject) return true;
// must return false if the explicit parameter is null
if (otherObject == null) return false;
// if the classes don't match, they can't be equal
if (getClass() != otherObject.getClass())
return false;
// now we know otherObject is a non-null Employee
Employee ohter = (Employee) otherObject;
// test whether the fields have identical values
return name.equals(ohter.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay)
}
}
在子类中定义equals方法时,首先调用超类的equals,如果检测失败,对象就不可能相等,然后再比较子类中的实例字段。
(三)相等测试与继承
Java语言规范要求equals方法具有以下特性:
- 自反性:对于任何非空引用x,x.equals(x) 应该返回 true
- 对称性:对于任何引用x和y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 返回 true
- 传递性
- 一致性
- 对于任何非空引用x,x.equals(null) 应该返回 false
有两种情形:
- 如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测
- 如果由超类决定相等性概念,则可以使用instanceof检测,这样可以在不同子类的对象之间进行相等性比较。
下面给出编写完美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;
- 现在根据相等性概念比较字段。使用 == 比较基本类型字段,使用 Objects.equals 比较对象字段:
return field1 == other.field1 && Objects.equals(field2, other.field2) && ...
对于数组字段,可以使用 Arrays.equals 方法检测是否相等。
(四)hashCode方法
散列码(hash code)是由对象导出的一个整数型。散列码是没有规律的。如果x和y是两个不同的对象,x.hashCode() 与 y.hashCode() 基本上不会相同。
每个对象都有一个默认的散列码,其值由对象的存储地址得出。
如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义 hashCode 方法,且equals 与 hashCode 的定义必须相容。
(五)toString方法
toString方法会返回表示对象的一个字符串。如 Point 类的 toString 方法将返回下面这样的字符串:
java.awt.Point[x=10,y=20]
下面是 Employee 类中 toString 方法的实现:
public String toString()
{
return "Employee[name=" + name
+ ", salary=" + salary
+ ", hireDay=" + hireDay
+ "]";
}
实际上还可以写得更好。最好调用 getClass().getName() 获得类名的字符串,而不要将类名硬编码写道 toString 方法中。
public String toString()
{
return getClass().getName() + "[name=" + name
+ ", salary=" + salary
+ ", hireDay=" + hireDay
+ "]";
}
这样的 toString 方法也可以由子类调用。
三、泛型数组列表
Java允许在运行时确定数组的大小:
int actualSize = ...;
var staff = new Employee[actualSize];
但这样做一旦确定了数组的长度就不能轻易改变了。可以使用ArrayList类。ArrayList是一个有类型参数(type parameter)的泛型类(generic class),生成数组如:ArrayList<Employee>
(一)声明数组列表
声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
在Java10中,最好使用var关键字以避免重复写类名:
var staff = new ArrayList<Employee>();
如果没有使用var关键字,可以省去右边的类型参数:
ArrayList<Employee> staff = new ArrayList<>();
这称为“菱形”语法。
使用add方法向数组添加元素:
staff.add(new Employee(...);
如果数组空间已满,数组列表会自动创建一个更大的数组,并将所有较小的数组拷贝到新数组中。
如果已知或可以估计出数组可能存储的元素数量,就可以在填充数组之前调用 ensureCapacity 方法:
staff.ensureCapacity(100);
另外,还可以把初始容量传递给ArrayList构造器:
ArrayList<Employee> staff = new ArrayList<>(100);
size 方法将返回数组列表中包含的实际元素个数:staff.size()
一旦确认数组列表大小,可以调用 trimToSize 方法保存当前元素所需要的存储空间,回收多余空间。
(二)访问数组列表元素
只能使用get和set方法访问数组列表。例如,要设置第i个元素:
staff.set(i, harry);
set方法只能用于替换数组中已经加入的元素。
要获得数组的一个元素:
Employee e = staff.get(i);
四、对象包装器与自动装箱
有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapper)。包装器类是不可变的,是final,不能派生它们的子类。
如定义整型数组列表就可以:
var list = new ArrayList<Integer>();
幸运的是,有一个很有用的特性,从而可以很容易地向ArrayList添加int类型的元素:
list.add(3);
将自动变换为:
list.add(Integer.valueOf(3));
这种变换称为自动装箱(autoboxing).
相反的,将一个Integer对象赋给一个int值时,会自动拆箱。
自动地装箱和拆箱也适用算术表达式。
由于包装器类引用可以为null,所以自动装箱有可能会抛出 NullPointerException 异常;另外,如果在一个条件表达式中混合使用 Integer 和 Double 类型,Integer 值就会自动拆箱,升级为 double,再装箱为Double。
装箱和拆箱都是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码时会插入必要的方法调用。
五、参数数量可变的方法
可以提供参数数量可变的方法,如 printf:
public class PrintStream
{
public PrintStream printf(String fmt, Object...args)
{
return format(fmt, args);
}
}
这里的 … 是Java代码的一部分,表示方法可以接收任意数量的对象。
六、枚举类
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }
实际上,这个声明定义的类型是一个类,它刚好有四个实例,不可能构造新的对象。因此,在比较两个枚举类型的值时,要用 ==
如果需要的话可以为枚举类型增加构造器、方法和字段。构造器只在构造枚举常量时调用。
public enum Size
{
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL")
private String abbreviation;
private Size(String abbreviation) { this.abbreviation = abbreviation;}
private String getAbbreviation() { return abbreviation;}
}
枚举器的构造器总是私有的。所有枚举类型都是Enum类的子类。
toString方法可以返回枚举常量名,如Size.SMALL.toString()
将返回字符 “SMALL”
toString的逆方法是静态方法valueOf。如
Size s = Enum.valueOf(Size.class, "SMALL");
将 s 设置为 Size.SMALL。
每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举类型的数组。
Size[] values = Size.values();
ordinal方法返回enum声明中枚举常量的位置,从0开始计数。
七、反射
反射库(reflection library)提供了一个丰富且精巧的工具集,可以用来编写能够动态操纵Java代码的程序。Java可以支持用户界面生成器、对象关系映射器等。
能够分析类能力的程序称为反射(reflection)。反射机制可以用来:
- 在运行时分析类的能力
- 在运行时检查对象。如,编写一个适用于所有类的toString对象
- 实现泛型数组操作代码
- 利用Method对象,类似C++的指针
(一)Class类
在程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要正确执行的方法。
可以用一个特殊的Class类访问这些信息。Object类中的 getClass() 方法将会返回一个Class类型的实例。
Employee e;
...
Class cl = e.getClass();
(二)异常
程序运行错误时,会抛出一个异常,可以通过处理器(handler)捕获异常并进行处理。
异常有两种类型:非检查型(unchecked)异常和检查型(checked)异常。
可以使用throws语句抛出异常,在方法名前加上一个throws子句即可:
public static void doSomethingWithClass(String name)
{
throws ReflectiveOperationException
{
Class cl = Class.forName(name); // might with throw exception
do something with cl
}
}
有关异常会在后续文章进行介绍。
##(三)资源
类通常会有一些关联的数据文件,如图像、声音、文本文件等,这些文件被称为资源(resource)。
有关反射不作详细解释,有兴趣的读者可以自行搜索资料。
八、继承的设计技巧
这里给出一些继承的设计技巧:
- 将公共操作和字段放在超类中
- 不要使用受保护的字段
- 使用继承实现“is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 再覆盖方法时,不要改变预期的行为
- 使用多态,而不要使用类型信息
- 不要滥用反射
参考资料:
狂神说Java
Java核心技术 卷I(第11版)