类、超类和子类
经理类与普通雇员类有很多相同之处,但还有一些差别。
经理在完成本职任务不仅可以获得工资,还获得奖金。而普通雇员只能获取工资。故而,可以重用Employee类中已编写的部分部分,还可在其中在增加一些新的功能。
每个经理都是一个雇员,是 is a 的关系。 --------继承的特征
继承Employee类来定义Manager类,关键字extends表示继承
class Manager extends Employee{ //所有的继承都为公有继承
添加方法和域
}
关键字extends :正在构建的新类派生于一个已存在的类
已存在的类----称为超类、基类或父类
新类----称为子类、派生类或孩子类
子类比超类的功能更多
Manager类–子类 Employee类—超类
在Manager类中添加一个存储奖金信息的域,和用于设置这个域的方法:
class Manager extends Employee{
......
public void setBonus(double b){ //获取奖金信息
bonus = b;
}
private double bonus; //奖金信息
}
Manager boss = new Manager(......);
boss.setBonus(5000);
setBonus方法在Manager类中定义,故Employee类的对象不可使用该方法。
虽然在Manager类中没有显示地定义getName和getHireDay等方法,但属于Manager类的对象可以使用它们,因为Manager类自动继承了超类Employee中的这些方法
同样也继承了name,salary,hireDay这三个域。故Manager类对象包含四个域:name,salary,hireDay,bonus
通用方法在超类中,特殊方法在子类中。
覆盖超类中的某方法:只有Employee类的方法才能够访问Employee中的私有部分(域)
public double getSalary(){ //覆盖超类的方法
double baseSalary = super.getSalary();
//运用super关键字调用超类的方法
return baseSalary+bonus;
}
在子类中可增加域、增加方法或覆盖超类的方法,但是不可以删除继承的任何域和方法。
super在构造器的应用:
public Manager(String n,double s,int year,int month,int day){
super(n,s,year,month,day); //使用super调用构造器的语句必须是子类构造器的第一条语句
bonus = 0;
}
super(n,s,year,month,day) 调用超类Employee中含有n,s,year,month和day参数的构造器
Manager类的对象都不能访问Employee类的私有域,所以必须通过super实现对超类的域/方法的调用。
若子类的构造器没有显示地调用超类的构造器,则将自动调用超类(无参数)的构造器。
若超类没有带参数的构造器,并且子类的构造器中没有显示地调用超类的其他构造器,则Java编译器将报告错误。
例:
//创建一新经理,并设置其奖金
Manager boss = new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);
//定义一包含3个雇员的数组
Employee[] staff = new Employee[3];
//经理和雇员都放到数组中
staff[0] = boss;
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Tony Tester",40000.1990,3,15);
//输出每个人薪水
for[Employee e:staff]
System.out.peirnln(e.getName()+" "+e.getSalary());
运行结果
Carl Cracker B5000.0
Harry Hancker 50000.0
Tony Tester 40000.0
staff[1]和staff[2]对应Employee对象,仅输出基本薪水;staff[0]对应Manager对象,它的getSalary方法将奖金与基本薪水加在一起。
e.getSalary()能够确定应执行哪个getSalary方法。这里e声明为Employee类型,但实际上e既可以引用Employee类型的对象,也可以引用Manager类型的对象。
当e引用Employee对象时,e.getSalary()调用的是Employee类中的getSalary方法;当e引用Manager对象时,e.getSalary()调用的是Manager类中的getSalary方法。虚拟机知道e实际引用的对象类型,因此能够正确地调用相应的类方法。
一个对象变量(例:变量e)可以引用多种实际类型的现象称为多态。在运行时能够自动地选择调用哪个方法的现象称为动态绑定。
5.1.1 继承层次
Employee类–>派生–>Manager类,Secretary类,Programmer类
Manager类–>派生–>Executive类
继承层次:由一个公共超累派生出来的所有类的集合
类的继承链:在继承层次中,从某个特定的类到其祖先的路径
一个祖先类可拥有多个子孙继承链。
5.1.2 多态
Employee e;
e = new Employee(...);
e = new Manager(...);
//一个Employee变量既可以引用一个Employee类对象,
//也可以引用一个Employee类的任何一个子类的对象(例:Manager、Executive等等)。
置换法则:
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
变量staff[0]与boss引用同一个对象。但编译器将staff[0]看成Employee对象。
boss.bonus(5000); //可以
//boss声明的类型是Manager,setBonus是Manager类的方法。
staff[0].Bonus(5000); //出错
//staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。
不能将一个超类的引用赋给子类变量
Manager m = staff[i]; //出错
不是所有的雇员都是经理。如果赋值成功,m有可能引用了一个不是经理的Employee对象,当在后面调用m.setBonus(…)时就有可能发生运行时错误。
注:
Manager[] managers = new Manager[10];
Employee[] satff = managers;
合法,但是这里将一普通雇员归入了经理行列。应避免这种引用。
5.1.3 动态绑定
调用对象方法的过程:
- 编译器查看对象的声明类型和方法名。获取所有可能被调用的候选方法
可能存在方法f(int),f(String)。编译器将会一一列举所有该类中名为f的方法和其超类中访问属性为public且名为f的方法。 - 编译器将查看调用方法时提供的参数类型。获取需调用的方法名字和参数类型
在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择该方法—该过程为重载解析。 - 若是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。调用时虚拟机查此表即可。
当搜索方法表时,若多态情况下,即可引用超类,也可引用子类。则会搜索子类和超类的方法表。
Employee的方法表:
Employee:
getName()---->Employee.getName()
getSalary()---->Employee.getSalary()
getHireDAy()---->Employee.getSalary()
raiseSalary(double)---->Employee.raiseSalary(double)
Manager的方法表:
Manager:
getName()---->Employee.getName()
getSalary()---->Manager.getSalary() //重写
getHireDay---->Employee.getHireDay()
raiseSalary(double)---->Employee.raiseSalary(double)
setBonus(double)---->Manager.setBonus(double) //新增
在运行时,调用e.getSalary()的解析过程:
- 先虚拟机提取e的实际类型的方法表。可能Employee、Manager、或Employee类的其他子类的方法表
- 再虚拟机搜索定义getSalary签名的类。此时,虚拟机已知道应调用哪个方法
- 虚拟机调用方法
5.1.4 阻止继承:final类和方法
final类:不允许扩展的类
阻止定义Executive类的子类:
final class Executive extends Manager{
......
}
类中的方法被声明为final,则子类不能覆盖该方法:
class Employee{
......
public final String getName(){
return name;
}
......
}
将方法或类声明为final可确保他们不会在子类中改变语义。
设计类层次时,应仔细考虑哪些方法和类声明为final。
内联:若一个方法没有被覆盖且很短,编译器就能对它进行优化处理。
例:内联调用e.getName()将被替换为访问e.name域。
若getName在另一个类中被覆盖,那么编译器就不知道覆盖的代码会做什么操作了,因此不可对其进行内联处理。
虚拟机中的即时编译器可准确知道类之间的继承关系,并能检测出类中是否真正地存在覆盖给定的方法。若方法简短、被频繁调用且没真正被覆盖,则即时编译器会将方法进行内联处理。若被覆盖了,则优化器将取消对覆盖方法的内联。该过程很慢且很少发生。
5.1.5 强制类型转换
Manager boss = (Manager)staff[0]; //staff数组为Employee对象的数组
子类的引用可赋值给超类变量,但超类的引用可赋值给子类变量。
但可以用instanceof运算符查一下是否能转换成功:(在超类转换为子类之前)
if(staff[1] instanceof Manager){
boss = (Manager) staff;
......
}
若这个转换不能成功,编译器将不会进行这个转换。
一般情况下,应尽量少使用类型转换和instanceof运算符。
5.1.6 抽象类
抽象----祖先类更加抽象,用于派生其他新类。将通用的属性、方法放到超类中,有利于继承。
abstract class Person{ //抽象类
public Person(String){
}
public abstract String getDescription(); //抽象方法
......
}
扩展抽象类的两个方法:
- 在子类中定义部分抽象方法或抽象方法不定义,则必须将子类也标记为抽象类
- 定义全部的抽象方法,子类就不用定义抽象类了
就算类不含抽象方法,也可声明为抽象类。
抽象类不能实例化------若将一个类声明为abstract,就不能创建这个类的对象。但可以创建一个具体子类的对象。
接口中会有更多抽象方法见地6章。
5.1.7 受保护访问
protected : 超类的某些方法允许被子类访问,或允许子类访问的方法访问超类的某个域。
例:
若是将超类Employee中的hireDay声明为protected,而不是私有的,Manager中的方法就可以直接访问它。
而且Manager类中的方法只能够访问Manager对象中的hireDay域,而不能访问其他Employee对象中的这个域。这种限制有助于避免滥用保护机制,使得子类只能获取访问受保护域的权利。
谨慎使用protected。因其可以对私有的域进行修改,导致其他人员不知,会造成混乱。修改后要进行通知。
Java用于控制可见性的四个访问修饰符:
1 private-----仅对本类可见
2 public-----对所有类可见
3 protected------对本包和所有子类可见
4 默认------对本包可见 无任何修饰符 不常用
5.2 Object-所有类的超类
Object类是Java中所有类的最终祖先,每个类都有它扩展而来。
但不需要写成:
class Employee extends Object
可用Object类型的变量引用任何类型的对象:
Object obj = new Employee("Harry Hacker",35000);
Employee e =(Employee)obj;
//在具体操作时,还要使用类型转换,在进行其他操作。
5.2.1 Equals方法
Object类的Equals方法:检测以对象是否等于另一个对象。====>引用是否相同
比较两个对象是否相等没有太大的意义,经常需要比较的是两个对象的状态是否相等,若状态相等了,两个对象也就是相等的了
例:
若两个雇员对象的姓名、薪水和雇用日期都是一样的,就认为他们是相等的。(实际应用中,ID更有意义)
class Employee{
......
public boolean equals(Object otherObject){
if(this==otherObject) return true;
if(this==null) return false;
if(getClass()!=otherObject.getClass()) return false;
Employee other = (Employee)otherObject;
return name.equals(other.name) && salary==other.salary && hireDay.equals(other.hireDay);
}
}
getClass方法返回一个对象所属的类。检测时,只有两个对象属于同一个类事才可能相等。
在子类中定义equals方法时,首先调用超累的equals。若失败,对象则不可能相等;若成功,则继续比较子类中的实力域。
5.2.2 相等测试与继承
进行相等测试时,不建议使用:
if(!(otherObject instanceof Employee) return false;
会出现其他麻烦。不建议使用这种方式(返回False的方式)
equals的特性:
- 自反性:
对任意非空引用x,x.equals(x)应返回true - 对称性
对任意引用x、y,当且仅当y.equals(x),x.equal(y)都应返回true - 传递性
对任意x、y、z,若x.equals(y)返回true,y.equals(z)、x.equals(z)也返回true - 一致性
若x、y引用的对象没有发生变化,则反复调用x.equals(y)返回的结果一致。 - 对任意非空引用x,x.equals(null)应该返回false
很好编写equals方法的建议:
1)显示参数命名为otherObject,稍后将它转换成另一个叫做other的变量。
2)检测this与otherObject是否引用同一个对象:
if(this == otherObject) return true;
这个等式比一个一个地比较类中的域所付出的代价小。
3)检测otherObject是否为null,若为null,返回false。
if(otherObject == null) return false;
4)比较this与otherObject是否属于同一个类。
这里可使用getClass检测:
if(getClass() != otherObject.getClass()) return false;
若所有子类都拥有统一的语义,就使用instanceof检测:
if(!(otherObject insatnceof ClassName)) return false;
5)将otherObject转换为相应的类类型变量:
ClassName other = (ClassName) otherObject;
6)对所有需要比较的域进行比较,使用==比较类型基本域,使用equals比较对象域。若都匹配,返回true;否则返回false。
return field1 == other.field1
&&field2 .equals(other.field2)
&&......
若在子类中重新定义equals,需调用super.equals(other).
static Boolean equals(type[] a,type[] b)
//若两个数组长度相同,并且在对应位置上数据元素也均相同,将返回true。数据的元素类型可以是:Object,int,long,short,char,byte,boolean,float或double。
5.2.3 HashCode方法
Hash Code–散列码 :由对象导出的一整数值,无规律。
HashCode方法定义在Object类中,每个对象都有一个默认的散列码,其值为对象的存储地址。
例:s,t,sb,st内容一致,只不过s,t为String类型,sb,st为StringBuffer类型
String s = “OK”;
String t = new String("OK");
String sb = new StringBuffer(s);
String tb = new STringBuffer(t);
System.out.println(s.hashCode()+" "+sb.hashCode());
System.out.println(t.hashCode()+" "+tb.hashCode());
输出:
2556 20526976
2556 20567144
注意:字符串s与t拥有相同的散列码,由于字符串的散列码是由内容导出的。
字符串缓冲sb与tb却有不同的散列码,由于String Buffer类中没有定义hashCode方法,他的散列码是由Object类的默认hashCode方法导出的对象存储地址。
若重新定义equals方法,就必须重新定义hashCode方法,以便用户可将对象插入到散列表中。详细内容在第二卷第二章。
HashCode方法应该返回一个整数值(可以为负数),并合理组合实例域的散列码,以便使各个不同对象产生的散列码更均匀)
Equals与hashCode的定义必须一致:若x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。
例:若定义的Employee.equals比较雇员的ID,那么hashCode方法就需要散列ID,而不是雇员的姓名或存储地址。
5.2.4 ToString方法
toString方法:返回对象值的字符串
Point类的toString方法返回的字符串:java.awt.Point[x=10,y=20]
绝大多数的toString方法使用格式:类名[域值]
Employee类中toString方法的实现:
public String toString(){
return "Employee[name="+name+
",salary="+salary+
",hireDay="+hireDay+
"]";
}
也可将Employee换成getClass().getName()+“name=”+name+…
getClass().getName()-----获得类名的字符串
toString方法子类也可使用
若超类中使用的是getClass().getName(),则在子类中调用就要使用super.toString()就可。
class Manager extends Employee{
......
public String toString(){
return super.toString()+"[bonus="bonus+"]";
}
}
输出
Manager[name=...,salary=...,hireDay=...][bonus=...]
只要对象与一字符串通过“+“连接,Java编译器就会自动调用toString方法,获取该对象的字符串描述。
Object类定义了toString方法:输出对象的类名和散列值
例:
System.out.println(System.out);
输出
java.io.PrintStream@2f6684
因为PrintStream类中没有覆盖toString方法
java.lang.Object
Class getClass():返回包含对象信息的类对象
Object clone():创建一个对象的副本。Java运行时系统将为新实例分配存储空间,并将当前的对象复制到这块存储区域中。
java.lang.Class
String getName():返回这个类的名字
CLass getSuperclass():以Class对象的形式返回这个类的超类信息
5.3 泛型数组列表
为解决过大设置数组的大小造成浪费,Java中定义了ArrayList类,使用起来很像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。
ArrayList是一个采用类型参数的泛型类。ArrayList 指定数组列表保存的元素对象类型。自定义泛型类见第十三章。
声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
Vector类实现动态数组,但ArrayList类更加有效,故不再使用Vector类。
add方法:把数据添加到数组列表中
staff.add(new Employee("Harry Hacker",)...) ;
staff.add(new Employee("Tony Tester",...));
若调用add且内部数组已满,数组列表将自动创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
若能清楚知道或估算出数组可能的存储大小,可在填充数组前调用ensureCapacity方法:
staff.ensureCapacity(100);
数组的大小是为数组分配100个元素的位置空间,数组就有100个空位置可使用。
数组列表的容量为100个元素,只是拥有保存100个元素的潜力,实际上分配会超过100,但在开始,完成初始化后,数据列表根本不含有任何元素。
size方法:数组列表中实际包含的元素数目 ==a.length
staff.size();
确认数组列表的大小不再变化后,调用trimToSize方法:将存储空间的大小调整为当前元素所需的存储空间的数目。垃圾回收器回收多余的存储空间。
调整后再添加新元素需要花时间再次移动存储块,所以应该在确认不会再添加任何元素时,再调用trimToSize。
5.3.1 访问数组列表元素
数组列表优劣势:
优势:数组列表可自动扩展容量
劣势:访问元素语法的复杂程度增加了
get、set方法实现访问或改变数组元素的操作,而不是 [ ] 格式
staff.set(i,harry);
等价于
a[i] = harry;
add方法是添加新元素;set方法是对已经存在的元素进行替换。故set中i取值为[0,length-1]
既可以灵活扩展数组,又可方便访问数组元素:
//创建一个数组,并添加所有元素
ArrayList<X> List = new ArrayList<X> ();
while(...){
x=...;
List.add(x);
}
//使用toArray方法将数组元素拷贝到一个数组中
X[] a = new X[List.size()];
List.toArray();
//可在数组列表的尾部、中间插入元素(使用带索引参数的add方法):
int n = staff.size()/2;
staff.add(n,e);
//插入一新元素,位置n之后的所有元素都想后移动一个位置
//若插入新元素之后,数组列表的大小超过了容量,数组列表就会重新分配存储空间
//从数组列表中删除一个元素
Employee e = staff.remove(n);
//位于n之后的所有元素都向前移动一个位置,且数组大小减一
对小型数组来说,插入、删除元素的操作效率较低,但仍可用;对于元素较多的元素,又经常需在中间位置插入、删除元素,就应该考虑使用链表了。有关链表的见第十三章。
for each循环对数组列表遍历:
for(Employee e:staff)
do something with e
将Employee[ ]数组改为ArrayList:(ArrayList与数组的不同)
- 不必指出数组的大小
- 使用add将任意多的元素添加到数组中
- 使用size()替代length计算元素的数目
- 使用a.get(i)替代a[i]访问元素
5.3.2 类型化与原始数组列表的兼容性
兼容性:编译器在对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始的ArrayList对象。在程序运行时,所有的数组列表都是一样的----没有虚拟机中的类型参数。因此,ArrayList和ArrayList的类型转换将执行相同的运行时检查。
ArrayList<> a = b ; // b返回一个ArrayList类型的对象 报错 出现交叉错误
5.4 对象包装器与自动打包
所有的基本类型都有一个与之对应的类:Integer类对应基本类型int 等。
称这些类为包装器。
对象包装器类名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6个类派生于公共的超类Number)。
对象包装器类是不可变的,一经构造不可更改包装中的值。因对象包装器类是final,因此不能定义它们的子类。
在ArrayLIst<>中的<>的参数类型不允许是基本类型,不可写成ArrayList;就需要用到Integer对象包装器类。
声明一个Integer对象的数组列表:
ArrayList<Integer> list = new ArrayList<Integer>();
注:ArrayList中每个值分别包装在对象中,所以其效率远远低于int[ ]数组。故应用它构造小型数组,原因在于程序员操作的方便性比执行效率更重要。
自动打包:
//添加或获取数组元素时,自动打包
list.add(3);
//将自动变成
list.add(new Integer(3));
自动拆包:
//当一个Integer对象赋值给一个int值时,自动拆包
int n = list.get(i);
//翻译成
int n = list.get(i).intValue();
甚至在算术表达式中也能自动打包和拆包:
//将自增操作符应用于一个包装器引用:
Integer n = 3;
n++;
编译器将自动插入一条拆开对象包的指令,然后进行自增计算,最后再将结果打入对象包内。
== 运算符可用应用于对象包装器的对象,检测对象是否指向同一个存储区域:
Integer a =1000;
Integer b =1000;
if(a==b) ... //这种比较通常不会成立
在两个对象比较时一般应调用equals方法。
打包和拆包时编译器认可的,不时虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
数值对象包装器:可将某些基本方法放置在包装器中
//将一个数字字符串转换成数值
int x = Integer.parseInt(s); //parseInt是一个静态方法。将此方法放在Integer类中是极好的
5.5 参数数值可变的方法
可以用可变的参数数值调用的方法-----"可变参"方法
可变参数方法:诸如printf("%d",n)或printf("%d , %s",n,“weidgets”)等
printf中参数可以是两个,三个或更多,这是因为printf方法被定义为:
public class PrintStream{
public PrintStream printf(String fmt,Object... args){
return format(fmt,args);
}
}
省略号…是Java代码的一部分,表示可接收任意数量的对象(除fmt参数以外)。
printf方法主要接收两个参数:一是格式字符串,另一个是Object[ ]数组----含所有的参数;若类型不一,则将它们自动打包成对象。当扫描fmt字符串时,讲将第i个格式说明符与args[i]的值匹配起来。
编译器需要对printf的每次调用进行转换,以便参数绑定到数组上,并在必要时进行自动打包。
用户可自定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型。
例:
//计算若干个数值的最大值
public static double max(double... values){
double largest = Double.MIN_VALUE;
for(double v:values)
if(v>largest)
largest = v;
return largest;
}
//调用max方法:
double m = max(3.1,49,3,-5);
编译器将new double[ ]{3.1,49,3,-5}传递给max方法
5.6 枚举类
例:
public enum Size{SMALL,MEDIUM,LARGE,EXTRA_LARGE};
这个声明定义的类型是一个类,有四个实例。
在比较两个枚举类型的值时,直接用 == 就可以,不要使用equals
可在枚举类型中添加一些构造器、方法和域。其中构造器只在构造枚举常量时被调用。
例:
enum Size{
SMALL("s"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private Size(String abbreviation){ this.abbreviation = abbreviation;}
public String getAbbreviation(){ return abbreviation;}
private String abbreviation;
}
所有的枚举类型都是Enum类的子类。它们继承了Enum类的许多方法。最有用的一个是toString----获得枚举常量名
例:
Size.SMALL.toString(); //返回字符串“SMALL“
toString的逆方法----静态方法valueOf
Size s = (SIze) Enum.valueOf(SIze.class,"SMALL");
//将s的设置为Size.SMALL
每个枚举类型都有一个静态的values方法----返回一个包含全部枚举值的数组
例:
Size[ ] values = Size.values();
//返回 包含元素Size.SMALL,Size.MEDIUM,
//Size.LARGE,Size.EXTRA_LARGE的数组
ordinal方法----返回Enum声明中枚举常量的位置,位置从0开始。
Size.MEDIUM.ordinal();//返回1
5.7 反射
反射库----便于编写可动态操纵Java代码的程序。
该功能广泛应用于JavaBeans中,它是Java组件的体系结构—JavaBeans详细内容见卷II。 反射可支持Visual Basic用户习惯使用的工具
特别在设计或运行中添加新类时,能快速应用开发工具动态的查询新添加类。
反射----能够分析类能力的程序。
用反射机制可以:
- 在运行中分析类的能力
- 在运行中查看对象,例:编写一个toString方法供所有类使用
- 实现数组的操作代码
- 利用Method对象,这个对象很像C++中的函数指针
使用反射的主要对象是工具制造者,而不是应用程序猿。若仅对设计应用程序有兴趣,对构造工具不感兴趣,可跳过本章剩余部分,稍后再回来学习。。。
5.7.1 Class类
程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息保存每个对象所属的类足迹。虚拟机利用运行时信息选择相应的方法执行。
可通过专门的Java类访问这些信息,保存信息的类被称为Class。
Class类易于让人混淆,举个例子:
Employee e;
...
Class cl = e.getClass();
getClass()方法返回一个Class类型的实例。----获得Class类对象的第一种方法
和Employee类一样,Class类中也包含了某些属性。
最常用的Class方法是getName----返回类的名字
例:
System.out.println(e.getClass().getName()+" "+e.getName());
若e是一个雇员,则输出
Employee Harry Hacker
若e是经理,则输出
Manager Harry Hacker
若类在一个包里,包的名字也作为类名的一部分:
Date d = new Date();
Class c1 = d.getClass();
String name = c1.getName(); //name="java.util.Date"
获得Class类的第二种方法
//还可以调用静态方法forName获取类名对应的Class对象
String className = “java.util.Date”;
Class c1 = Class.forName(className);
类名保存在字符串中且在运行中可改变,就可使用该方法。
注:此方法只有在className是类名或接口名时才能够执行。否则,forName方法将抛出一个checked exception(已检查异常)。使用该方法时,应提供一个异常处理器(exception handler),见本节中“捕获异常”。
获取Class类的第三种方法
若T是任意的Java类型,T.class将代表匹配的类对象。
例:
Class cl1 = Date.class; //导入了java.util.*;
Class cl2 = int.class;
Class cl3 = Double[].class;
注:一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。诸如int不是类,但int.class是一个Class类型的对象。
警告:getName()在应用于数组类型时会返回一个奇怪的名字:
Double [ ].class.getName( ) 返回“[Ljava.lang.Double; ”
int[ ].class.getName( ) 返回“[I"
虚拟机为每个类型管理一个Class对象。可用==运算符对两个类对象进行比较。例:
if(e.getClass() == Employee.class) ...
newInstance():快速创建一个类的实例—调用默认的构造器初始化新创建的对象。若该类没有默认的构造器,就会抛出一个异常。
例:
e.getClass().getInstance(); //创建了一个与e有相同类类型的实例
forName+newInstance配合使用:根据存储在字符串中的类名创建一个对象
String s = “java.util.Date”;
Object n = Class.forName(s).newInstance();
5.7.2 捕获异常
异常处理机制见第十一章。
当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序更加灵活,因为有一个“捕获”异常的处理器对异常情况进行处理。
若没有提供处理器,程序会终止,并在控制台上打印出一条信息,其中给出了异常的类型。例:偶然使用了null引用或者数组越界等。
异常:未检查异常 + 已检查异常
已检查异常—编译器将会检查是否提供了处理器
未检查异常—例如:访问null引用。编译器不会查看是否为这些错误提供了处理器,应编写代码来避免这些错误发生。但是不是所有错误都可以避免的,若竭尽全力还是发生了异常,编译器就要求提供一个处理器。Class.forName方法就是一个抛出已检查异常的例子。异常处理的策略见第十一章。
最简单的处理器
//将可能抛出已检查异常的一个或多个方法调用代码放在try块中,
//然后在catch子句中提供处理器代码。
try{
statements that might throw exceptions
}
catch(Exception){
handler action;
}
例:
try{
String name = ...;
Class cl = Class.forName(name);
...
}
catch(Exception e){
e.printStackTrace();
}
若类名不存在,则将跳过try块中的剩余代码,程序直接进入catch子句(这里,Throwable类的printStackTrace方法打印出栈的轨迹。Throwable是Exception类的超类)。若try块中为抛出任何异常,则跳过catch自居的处理器代码。
对于已检查异常,只需要提供一个异常处理器。
若调用了一个抛出已检查异常的方法,而又没有提供处理器,编译器就会给出错误报告。
5.7.3 利用反射分析类的能力
反射机制最重要的内容-------检查类的结构
java.lang.reflect包中有Field、Method和Constructor分别描述类的域、方法和构造器。
这三个类中都有一个getName方法----返回项目的名称
都有一个getModifiers方法----返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用状况。在Modifier类中。Modifier类中的isPublic、isPrivate或isFind判断方法或构造器是否是public、private或final。
Field类有一个getType方法----返回描述域所属类型的Class对象
Method和Constructor类有能够报告参数类型的方法
Method类还有一报告返回类型的方法
Class类中的getFields、getMethods和getConstructor方法分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。
Class类的getDeclareFields、getDeclareMethods和getDeclareConstructors方法分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
要应用到程序上!!!实践。。。
5.7.4 在运行时使用反射分析对象
Field类中的get方法:查看对象域。若f是一个Field类型的对象(例:通过getDeclareFields得到的对象),obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值
Employee harry = new Employee("Harry Hacker",35000,10,1,1989);
Class cl = harry.getClass(); //Employee
Field f = cl.getDeclareField("name"); //返回cl对象的name域
Object v = f.get(harry); //返回cl对象的name域的值 “Harry Hacker”
该段代码中存在一个问题。由于name是一个私有域,所以只有用get方法才能得到所访问的域的值;否则会抛出IllegalAccessException。
除非有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。
反射机制的默认行为受限于Java的访问控制。在一个Java程序没有安全管理器的控制,就可以覆盖访问控制。为达到该目的,需调用Field、Method或Constructor对象的setAccessible方法。
例:
f.setAccessible(true);
setAccessible方法是AccessibleObject类中的一个方法—是Field、Method和Constructor类的公共超类。为调试、持久存储和相似机制提供。
get方法:在查看String类型的name域时,没有任何问题;但是当想查看double类型的salary域时,因为Java中数值类型不是对象,可用Field类中的getDouble方法,也可调用get方法。反射机制会自动将这个域值打包到相应的对象包装器中,此处打包为Double。
f.set(obj,value)-----将obj对象的f域设置为新值