Java核心技术--第五章 继承

类、超类和子类

经理类与普通雇员类有很多相同之处,但还有一些差别。

经理在完成本职任务不仅可以获得工资,还获得奖金。而普通雇员只能获取工资。故而,可以重用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 动态绑定

调用对象方法的过程:

  1. 编译器查看对象的声明类型和方法名。获取所有可能被调用的候选方法
    可能存在方法f(int),f(String)。编译器将会一一列举所有该类中名为f的方法和其超类中访问属性为public且名为f的方法。
  2. 编译器将查看调用方法时提供的参数类型。获取需调用的方法名字和参数类型
    在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择该方法—该过程为重载解析。
  3. 若是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定。
  4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与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()的解析过程:

  1. 先虚拟机提取e的实际类型的方法表。可能Employee、Manager、或Employee类的其他子类的方法表
  2. 再虚拟机搜索定义getSalary签名的类。此时,虚拟机已知道应调用哪个方法
  3. 虚拟机调用方法

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();   //抽象方法
	......
}

扩展抽象类的两个方法:

  1. 在子类中定义部分抽象方法抽象方法不定义,则必须将子类也标记为抽象类
  2. 定义全部的抽象方法,子类就不用定义抽象类了

就算类不含抽象方法,也可声明为抽象类。
抽象类不能实例化------若将一个类声明为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的特性:

  1. 自反性:
    对任意非空引用x,x.equals(x)应返回true
  2. 对称性
    对任意引用x、y,当且仅当y.equals(x),x.equal(y)都应返回true
  3. 传递性
    对任意x、y、z,若x.equals(y)返回true,y.equals(z)、x.equals(z)也返回true
  4. 一致性
    若x、y引用的对象没有发生变化,则反复调用x.equals(y)返回的结果一致。
  5. 对任意非空引用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域设置为新值

猜你喜欢

转载自blog.csdn.net/weixin_43137176/article/details/82984953