-
使用extends表示继承
public class manager extends Employee { adds methods and fields }
-
在Java中,所有的继承都是公共继承。子类会自动继承父类的方法和字段。
-
关键字extends表明正在构造的新类派生于一个已存在的类。这个已存在的类被称为超类(superclass)、基类(base class)或父类(parent class);新类被称为子类(subclass)、派生类(derived class)或孩子类(child class)。
-
通过扩展超类来定义子类的时候,只需指出子类与超类的不同之处。因此在设计类的时候,应该将最一般的方法放在超类中,而将更特殊的方法放在子类中,这种将通用功能抽取到超类的做法在面向对象程序设计中十分普遍。
-
超类中的有些方法对子类并不一定适用,需要提供一个新的方法来覆盖(override)超类中的这个方法。
public class Manager extends Employee { ... public double getSalary { ... } ... } public double getSalary { return super.getSalary()+bonus; }
-
超类的方法和字段只有超类才能直接访问。希望调用超类中的方法而不是调用当前类的这个方法时,可以使用关键字super来解决。
super.getSalary()
-
super不是一个对象的引用,和this并不一样。它只是一个指示编译器调用超类方法的特殊关键字。
-
在子类中可以增加字段、增加方法或覆盖超类的方法,不过绝对不会删除任何字段或方法。
-
public manager(String name,double salary,int year,int month,int day) { super(name,salary,year,monthmday); bonus=0; }
关键字super可以的调用构造器,使用super调用构造器的语句必须是子类构造器的第一条语句。
-
如果子类的构造器没有显式地调用超类的构造器,将自动调用超类的无参数构造器。如果超类没有无参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,Java编译器就会报告一个错误。
-
关键字this有两个含义:一是指示隐式参数的引用,二是调用该类的其它构造器。类似地,super关键字也有两个含义:一是调用超类的方法,二是调用超类的构造器。
-
调用构造器的语句只能作为另一个构造器的第一条语句出现。构造器参数可以传递给当前类(this)的另一个构造器,也可以传递给超类(super)构造器。
```java
Manager boss=new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);
var staff=new Emplyee[3];
staff[0]=boss;
staff[1]=new Employee("Harry Hacker",5000,1989,10,1);
staff[2]=new Employee("Tony Tester",40000,1990,3,15);
for(Employee e:staff)
{
System.out.println(e.getName()+" "+e.getSalary());
}
```
e.getSalary()能够选出应该执行的正确getSalary方法。尽管这里将e声明为Employee类型,但实际上e即可以引出Employee类型的对象,也可以引用Manager类型的对象。
当e引用Employee对象使,e.getSalary()调用的是Employee类中的getSalary方法;当e引用Manager对象时,e.getSalary()调用的是Manager类中的getSalary方法。虚拟机知道e的实际引用对象类型,因此能够正确地调用相应的方法。
-
一个对象变量可以指示多种实际类型的现象称为多态(polymorphism)。在运行时能够自动地选择适当的方法称为动态绑定(dynamic binding)。
-
继承并不仅限于一个层次。由一个公共超类派生出来的所有类的集合称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链(inheritancde chain)。
![](C:\Users\lenovo\Desktop\学习笔记\java 核心技术卷 1\图片\5_1.jpg)
-
有一个简单规则可以用来判断是否应该将数据设计为继承关系,这就是"is-a"规则,他指出子类的每个对象也是超类的对象。反之,则不然。"is-a"规则的另一种表述是替换原则(substitution principle)。他指出程序中出现超类对象的任何地方都可以使用子类对象替换。在Java程序设计语言中,对象变量是多态(polymorphic)。一个Employee类型的变量既可以引用一个Employee类型的对象,也可以引用Employe类的任何一个子类的对象。
-
Manager boss=new Manager(...); Employee[] staff=new Employee[3]; staff[0]=boss;
变量staff[0]与boss引用同一个对象。但编译器只将staff[0]看成是一个Employee对象。
boss.setBonus(5000)//ok staff[0].setBonus(5000)//error staff[0]声明的类型是Employee,setbonus是Manager类的方法
不能将超类的引用赋给子变量。
Manager m=staff[i];//error 多态的调用是从下而上的,也就是所有的子类对象都是超类对象,符合 “is-a"。从上而下是错误的。
-
方法调用的详细过程为(以x.f(args)为例,隐式参数x声明为类C的一个对象):
-
编译器查看对象的声明类型和方法(不考虑参数,也就是说可能存在多个名字为f但参数类型不一样的方法)。
-
接下来,编译器要确定方法调用中提供的参数类型。如果在所有名为f的方法中存在一个与所提供的参数类型完全匹配的方法,就选择这个方法。这个过程称为重载解析(overloading resolution)。
-
如果是private方法,static方法,final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法。这称为静态绑定(static binding)。
-
程序运行并且采用动态绑定调用方法时,虚拟机必须调用与X所引用对象的实际类型对应的那个方法、如果是超类的方法,会在超类中寻找。
每次调用方法都要搜索花销大。多以虚拟机预先为每个类计算了一个方法表(method table),其中列出了所有方法的签名和调用的实际方法。这样,在真正调用方法时,虚拟机仅查找这个表就行了。
-
-
方法的名字和参数列表称为方法的签名。返回类型不是签名的一部分。不过在覆盖时,允许子类将覆盖方法的返回类型改为原返回类型的子类型。
public Employee getBuddy { ... } //覆盖 public Manager getBuddy//ok to change return type { } //这两个getBUddy方法有可协变的返回类型
-
Employee类方法表
Employee:
getName()->Employee.getName()
getSalary()->Employee.getSalary()
getHireDay()->Employee.getHireDay()
raiseSalary(double)->Employee.raiseSalary(double)
实际上上面列出的方法并不完善。Employee类有一个超类Object。Employee类从Object类继承了大量方法。此处省略。
Manager类方法表
Manager:
getName()->Employee.getName()
getSalary()->Manager.getSalary()
getHireDay()->Employee.getHireDay()
raiseSalary(double)->Employee.raiseSalary(double)
setBonus(double)->Manager.setBonus(double)
-
在运行e.getSalary()解析过程为:
- 首先,虚拟机获取e的实际类型的放啊表。
- 接下来,虚拟机查找定义了getSalary()签名的类
- 最后,虚拟机调用这个方法。
-
动态绑定有一个非常重要的特性:无需对现有的代码进行修改就可以对程序进行扩展。
-
在覆盖一个方法时,子类方法不能低于超类方法的可见性。特别是,超类方法时public,子类方法也必须声明为public。
-
不允许扩展的类被称为final类。
弱国希望阻止派生Executive类的子类
public final class Executive extends Manager { ... }
类中的某个特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地称为final方法)。
public class Employee { ... public final String getName() { ... } ... }
-
字段也可以被声明为final。对于final字段来说,构造对象之后就不允许改变他们的值了。不过,如果一个类声明为final,只有其中的方法自动地称为final,而不包括字段。
-
将方法或类声明为final的主要原因是:确保他们不会在子类中改变语义。
-
如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程被称为内联(inlining)。
-
要完成对象引用的强制类型转换,转换语法与数值表达式的强制类型转换类似,进需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前。
Manager boss=(Manager) staff[0];
-
进行强制类型转换的唯一原因是:要在暂时忽视对象的实际类型之后使用对象的全部功能。例如,由于某些元素是普通员工,staff数组被声明为Employee对象的数组。对于个别元素需要复原成Manager类才能访问Manager类中新增加的字段或方法。
-
将子类的引用赋给超类变量编译器是允许的。但是,将超类的引用赋给子类变量时必须使用强制类型转换。
-
如果试图在继承链上进行向下的强制类型转换,并且“谎报”对象包含的内容(强制类型转换从上至下要求原来这个元素本身就是下的内容),会发生错误。java会产生一个ClassCastException异常。
-
在进行强制类型转换之前,先查看是否能够成功转换。只需使用instanceof操作符就可以实现。instanceof判断前面的对象是否属于后面的类,或者属于其子类;如果是,返回 true,不是返回 false;
if (staff[1] instanceof Manager) { boss=(Manager) staff[1]; ... }
-
综上所述,只能在继承层次内进行强制类型转换;再将超类转换成子类之前,应该使用instanceof操作符进行检查。
-
一般情况下,最好尽量少用强制类型转换和instanceof操作符。
-
如果自下而上在类的继承层次结构中上移,位于上层的类更具有一般性,可能更加抽象。以Student和Employee类为例,可以抽象出Person类。当要增加一个getDescrition方法来返回对一个人的简要描述。此时,在Person类中实现可以返回一个空的字符串,但是,也可以使用abstract关键字让该类不需要实现该方法。
public abstract class Person { ... public abstract String getDescription(); }
为了提供好程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象类。
除了抽象方法之外,抽象类还可以包含字段和具体方法。
-
抽象方法·充当着占位方法的角色,他们在子类中具体实现。扩展抽象类可以有两种选择。一种是在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样就必须将子类也标记为抽象类;另一种是定义全部方法,这样子类就不是抽象的了。
-
在类中不含有抽象方法时,可以将这个类声明为抽象类也不可以不声明为。
-
抽象类不能实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象,但可以创建这个具体子类的对象。可以定义一个抽象类的对象变量,但是这样与一个变量只能引用非抽象子类对象。例如,Person为抽象类,Studnet为其子类。
Person p=new Student(...);
这里p是一个抽象类型Person的变量,他引用了一个非抽象子类Student的实例。
-
限制超类中的某个方法只允许子类访问或者希望允许子类的方法访问超类的某个字段可以将这些类方法或字段声明为受保护(protected)。
-
在Java中,保护字段能由同一个包中的类访问。有了这个限制,就能避免滥用保护机制,不能通过派生子类来访问受保护字段。
-
访问控制修饰符小结:
- 仅对本类可见-private
- 对外部完全可见-public
- 对本包和所有子类可见-protected
- 对本包可见-默认,无修饰符
-
Object类时Java中所有类的始祖,在Java中每个类都扩展了Object。但是并不需要写成public class Employee extends Object。如果没有明确地指出超类,Object会被认为是这个类的超类。
-
可以使用Object类型的变量引用任何类型的对象
Object obj=new Employee("Harry Hacker",35000);
Object类型的变量只能用于作为各种值得一个泛型容器。要想对其中的内容进行具体的操作,需要清楚对象的类型,还要搞清楚 对象的原始类型,并进行相应的强制类型转换。
Employee e=(Employee) obj;
-
在Java中,只有基本类型(primitive type)不是对象。
-
所有数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
-
Object类中equals方法用于检测一个对象是否等于另外一个对象。Object类实现的equals方法将确定两个对象引用是否相等。
-
getClass返回一个对象所属的类。
-
Objects.equals(a,b)如果两个参数都为null返回true;如果一个参数为null则返回false;否则,如果两个参数都不为null,则调用a.equals(b)。
-
在子类中定义equals方法时,首先调用超类得equals。如果检测失败,对象就不可能相等。如果超类中的字段都相等,就需要比较子类中的实例字段。
public class Manager extends Employee { ... public boolean equals(Object otherObject) { if(!super.equals(otherobject)) return false;//首先先判断父类equals方法 //如果成功,先将类型强制转换为相同类型 Manager other=(Manager) otherObject; return bonus==other.bonus; } }
-
Java语言规范要求equals方法具有下面特性:
- 自反性:对于任何非空引用,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方法判断返回false。
如果用
if(!(OtherObject instanceof Employee)) return false;//意思为如果显式参数和显式参数一样或者是其子类即成功
会违反对称性。
所以,如果子类有自己的相等性概念,则对称性需求将强制使用getClass检测。(此时因为子类的相等有自己的定义,所以两个对象如果要相等必须按照子类的定义来也就是类必须一样。)
如果由超类决定相等性概念,那么就可以使用instanceof检测,这样可以在不同子类的对象之间进行相等性比较。(打个比方,假设有男人和女人两个子类继承于人这个超类,如果是以身高这个超类字段来确认两个对象是否相等,此时不需要两个类类型一样,只要两个类是人这个超类的子类并且身高相等就可以确认两个对象相等。)
-
对于数组类型的字段,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。
-
使用@override标记要覆盖超类方法的那些子类方法。当出现了错误,并且正在定义一个新方法,也就是标记了之后并没有在覆盖超类方法,编译器就会报告这个错误。
-
散列码(hash code)是由对象导出的一个整型值。
-
String类的散列码是由内容导出的。
//String类散列码的算法 int hash=0; for(int i=0;i<length();i++) { hash=31*hash+charAt(i); }
也就是说如果两个String对象的内容相同,散列码就一样。
hashCode方法定义在Object类中。Object类散列码会从对象的存储地址得出,也就是说两个不同的对象其散列码基本不会相同。
-
当我们对一个类重新定义equals方法时,就必须重新定义hashCode方法。、
重写hashCode的思路:
首先,两个类相同其实例字段肯定相同。根据实例字段来设置。要合理地组合实例字段的散列码,以便能够让不同对象产生的散列码分布更加均匀。
public class Employee { public int hashCode { return 7*name.hashCode()+11*new Double(salary).hashCode()+13*hireDay.hashCode(); } }
改进方法:最好使用null安全的Objects.hashCode。如果其参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。另外使用静态方法Double.hashCode来避免创建Double对象。
public class Employee { public int hashCode { return 7*Objects.hashCode(name)+11*Double.hashCode(salary)+13*Objects.hashCode(hireDay); } }
更好的做法:需要组合多个散列值时,可以调用Objects.hash并提供所有这些参数。这个方法会对各个参数调用Objects.hashCode,并组合这些散列值。
public class Employee { public int hashCode { return Objects.hash(name,salary,hireDay); } }
由此例也可以看出,改写一个自定义类的hashCode方法可以通过Objects.hash(其实例字段)。
-
equals与hashCode的定义必须相容:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()返回相同的值。
-
如果存在数组类型的字段,可以使用静态的Array.hashCode方法来计算散列码,该散列码有数组元素的散列码组成。
-
Objects toString方法会返回表示对象值的一个字符串。绝大多数(但不是全部)的toString方法都遵循这样一个格式:类的名字,随后是一对方括号括起来的字段值。
//Point类的toString方法 java.awt.Point[x=10.y=20]
-
最好通过调用getClass().getName()获得雷鸣的字符串而不要将类名硬编码写到toString方法中。
//以Employee类为例 public String toString() { return getClass().getName()+"[name"+name+",salary"+salary+",hireDay"+hireDay+"]"; }
对于子类Manager
public class Manager extends Employee { ... public String toString() { return super.toString()+"[bonus="+bonus+"]"; } }
-
如果x是一个任意对象,并调用System.out.println(x); println方法就会简单地调用x.toString(),并打印输出得到的字符串。
-
Object类1定义了toString方法,可以打印对象的类名和散列码。
-
数组继承了object类的toString方法。
int[] luckyNumbers={2,3,5,7,11,13}; String s=""+luckyNumbers;
会生成字符串"[I@1a46e30"(前缀[I表明是一个整型数组)。正常使用为调用Arrays.toString(luckyNumbers),打印出[2,3,5,7,11,13]。打印多维数组需调用Arrays.deepToString。
-
强烈建议为自定义的每一个类添加toString方法。
-
ArrayList类似于数组,但在添加或删除元素时,它能够自动地调整列表保存的元素对象的类型。
-
ArrayList是一个有类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象的类型,需要用一堆尖括号将类名括起来追加到ArrayList后面。例如ArrayList
。 -
声明或构造一个保存Employee对象的数组列表
ArrayList
staff=new ArrayList ();
在java10中,最好使用Var关键字以避免重复写类名
var staff=new ArrayList();
如果没有使用var关键字,可以省去右边的类型参数:ArrayList
staff=new ArrayList<>(); 这称为菱形语法,因为空间括号就像是一个菱形。可以结合new操作符使用菱形语法。如果赋值给一个变量,或传递给某个方法,或者从某个方法返回,编译器会检查这个类型、参数或方法的泛型类型,然后将这个类型放在<>中。在这个例子中,new ArrayList<>()将赋值给一个类型为ArrayList
变量,所以泛型类型为Employee。 -
如果使用var声明ArrayList,就不要使用菱形语法,也就是不要省略右边的类型参数。
-
使用add方法可以将元素添加到数组列表中。staff.add(...)
-
数组列表管理着一个内部的对象引用数组。如果调用add而内部数组已经满了,数组列表就会自动的创建一个更大的数组,并将所有对象从较小的数组拷贝到较大的数组中。
-
如果已经知道或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法:staff.ensureCapacity(100);
这个方法调用将分配一个包含100个对象的内部数组。这样,前100add调用不会带来开销很大的分配空间。另外,还可以把初始容量传递给ArrayList构造器:ArrayList
staff=new ArrayList<>(100); -
new ArrayList<>(100)和new Employee[100]有所不同,数组分配100个是有100个空位置可以使用。而数组列表只是可能保存100个元素,在最初不包含任何元素。
-
size方法将返回数组列表中包含的实际元素个数。例如,staff.size();将返回staff数组列表当前的元素个数,等价于数组a.length()。
-
一旦确定数组列表的大小将保持恒定,不再发生变化,就可以调用trimToSize方法。这个方法将存储块的大小调整为保存当前元素数量所需要的存储空间,垃圾回收器将回收多余的存储空间。
一旦削减了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该再确定不会再向数组列表中添加任何元素时再调用trimToSize。
-
不能使用[]语法访问或改变数组列表的元素,而要使用get和set方法。
例如要设置第i个元素,staff.set(i,harry);它等价于对数组a的元素赋值a[i]=harry;
要使用add方法为数组添加新元素,而不是set方法,set方法只是用来替换数组中已经加入的元素。
要得到一个数组列表的元素,Employee e=staff.get(i); 这等价于Employee e=a[i];
-
原始的ArrayList类提供的get方法只能返回Object。需要对返回值进行强制转换。并且其add和set方法接受任意类型的对象,存在一定的危险性。
-
即可以灵活扩展数组并由可以方法访问数组的方法
var list=new ArrayList<X>(); while() { x=...; list.add(x); } var a=new X[list.size()]; list.toArray(a);
-
staff.add(n,e)。可以在add方法提供一个索引参数,位置n及之后的所有元素都向前移动一个位置,为新元素留出空间。插入元素。若插入后,大小超出了容量,数组列表会重新分配它的存储数组。
-
Employee e=staff.remove(n)。位于n这个位置之后的所有元素都向前移动一个位置。删除元素。并且数组的大小减1.
-
数组列表插入和删除元素的效率很低。如果元素数=较多,这类行为很频繁,可以考虑使用链表。
-
与遗留的原始ArrayList类交互操作
public class EmployeeDB { public void update(ArrayList list){} public ArrayList find(Stirng query){} }
可以将一个类型化的数组列表传递给update方法,而并不需要进行任何强制类型转换。
ArrayList
staff=...; employeeDB.update(staff);
可以将staff对象直接传递给update方法。
相反,将一个原始ArrayList赋给一个类型化ArrayList会得到一个警告,使用强制类型转换也仍然会。
-
在与遗留的代码进行交互时,要研究编译器的警告,确保这些警告不太严重就行了。
一旦确保问题不太严重,可以用@SuppressWarnings("unchecked")注解来标记接受强制类型转换的变量。
@SuppressWarnings("unchecked") ArrayList<Employee> result=(ArrayList<Employee) employeeDB.find(query);
-
所有的基本类型都有一个与之对应的类。通常,这些类称为包装器(wrapper)。这些包装器名字为:Integer、Long、Float、Double、Short、Byte、Character和Boolean(前六个类派生于公共的超类Number)。包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时包装器类还是final,因此不能派生它们的子类。
-
尖括号中的类型不允许是基本类型。但可以声明一个包装了包装器的数组列表。
var list=new ArrayList<Integer>();
list.add(3)将自动地变化为list.add(Integer.valueOf(3));
这种变换称为自动装箱(autoboxing)。相反地,当将一个Integer对象赋给一个Int值时,将会自动拆箱。
int n=list.get(i);会转换成int n=list.get(i).intValue();
自动装箱和拆箱也适用于算术表达式。
-
由于包装器类引用可以为null,所以自动装箱有可能会抛出NullPointerException异常
Integer n=null; System.out.println(n*2);
-
在一个条件表达式中混合使用Integr和Double类型,Integer值就会拆箱提升为Double,再装箱位Double。
-
装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码时会插入必要的方法调用。虚拟机只是执行这些字节码。
-
使用数值包装器可以将某些基本方法放在包装器内,例如parseInt。
-
包含在包装器中地内容不会改变,不饿能使用这些包装器类创建会修改数值参数地方法。如果确实想写一个修改数值参数值的方法,可以使用org.omg.CORBA包中定义的某个持有者(holder)类型。
-
可以提供参数数量可变的方法(有时这些方法被称为“变参“(varargs)方法),例如printf方法。
-
printf方法是这样定义的
public class PrintStream { public PrintStream printf(String fmt,Object...args){ return format(fmt,args); } }
这里的省略号...是Java代码的一部分,它表明这个方法可以接受任何数量的对象(除fmt参数之外)。
实际上,printf方法接受两个参数,一个是格式字符串,另一个是Object[]数组,其中存在所有其他参数(如果调用者提供的是整数或者其他基本类型的值,会把它们自动装箱对象)。
-
允许将数组作为最后一个参数传递给有可变参数的方法。例如
System.out.printf("%d %s",new Object[]{new Integer(1),"widgets"});
因此如果一个已有方法的最后一个参数是数组,可以把它重新定义为可变参数的方法,而不会破坏任何已有的代码。例如
public static void main(String...args)
-
public enum size{SAMLL,MEDIUM,LARGE,EXTRA_LARGE}
这个声明定义的类型是一个类,它刚好有4个实例,不可能构造新的对象。
因此,在比较两个枚举类型的值时,并不需要调用equals,直接使用"=="就可以。
-
可以为枚举类型增加构造器、方法和字段。
public enum Size { SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL"); private String abbreviation; private Size(String abbreviation){this.abbrevation=abbreviation;} public String getAbbreviation(){return abbreviation;} }
枚举的构造器总是私有的,可以省略private修饰符。如果声明一个构造器为public或protected时,会出现语法错误。
所有的枚举类型都是Enum类的子类。他们继承了这个类的许多方法。其中最有用的是toString,这个方法会返回枚举常量名。例如Size.SMALL.toString()将返回字符串"SMALL".
toString的逆方法是静态方法valueOf。例如
Size s=Enum.valueOf(Size.class,"SMALL");
每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。例如,Size[] values=Size.values();返回包含元素Size.SMALL、Size.MEDIUM、Size.LARGE、SIZE.EXTRA_LARGE。的数组。
ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。例如,Size.MEDIUM.ordinal()返回1.
Enum类有一个类型参数,可省略。
-
反射库(reflection library)提供了一个丰富且精巧的工具集,可以用来编写能够动态操纵Java代码的程序。使用反射,Java可以支持用户界面生成器、对象关系映射其以及其他需要动态查询类能力的开发工具。
-
能够分析类能力的程序称为反射(reflective)。
-
反射机制可以用来:在运行时分析类的能力;在运行时检查对象,例如百年写一个适用于所有类的toString方法;实现泛型数组操作代码;利用Method对象。
-
在程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。
可以用特殊的类访问信息。保存这些信息的类名为Class。Object类中的getClass方法将会返回一个class类型的实例。
Employee e; ... Class cl=e.getClass();
Class对象会描述一个特定类的属性。Class对象中的getName方法将返回类的名字。
如果类在一个包里,包的名字也作为类的名字的一部分。
还可以使用静态方法forName获得类名对应的Class对象。
String className="java.util.Random"; Class cl=Class.forName(className);
如果类名保存在一个字符串中,这个字符串会在运行时发生变化,就可以用forName方法。如果className是一个类名或接口名,这个方法可以执行。否则,forName方法将抛出一个检查性异常(checked exception),无论何时使用这个方法,都应该提供一个异常处理器(exception handler)。
-
如果T是任意的Java类型(或void 关键字),T.class将代表匹配的类的对象,例如
Class cl1=Random.class;//if you import java.util.* Class cl2=int.class; Class cl3=Double[].class;
一个Class对象实际上表示的是一个类型,这可能是类,也可能不是类。如int不是类,int.class是一个class类型的对象。
Class类实际上是一个泛型类。
-
虚拟机为每个类型管理一个唯一的Class对象。因此,可以利用==运算符实现两个类对象的比较。
-
如果有一个Class类型的对象,可以用它构造类的实例。利用newConstructor方法将得到一个Constructor类型的对象,然后用newInstance方法来构造一个实例
var className="java.utill.Random"; Class cl=Class.forName(className); Object obj=cl.getConstructor().newInstance();
如果这个类没有无参数的构造器,getConstructor方法会抛出一个异常。
-
异常有两种类型:非检查型(unchecked)异常和检查性(checked)异常。对于检查性异常,编译器将会检查是否知道这个异常并做好准备来处理后果。很多异常如越界错误或者访问null引用,都属于非检查型异常。
-
如果一个方法包含一条检查型异常的语句,则在方法名上增加一个throws子句。
public static void doSomethingWithClass(String name) thorws ReflectiveException { Class cl=Class.forName(name);//might throw exception do somethins with cl }
调用这个方法的任何方法也都需要一个thorws声明。这也包括main方法。如果一个异常确实出现,main方法将终止并提供一个堆栈轨迹。
-
类通常有一些关联的数据文件,例如图像和声音文件,包含消息字符串和按钮标签的文本文件。在Java中,这些关联的文件被称为资源(resource)。
-
Class类提供了一个很有用的服务可以查找资源文件。步骤如下
- 获得拥有资源的类的Class对象
- 有些方法,如ImageIcon类的getImage方法,接受描述资源位置的URL。则要调用URL url=cl.getResource("about.gif");
- 否则,使用getResourceAsStream方法得到一个输入流来读取文件中的数据。
-
文件的自动装载是利用资源加载特性完成的。没有标准的方法来解释资源文件的内容。每个程序必须有自己的方法来解释它的资源文件。
-
在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的字段、方法和构造器。这三个类都有一个叫做getMethod的方法,用来返回字段、方法或构造器的名称。Field类有一个getType方法,用来返回描述字段类型的一个对象,这个对象的类型同样是Class。Method和Contsructor类有报告参数类型的方法,Method类还有一个报告返回类型的方法。这三个类都有一个名为getModifiers的方法,它将返回一个整数,用不同的0/1位描述所使用的修饰符,如Public和static。另外,还可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的这个整数。例如,可以使用Modifier类中的isPublic、isPrivate或isFinal判断方法或构造器是public|private还是final。
还可以利用Modifier.toString方法将修饰符打印出来。
-
Class类中的getFields、getMethods和getcConstructors方法将分别返回这个类支持的公共字段、方法和构造器的数组,其中包括超类的公共成员。Class类的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部字段、方法和构造器的数组,其中包括私有成员、包成员和受保护成员,但不包括超类的成员。
-
通过获得对应的Class对象并在这个对象上调用getDeclaredFields查看任意对象数据字段的名字和类型。
-
利用反色和机制可以查看在编译时还不知道的对象字段。如果f是一个Field类型的对象,obj是某个包含f字段的类的对象。f.get(obj)将返回一个对象,其值为obj的当前字段值。
var harry=new Employee("Harry Hacker",50000,10,1,1989); Class cl=harry.getClass(); Field f=cl.getDeclaredField("name"); Object v=f.get(harry);
不仅可以获得值,也可以设置值。调用f.set(obj,value)将把对象obj的f表示的字段设置为新值。
该代码存在一个问题。由于name是一个私有字段,所以get和set方法会抛出一个IllgalAccessException。只能对可以访问的字段使用get和set方法。Java安全机制允许查看一个对象有哪些字段,但是除非拥有访问权限,否则不允许读写哪些字段的值。
-
反射机制的默认行为受限于Java的访问控制。不过可以调用Field、Method、Constructor对象的setAccessible方法覆盖Java的访问控制。例如
f.setAccessible(true);
setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method、Constructor的公共超类。这个特性是为调试、持久存储和类似机制提供的。如果不允许访问,setAccessible调用会抛出一个异常。
-
可变句柄。
-
使用反射编写范型数组代码。
-
Method类有一个invoke方法,允许调用包装在当前Method对象中的方法。invoke方法的签名是:
Object invoke(Object obj,Object...args)
第一个参数是隐式参数,其余的对象提供了显示参数。
对于静态方法,第一个参数可以忽略,即可以将它设置为null。
如果返回类型是基本类型,invoke方法会返回其包装器类型。
-
调用getDeclaredMethods方法和Class类的getMethod方法可以得到Method对象。要准确得到方法还必须提供方法参数类型。getMethod的签名是
Method getMethod(String name,Class... parameterTypes)
-
继承的设计技巧:
- 将公共操作和字段放在超类中
- 不要使用受保护的字段
- 使用继承实现“is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承。
- 在覆盖方法时,不要改变预期的行为。
- 使用多态,而不要使用类型信息。
- 不要滥用反射。