第5章 继承
-
利用继承可以已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的全部方法和全部域。在此基础上可以添加一些新的方法和域,以满足新的需求。这是Java程序设计中的一项核心技术。
-
反射是指在程序运行期间发现更多的类及其属性的能力。
- 程序清单5-1 inheritance/Empolyee.java
package inheritance;
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;
}
}
5.1 类、超类和子类
5.1.1 定义子类
- 关键字extends表明正在构造的新类派生与一个已存在的类。已存在的类成为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或子类(child class)。子类比超类拥有的功能更加丰富。
- 在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。这种做法在OOP中十分普遍。
5.1.2 覆盖方法
-
超类中的有些方法对子类并不一定适用,需要提供一个新的方法来覆盖(override)超类的方法。绝对不能删除继承的任何域和方法。
/** //使用员工和经理的传统示例 //具体来说,Employee只需要返回薪水,而Manager应该返回薪水和奖金的总和 */- 程序清单5-2 inheritance/Manager.java
package inheritance;
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;
}
}
-
由于子类的方法不能直接访问超类的私有域,只能使用关键字super来调用超类的公有方法,进而访问超类的私有域。
/** //程序清单5-2引用 public class Manager extends Employee{ private double bonus; ... public void setBonus(double bonus){ this.bonus = bonus; } public double getSalary(){ double baseSalary = super.getSalary(); return baseSalary + bonus; } ... } */- super与this引用的概念并不类似。因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示javac调用超类方法的特殊关键字。
5.1.3 子类构造器
-
由于子类的构造器不能访问超类的私有域,所以必须利用super调用超类的构造器,进而对私有域进行初始化。使用super调用构造器的语句必须是子类构造器的第一条语句。
-
如果子类的构造器没有显式调用超类的构造器,则自动调用超类默认无参构造器。在此基础上,如果超类默认的构造器不是无参构造器,则javac会报错。
- this有两个用途:一是引用隐式参数,二是调用该类其他构造器。super也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。
-
一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动的选择调用哪个方法的现象称为动态绑定(dynamic binding)。
/** //程序清单5-3引用 for (Employee e : staff) { System.out.println("name=" + e.getName() + ",salary=" + e.getSalary()); //尽管将e声明为Employee类型,但实际上e既可以引用Employee的实例,也可以引用Manager的实例。 } */- 程序清单5-3 inheritance/ManagerTest.java
package inheritance;
/**
* 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
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
Employee[] 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());
}
}
}
5.1.4 继承层次
继承并不仅限于一个层次。由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。
/**
//例如,可以由Manager派生Executive。
*/
5.1.5 多态
在Java中,对象变量是多态的。一个超类变量既可以引用一个超类实例,也可以引用任何一个子类的实例。不能将一个超类的引用赋给子类变量。
警告 在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。此时子类数组和超类数组引用的是同一个数组。 当子类数组和超类数组引用的是同一个数组时,不能对此数组赋值超类实例的引用,因为赋值后当调用子类独有的方法时,可能会导致调用不存在的实例域,进而搅乱相邻存储空间的内容。如果试图存储一个超类实例的引用就会引发ArrayStoreException。 为了确保不发生这类错误,所有数组都要牢记创建他们的元素类型,并负责监督仅将类型兼容的实例的引用存储到数组中。
5.1.6 理解方法调用
-
假设调用隐式参数x声明为类C实例的x.f(param)。方法调用的详细过程为:
- javac获得所有可能被调用的候选方法。javac查看对象的声明类型和方法名,并一一列举C中的f()和其超类中public的f()。
- 接下来javac获得需要调用的方法名和参数类型。查看调用方法时提供的参数类型。如果在步骤1候选的方法中存在一个与提供参数类型匹配的方法则选择调用,这个过程被称为重载解析(overloading)。如果javac没有找到匹配方法,或者发现经过类型转换后有多个方法匹配就会报错。
-
方法名字和参数列表称为方法的签名。如果在子类中定义了一个与超类签名相同的方法,那么子类的方法会覆盖超类的方法。
覆盖方法时一定要保证返回类型的兼容性,将子类覆盖方法的返回类型定义为原返回类型的子类型。这时原方法和覆盖方法具有可协变的返回类型。
-
如果是private方法、static方法或者构造器,javac可以准确的知道调用哪个方法,这种调用方式称为静态绑定(static binding)。其他方法的情况下,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
/** //对于调用x.f("Hello")来说,javac采用动态绑定的方式生成一条调用f(String)的指令。 */ -
最后是虚拟机调用方法。当采用动态绑定调用方法运行时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
/** //对于调用x.f("Hello")来说,如果C定义了f(String)则直接调用。否则将在C的超类中寻找f(String),以此类推。 */调用方法时的搜索时间开销相当大。因此虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法,虚拟机仅查找这个表就行了。
动态绑定有一个非常重要的特性:无需对代码进行修改就可以扩展程序。当对象变量引用新增类的实例时,调用方法不需要对方法重新编译,虚拟机会自动调用新增类实例的对应方法。
警告:覆盖方法时,子类方法不能低于超类方法的可见性。特别是当超类方法时public,子类方法一定要声明为public,防止javac将覆盖方法解释为更严格的访问权限。
5.1.7 阻止继承:final类和方法
-
不允许扩展的类被称为final类,可以阻止利用某个类定义子类。
/** public final class Executive extends Manager{ ... } */ -
类中的方法也可以被声明为final。子类不能覆盖这个方法。(final类中的所有方法自动声明为final)。
- 域也可以被声明为final,构造对象后其值不可改变。但是将一个类声明为final,只有其中的方法自动地声明为final。
-
将方法或类声明为final的主要目的是:确保他们不会在子类中改变语义。
-
如果一个方法没有被覆盖并且很短,编译器就能对其进行优化处理,这个过程称为内联(inlining)。虚拟机中的即时编译器比传统编译器的处理能力强得多,可以准确知道各个类的继承关系,并检测出类中是否真正存在覆盖给定的方法。如果方法很简短、被频繁调用且没有被真正被覆盖即时编译器就会做内联处理。如果虚拟机加载了另一个覆盖内联方法的子类,就会取消对覆盖方法的内联。这个过程很慢但很少发生。
5.1.8 强制类型转换
- 将某个类的实例引用转换成另外一个类的实例引用与数值表达式的类型转换语法类似,用圆括号将目标类名括起来放置在转换的对象之前。
- 进行类型转换的唯一原因是:在暂时忽视对象的实际类型使用对象的全部功能。而且只能在继承层次内进行类型转换。
- 只有超类实例使用子类特有的方法时才考虑类型转换。更合理的解决方式是检查超类的设计是否合理,重新设计一下超类并添加相应的子类方法才是最好的选择。
- 通过类型转换调整对象并不是一种好的做法,一般情况下尽量少用类型转换,因为实现多态性的动态绑定机制能够自动的找到相应的方法。
5.1.9 抽象类
- 在类的继承层次结构中,进行高层次的抽象可以将通用的方法放在继承层次较高的通用的超类中。
- 为了提高的程序的清晰度,包含一个或多个抽象方法的类本身必须使用
abstract关键字声明为抽象的。 - 类即使不含抽象方法,也可以被声明为抽象类。
- 抽象类不能被实例化,但是可以定义一个抽象类的实例变量引用非抽象子类的实例。
5.1.10 受保护访问
-
protected关键字可以声明超类中的方法和域,允许方法被子类访问,域被被子类中的方法访问。-
自己想尝试可以用子类实例继承超类的protected方法来输出超类众多实例中的某个实例的某个域值。发现做不出来。着手后定义超类时发现不能定义一个方法来输出众多实例中某一个实例的域值。一是因为定义超类时实例还没有创建,没有途径可以调用实例中用来输出其他实例域值的方法;二是因为以我目前的知识还做不出来;三是我想到了OOP,既然想输出众多实例中某一个实例的域值,那我new几个实例,new出域值后调用一下定义超类时定义的输出方法输出就可以了。
总结:4个访问修饰符似乎是用来声明类和方法的,对象是另外一个范畴。
-
5.2 Object:所有类的超类
-
Object类是所有类的超类,但是在声明类时并不需要明确的指出与Object的继承关系。如果在声明类时没有明确的指出超类,默认超类是Object。
-
可以使用Object类型的变量引用任何类型的实例,Object类型的变量只能用于作为各种值的通用持用者。要想对其中的内容进行操作,需要类型转换实例为原始类型。
在Java中只有基本类型不是对象,所有的数组类型都扩展了Object类。
5.2.1 equals()
-
Objec类中的equals方法用于检测两个对象是否具有相同的引用。
然而我们经常需要检测两个对象状态的相等性(是否具有相同的类型以及相同的属性值),这与判断两个对象是否具有相同的引用是有区别的。
5.2.2 相等测试与继承
-
Java语言规范要求equals()应该具有如下特性:
- 自反性:对于任何非空引用x,x.equals(x)应该返回true。
- 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。
- 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true。
- 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
- 对于任意非空引用x,x.equals(null)应该返回false。
-
为了检测两个对象状态的相等性,我们可能会需要根据实际情况自己编写一个equals(),下面给出一个完美编写的方法,如果是在子类重新定义equals(),就要在其中包含调用超类equals()。
- 在标准Java库中包含150多个equals()的实现,包括使用instanceof检测、调用getClass检测、捕获ClassCastException或什么也不做。在其中Timestamp继承自java.util.Date,而后者的equals()使用了instanceof测试,这样一来无法覆盖实现equals,使之同时做到对称且准确。
- 显示参数命名otherObject,稍后需要将它转换为另一个名为other的变量。
public boolean equals(Object otherObject) {}- 检测this与otherObject是否引用同一个对象。
if(this == otherObject) { return true; }- 检测otherObject是否为null,如果为null,返回false。
if(otherObject == null) { return false; }-
比较this与otherObject是否属于同一个类。
(1) 如果equals的语义在每个子类中有所改变,或者说子类有自己的相等概念,由于对称性需求就必须使用getClass检测。
(2) 如果所有的子类都拥有统一的语义,或者说是由超类决定相等的概念,那就使可以用instanceof检测,并将ClassName.equals()声明为final,这样可以在不同子类实例之间进行相等比较。
if(getClass() != otherObject.getClass()) { return false; }if(!(otherObject instanceof ClassName)) { return false; }- 将otherObject转换为相应的类变量
ClassName other = (ClassName)otherObject;- 开始对所有需要比较的域进行比较。使用
==比较基本类型域,使用equals()比较对象域,如果所有的域都匹配就返回true,否则返回false。
return field1 == other。field1 && Objects.eauqls(field2,other.field2) && ...;- 对于数组类型的域,可以使用静态的Arrays.equals()检测相应的数组元素是否相等。
-
API java.util.Arrays | 方法 | 解释 | | --- | --- | | static Boolean equals(type[] a,type[] b) | 如果两个数组长度相同,并且在对应位置上数据元素也相同,将返回true。数组的元素类型可以是8种基本类型或Object类型 |
-
API java.util.Objects | 方法 | 解释 | | --- | --- | | static Boolean equals(Object a,Object b) | 如果a和b都为null,返回true;如果其中之一为null,返回false;否则返回a.equals(b) |
5.2.3 hashCode()
- 散列码(hash code)是由对象导出的一个整型值。散列码没有规律。
- String使用下列算法计算散列码:
int hash = 0;
for(int i = 0; i < length(); i++) {
hash = 31 * hash + charAt(i);
}
-
没有定义hashCode()的类会使用Object默认的hashCode()导出实例的存储地址作为散列码。
-
如果重新定义equals()就必须重写hashCode(),以便用户可以将对象插入到散列表中。
重写时,hashCode()应该返回int值,可以为负,并合理的组织实例域的散列码,让各个实例产生的散列码更加均匀。
最好使用null安全的Object.hashCode()来生成散列码,如果其参数为null,这个方法会返回0否则会对参数调用Object默认的hashCode()。
可以使用静态方法Double.hashCode()来避免创建Double对象。
当需要组合多个散列值时,可以调用Objects.hash()提供多个参数,并对所有参数调用Object默认的hashCode()。
- 如果存在数组类型的域,可以使用静态的Arrays.hashCode()计算散列码,这个散列码由数组元素的散列码构成。
-
equals()与hashCode()的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()必须与y.hashCode()具有相同值。
-
API java.util.Object | 方法 | 解释 | | --- | --- | | int hashCode() | 返回对象的散列码,可以为负 | | static int hash(Object... Objects) | 返回一个散列码,由提供的所有对象的散列码组合得到 | | static int hashCode(Object a) | 如果a为null,返回0,否则返回a.hashCode() |
-
API java.lang.(Integer|Long|Short|Byte|Double|Float|Character|Boolean) | 方法 | 解释 | | --- | --- | | static int hashCode(int|long|short|Byte|double|float|character|boolean) value | 返回给定值的散列码 |
-
API java.util.Arrays | 方法 | 解释 | | --- | --- | | static int hashCode(type[] a) | 计算数组a的散列码,数组元素类型可以是8种基本类型或Object类型 |
5.2.4 toString()
-
Object本身已经定义了toString()用来打印生成对象所属类名和散列码。
只要对象与一个字符串通过
+连接,javac自动调用toString()获得这个对象的所属类名和散列码。如果x为任意对象,System.out.println(x)时,println()会自动调用toString()。
-
toString()可重写用于返回表示对象值的字符串。绝大多数重写的toString()返回值都遵循这样的格式:ClassName[field1Value = ..., field2Value = ..., ...]。
重写的toString()是一种非常有用的调试工具,在标准类库中,许多类都定义了toString()来获得一些有关对象状态的必要信息。
- 强烈建议为自定义类重写toString()。自己和其他人都会从这个日志记录支持中受益匪浅。
警告:数组继承了Object的toString()。 例如:
int\[] luckyNumbers = {2, 3, 5, 7, 11, 13}; String s = "" + luckyNumbers;当toString()被调用时会生成"[I@1a46e30"字符串,
[I表示是一个整型数组。 修正的方式是调用静态Arrays.toString()。 如果是生成多维数组,则需要调用Arrays.deepToString()。
- API java.lang.Object | 方法 | 解释 | | --- | --- | | Class getClass() | 返回包含实例信息的类实例 | | Boolean equals(Object otherObject) | 比较两个对象是否相等,如果两个对象指向同一块存储区域则返回true,否则返回false。在自定义的类中,应该覆盖这个方法 | | String toString() | 返回描述该对象值的字符串。在自定义的类中,应该覆盖这个方法 |
- API java.lang.Class | 方法 | 解释 | | --- | --- | | String getName() | 返回当前类名 | | Class getSuperclass | 以Class实例的形式返回当前类的超类信息 |
5.3 泛型数组列表
-
Java允许运行时确定数组的大小,不必在编译时就要确定。但在运行时想要动态更改数组的大小,最简单的方法就是使用ArrayList,ArrayList类似数组,在增删元素时可以自动调节数组容量的功能,而不需要编写任何代码。
-
ArrayList是一个采用类型参数(type parameter)的泛型类(generic class),用
<>将元素对象类型括起来加在后面。格式:ArrayList<Object> variable = new ArrayList<>();这被称为菱形语法(因为
<>),javac会检查检查新值是什么,如果赋值给一个变量、传递至某个方法或者从某个方法返回,javac会检查这个变量、参数或方法的泛型类型,然后将其放在<>中。 -
add()可以将元素添加到ArrayList中,如果ArrayList内部已满,ArrayList就会创建一个更大的数组,并将所有的对象从原数组拷贝到新数组中。
-
如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity()。这个方法会分配一个包含固定数量对象n的内部数组 ,然后调用固定数量的add()。
/** ArrayList\<Object> variable = new ArrayList<>(); variable.ensureCapacity(n); */或者直接把初始容量传递给ArrayList:ArrayList<Object> variable = new ArrayList<>(n);
警告:按照固定数量,分配ArrayList与为新数组分配空间不同。ArrayList可以继续重新分配空间甚至超过原定元素数量,但是在最初直至初始化之后,ArrayList中没有任何元素。
-
size()可以返回ArrayList中包含的实际元素数量。
每次添加新元素都要花时间移动存储块,所以确认不会继续添加任何元素时,可以调用trimToSize()将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器将回收多余的存储空间。
-
API java.util.ArrayList | 方法 | 解释 | | --- | --- | | ArrayList<E>() | 构造一个空ArrayList | | ArrayList<E>(int initialCapacity) | 用指定容量构造一个空ArrayList | | Boolean add(E obj) | 在数组列表的尾端添加一个元素,永远返回true | | int size() | 返回存储在数组列表中的当前元素数量 | | void ensureCapacity(int capacity) | 确保ArrayList在不需要重新分配空间的情况下就能够保存给定数量的元素 | | void trimToSize() | 将数组列表的容量削减到当前尺寸 |
5.3.1 访问ArrayList元素
-
ArrayList自动扩展容量的便利增加了访问元素语法的复杂程度。其原因是ArrayList不是Java程序设计语言的一部分,它只是由某些人编写并放在标准库里的一个实用类。
-
使用get()和set()实现访问或改变元素的操作。
改变元素格式:set(i, value) 访问获取元素格式:get(i)
警告: 只有i不超过ArrayList的大小时才能调用set()。
使用add()增加元素,而不是set(),set()只能替换数组中已经存在的元素内容。
-
可以使用带索引参数的add(),格式:add(n,e)
插入新元素后,位于n之后的元素都要向后移动一位,如果插入新元素后数组列表的大小超过了容量,ArrayList就会被重新分配空间。
-
删除一个元素格式:remove(n)
删除元素后,位于n之后所有的元素向前移动一位,并且数组的大小减1。
-
对ArrayList插入和删除效率较低。如果ArrayList存储的元素较多,又常需要插入和删除元素,要使用链表。
-
可以使用foreach()循环遍历ArrayList。
-
API java.util.ArrayList<T> | 方法 | 解释 | | --- | --- | | void set(int index, E obj) | 设置ArrayList指定位置元素值,会覆盖原有内容 | | E get(int index) |获得指定位置元素值 | | void add(int index, E obj) | 向指定位置插入元素 | | E remove(int index) | 删除指定位置元素 |
5.3.2 类型化与原始ArrayList的兼容性
-
使用类型化ArrayList与带有原始ArrayList的代码交互操作时,并不需要做类型转换。
/** public class EmployeeDB { public void update(ArrayList list) {...} ArrayList<Employee> staff = ...; employeeDB.update(staff); } */ -
使用原始ArrayList与带有类型化ArrayList的代码交互操作时,会得到警告。
/** public class EmployeeDB { public ArrayList find(String query) {...} // yields warning ArrayList<Employee> result = employeeDB.find(query); } */使用类型转换会得到另外的警告信息,指出类型转换有误。
/** // yields another warning ArrayList<Employee> result = (ArrayList<Employee>)employeeDB.find(query); */ -
鉴于兼容性的考虑,javac对类型转换进行检查后,如果没有发现违反规则,就将所有的类型化ArrayList转换成原始ArrayList对象。在程序运行时,所有的ArrayList都一样,没有虚拟机中的类型参数,类型化ArrayList和原始ArrayList将执行同样的运行时检查。
使用类型化ArrayList与带有原始ArrayList的代码交互操作时,我们只需要注意javac的警告不会造成太严重的后果就可以,同时使用
@SuppressWarnings("unchecked")标注来标记这个变量能够接受类型转换。
5.4 对象包装器与自动装箱
- 所有的基本类型都有一个与之对应的类,这些类称为包装器(wrapper):Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean,前6个类派生与公共的超类Number。
- 对象包装器类不可变,一旦构造了包装器,就不允许更改包装在其中的值。对象包装器是final,不能定义它们的子类。
警告: 由于值被包装在对象中,所以ArrayList<Integer>的效率远远低于int[]数组,因此应该用它构造小型集合。
-
Integer实例获取int元素时,Integer实例会自动调用:valueOf(n),自动调用valueOf()的操作称为自动装箱(autoboxing)。
从Integer实例中获取int元素时,Integer实例会自动调用:intValue(),自动调用intValue()的操作称为自动拆箱。
比较两个包装器实例中引用元素值可以调用equals()。
- 自动装箱要求boolean、byte、char<=127。
-
由于包装器引用可以为null,所以可能会抛出NullPointerException异常。
-
在一个条件表达式中混合使用Integer和Double类型,Integer值会拆箱转换为double,再装箱为Double。
-
装箱和拆箱是javac认可的,而不是虚拟机。javac在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
-
API java.lang.Integer | 方法 | 解释 | | --- | --- | | int intvalue() | 以int的形式返回Integer实例的值(Number中覆盖了此方法) | | static String toString(int i) | 以一个String实例的形式返回给定i的十进制表示 | | static String toString(int i, int radix) | 返回给定i的基于给定radix参数进制的表示 | | static int parseInt(String s) | 返回字符串s的十进制的值 | | static int parseInt(String s, int radix) | 返回字符串s的给定radix参数进制的值 | | static Integer valueOf(String s) | 返回用s表示的值的十进制Integer实例 | | static Integer valueOf(String s, int radix) | 返回用s表示的值的给定radix参数进制Integer实例 |
-
API java.text.NumberFormat | 方法 | 解释 | | --- | --- | | Number parse(String s) | 返回给定s表示的值的数字值 |
5.5 参数数量可变的方法
5.6 枚举类
-
枚举类型定义好以后尽量不要构造新对象,在比较两个枚举类型的值时永远不要使用equals(),直接使用
==就可以。 -
Enum中,toString()的逆方法是静态方法valueOf()。
/** //将s设置成Size.SMALL Size s = Enum.valueOf(Size.class,"SMALL"); */ -
Enum中,静态values()用于返回一个包含所有枚举值的数组。静态ordinal()用于返回声明中某个枚举常量的位置,位置从0开始计数。
-
如同Class类一样,出于简化考虑,Enum类省略了一个类型参数。类型参数在compareTo()中使用
/** //例如,实际上应该将枚举类型Size扩展为Enum<Size> */
-
-
API java.lang.Enum<E> | 方法 | 解释 | | --- | --- | | static Enum valueOf(Class enumClass, String name) | 返回指定名字,给定类的枚举常量 | | String toString() | 返回枚举变量名 | | int ordinal() | 返回声明中某个枚举常量的位置,位置从0开始计数。 | | int compareTo(E other) | 比较枚举常量和other之间的次序,如果出现在other之前,则返回一个负值;如果本身就是other,返回0;否则,返回正值 |
5.7 反射
- 反射库(reflection library)提供了一个丰富的工具集,以便编写动态操纵Java代码的程序。能够分析类能力的程序称为反射(reflective),反射机制可以用来:
- 在运行时分析类的能力
- 在运行时查看对象
- 实现通用的数组操作代码
- 利用method对象
5.7.1 Class类
-
程序运行期间,Java始终为所有的对象维护一个被称为运行时的类型标识,这个标识跟踪着每个对象所属的类,虚拟机利用这个类型类型标识的信息选择相应的方法执行。
可以用专门的Java类访问这些信息,保存这些信息的类被称为Class,Object的getClass()会返回一个Class的实例。
-
最常用的方法是getName(),用来返回类的名字,如果类在一个包里,包的名字也会作为返回类名的一部分。
/** Random generator = new Random(); Class cl = generator.getClass(); String name = cl.getName(); //name会返回“java.util.Random” */静态方法forName()可以获得类名对应的Class实例。如果类名保存在字符串中,并可在运行中改变就可以使用这个方法。只有入参是类名或者接口名才会执行,否则会抛checked exception。所以使用这个方法都要提供一个异常处理器(exception handler)。
- 启动时包含main()的类被加载,进而一层一层的向下加载全部需要的类。可以先显示一个启动画面,然后调用Class.forName()手工加载其他全部类,可以给人启动速度比较快的感觉。不过要确保main()的类没有显式的加载其他类。
-
第三种获得Class类对象的方法是任意的Java类型后缀
.class。- Class类实际上是泛型类。例如,int.class的类型是Class<Int>,这是一个Class类的实例,但表示的是int类型。
警告:鉴于历史原因,getName()应用于数组类型的时候会返回一个很奇怪的名字:
·Double[].class.getName()返回“[Ljava.lang.Double”
·int[].class.getName()返回“[I”
-
虚拟机为每个类型管理一个Class对象,可以利用
==来比较两个类对象。/** if(e.getClass() == Employee.class) */newInstance()可以动态的创建一个类的实例。不过他调用的只能是默认的构造器,如果这个类中没有默认的构造器就会抛出异常。
/** e.getClass().newInstance() */
5.7.2 捕获异常
-
程序运行发生错误就会抛出异常,程序终止,并在控制台上打印一条信息,其中给出了异常类型。这比直接终止程序灵活,因为可以提供异常处理器来捕获并处理。
-
异常有两种类型,未检查异常和已检查异常。只有对于已检查异常,javac才会检查是否提供了处理器。所以我们应该精心编写代码来避免异常,而不是精心编写异常处理器。
-
实现语法:
/** try{ //可能抛出异常的代码块 } catch (Exception e){ //处理器的处理方式,例如:e.printStackTrace(); } /如果try中某句代码抛出异常,则直接跳过try中的剩余代码语句进入catch子句里。printStackTrace()是Throwable的方法,用来打印栈的轨迹,Throwable是Exception的超类。如果try中没有抛出异常,则会跳过catch子句。
-
API java.lang.Class | 方法 | 解释 | | --- | --- | | static Class forName(String classname) | 返回描述类名为className的Class实例 | | Object newInstance() | 返回这个类的一个新实例 |
-
API java.lang.reflect.Constructor | 方法 | 解释 | | --- | --- | | Object newInstance(Object[] args) | 构造一个这个构造器所属类的新实例。args是提供给构造器的参数 |
-
API java.lang.Throwable | 方法 | 解释 | | --- | --- | | void printStackTrace() | 将Throwable实例和栈的轨迹输出到标准错误流 |
5.7.3 利用反射分析类的能力
-
java.lang.reflect中有Field、Method和Constructor三个类,对应用来描述域、方法和构造器。这三个类都有getName(),返回所指的名称。都有getModifiers(),返回一个int来描述修饰符的使用状况。
Field还有getType()来返回域所属类的Class实例。Method有方法可以报告参数类型和返回类型。Constructor有方法可以报告参数类型。
java.lang.reflect中,Modifier有静态方法可以分析getModifiers()返回的int。isPublic()、isPrivate()、isFinal()可以判断方法或构造器的修饰符,toString()可以将修饰符打印出来。
-
Class中的getFields()、getMethods()、getConstructors()可以返回类提供的public域、public方法、public构造器数组,其中包括超类的public成员。
getDeclaredFields()、getDeclaredMethods()、getDeclaredConstructors()返回声明的全部成员,但是不包括超类的成员。
- 程序清单5-13 reflection/ReflectionTest.java
package reflection;
import java.util.*;
import java.lang.reflect.*;
/**
* This program uses reflection to print all features of a class.
* @version 1.1 2004-02-21
* @author Cay Horstmann
*/
public class ReflectionTest
{
public static void main(String[] args)
{
// read class name from command line args or user input
String name;
if (args.length > 0) name = args[0];
else
{
Scanner in = new Scanner(System.in);
System.out.println("Enter class name (e.g. java.util.Date): ");
name = in.next();
}
try
{
// print class name and superclass name (if != Object)
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print("class " + name);
if (supercl != null && supercl != Object.class) System.out.print(" extends "
+ supercl.getName());
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
System.exit(0);
}
/**
* Prints all constructors of a class
* @param cl a class
*/
public static void printConstructors(Class cl)
{
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors)
{
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(name + "(");
// print parameter types
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all methods of a class
* @param cl a class
*/
public static void printMethods(Class cl)
{
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods)
{
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
// print modifiers, return type and method name
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
// print parameter types
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all fields of a class
* @param cl a class
*/
public static void printFields(Class cl)
{
Field[] fields = cl.getDeclaredFields();
for (Field f : fields)
{
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.println(type.getName() + " " + name + ";");
}
}
}
- API java.lang.Class | 方法 | 解释 | | --- | --- | | Field[] getFields() | 返回一个包含Field实例的数组,这些实例记录了这个类和其超类的公有域 | | Field[] getDeclaredFields() | 返回一个包含Field实例的数组,这些实例记录了这个类的全部域 | | Mothod[] getMothods() | 返回一个包含Mothod实例的数组,这些实例记录了这个类和其超类的公有方法 | | Mothod[] getDeclaredMothods() | 返回一个包含Mothod实例的数组,这些实例记录了这个类或者接口的全部方法 | | Constructor[] getConstructors() | 返回一个包含Constructor实例的数组,这些实例记录了这个类和其超类的公有构造器 | | Constructor[] getDeclaredConstructors() | 返回一个包含Constructor实例的数组,这些实例记录了这个类的全部构造器 |
- API java.reflect.Field | 方法 | 解释 | | --- | --- | | Class getDeclaringClass() | 返回一个类中域的声明类型的Class实例 | | int getModifiers() | 返回一个描述域的修饰符的int | | String getName() | 返回一个描述域名的串 |
- API java.reflect.Method | 方法 | 解释 | | --- | --- | | Class getDeclaringClass() | 返回一个类中方法的声明类型的Class实例 | | Class[] getExceptionTypes() | 返回一个类中方法抛出异常类型的Class实例数组 | | int getModifiers() | 返回一个描述方法的修饰符的int | | String getName() | 返回一个描述方法名的串 | | Class[] getParameterTypes() | 返回一个描述方法参数类型的Class实例数组 | | Class[] getReturnTypes() | 返回一个描述方法返回类型的Class实例 |
- API java.reflect.Constructor | 方法 | 解释 | | --- | --- | | Class getDeclaringClass() | 返回一个类中构造器的声明类型的Class实例 | | Class[] getExceptionTypes() | 返回一个类中构造器抛出异常类型的Class实例数组 | | int getModifiers() | 返回一个描述构造器的修饰符的int | | String getName() | 返回一个描述构造器名的串 | | Class[] getParameterTypes() | 返回一个描述构造器参数类型的Class实例数组 |
- API java.reflect.Modifier | 方法 | 解释 | | --- | --- | | Static String toString(int modifiers) | 将描述域、方法、构造器的修饰符的int转成串 | | Static boolean isAbstract(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isFinal(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isInterface(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isNative(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isPrivate(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isProtected(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isPublic(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isStatic(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isStrict(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isSynchronized(int modifiers) | 判断描述域、方法、构造器的修饰符的int | | Static boolean isVolatile(int modifiers) | 判断描述域、方法、构造器的修饰符的int |
5.7.4 在运行时使用反射分析对象
-
利用反射机制可以查看在编译时还不清楚的对象域值。
如果f是一个Field类型的对象(例如,通过getDeclaredFields()得到的对象),obj是包含f里面的域的某个类的实例,f.get(obj)将返回一个对象,其值为obj域的当前值。如果要查看的域是数值类型,Field的getDouble()等或者get()都可以自动的将域值打包到相应的对象包装器中。f.set(obj,value)可以设置f里面的域在obj里的值。
/** Employee harry = new Employee("Harry Hacker"); Class cl = harry.getClass(); // 获取harry的Class实例 Field f = cl.getDeclaredField("name"); // 获取Class实例的name域 Object v = f.get(harry); // 获取name域在harry里的值,并赋给v,v是一个String Object,值是"Harry Hacker" */ -
除非拥有访问权限,否则Java安全机制只允许查看有哪些域。有访问权限下,只有get()能得到可访问域的值。如果访问私有域值会抛IllegalAccessException。
-
反射机制的默认行为,默认接口除了受限与Java的安全管理器外,其次是Java的访问控制。如果一个Java程序没有受限于安全管理器的控制,就可以通过Field、Method、Constructor的setAccessible()覆盖访问控制。 setAccessible()是AccessibleObject的一个方法,AccessibleObject是Field、Method、Constructor的公共超类。这个特性是为调试、持久存储和相似机制提供的。