Java核心技术卷一 3. java继承多态和反射
Posted lovezyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java核心技术卷一 3. java继承多态和反射相关的知识,希望对你有一定的参考价值。
类、超类和子类
雇员用 Employee 类表示,但是经理是雇员但他有独自的域和方法,所以我们可以添加一个新类 Manager。我们在新类中增加新功能,用继承的方法将 Employee 类中的域和方法保留下来。每个经理都是一个雇员,is-a
关系是继承的一个明显特征。
定义子类
关键字extends
表示继承:
class Manager extends Employee{
...
}
关键字 extends 表明正在构造的新派生类生于一个已存在的类。已存在的类成为超类、基类或父类;新类成为子类、派生类或孩子类。子类继承了父类的域和方法,子类比超类拥有的功能更加丰富。
然而子类中定义的域和方法,超类不可以使用它们。
因此我们在设计类的时候,因该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。
覆盖方法
重写:若超类有些方法不适应子类,子类可以提供一个新的方法来覆盖超类中的这个方法。在覆盖超类方法时,子类方法不能低于超类方法的可见性。
调用重写父类后的方法:如果该方法用到了超类的被重写的方法,此时可以在子类的新方法中调用超类的方法(通过关键字 super.父类方法()来调用超类方法),在进行后面的更改实现自己的需求。
调用父类的私有域:子类不能直接使用父类的私有域,只能通过公有的接口来调用父类的私有域。
class Manager extends Employee{
//不能访问超类的私有域baseSalary
public double getSalary(){
return baseSalary + bonus;//err
}
}
class Manager extends Employee{
double = bonus;
//覆盖重写
public double getSalary(){
return super.getSalary() + bonus;
}
}
子类只能增加域、增加方法或覆盖超类的方法,不能删除继承的任何域和方法。
子类构造器
super 在子类构造器中的应用:
public Manager(String n, double s, int year, int month, int day){
super(n, s, year, month, day);
bonus = 0;
}
- 创建子类对象时,如果子类没有调用父类的构造函数,将自动调用超类默认的没有参数的构造函数。
- 使用子类调用父类的构造函数,必须在子类的构造函数的第一行使用。
- 子类构造器的参数可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。
继承层次
继承层次:由一个公共父类派生出来的所有类的集合。
继承链:在继承层次中,从某个特定的类到其祖先的路径。
java 不支持多继承,但有多继承的方式(接口)。
多态
is-a
规则表明,子类的每个对象也是超类的对象。
置换法则,表明程序中出现父类对象的任何地方都可以用子类对象置换。
Employee e;
e = new Employee();
e = new Manager();
所有在 java 程序设计语言中,对象变量是多态的。父类类型的对象变量可以引用一个父类的任何一个子类对象。
不能将一个超类的引用赋值给子类变量,将产生错误。不是所有的雇员都是经理。
多态的特点:
- 存在继承关系
- 子类要重写父类的方法
- 调用方法时会调用子类重写的实现,而不是父类的实现
- 调用静态方法时,调用父类的实现,静态和类相关算不上重写,对象是哪个类就调用那个类的静态方法。
- 不能使用子类的成员属性和子类特有的成员方法。
- 如果非要使用子类子类特有的成员属性和特有的成员方法就要将它强制转换为子类,但如果父类和子类有同名的域,此时只能访问的域为子类的,除非向上转型。
Manager manager = new Manager();
Employee employee = new Employee();
System.out.println(employee.getSalary());//调用了 Employee 的方法
employee = new Manager();
employee.getSalary();
System.out.println(employee.getSalary());//调用了 Manager 重写的方法
manager = (Manager) employee;//强制转换,可以成功,因为 employee 是多态,manager现在是Manager类的对象
employee = manager;//转换回来,employee 是多态
警告:java 中子类数组的引用可以转换为超类的引用,而不需要强制类型转换。
因为两个数组都指向了同一个引用,此时managers[0]
会调用一个不存在的实例。
Manager manager = new Manager();
Employee employee = manager;
employee = new Employee();//变量改变了地址
System.out.println(employee == manager);//false
Manager[] managers = new Manager[3];
Employee[] employees = managers;
employees[0] = new Employee();//数组变量没有改变地址,employees[0] 和 managers[0] 引用同一个对象
System.out.println(managers[0] == employees[0]);//上一句异常 java.lang.ArrayStoreException
Manager[] managers = new Manager[3];
Employee[] employees = managers;
try{
employees[0] = new Employee();//数组变量没有改变地址,employees[0] 和 managers[0] 引用同一个对象
} catch (Exception ignored){}
System.out.println(managers[0] == employees[0]);//true
动态绑定
调用对象方法的执行过程:
- 编译器查看对象的声明类型和方法名 f。此时声明类型会确定,但可能存在多个方法名相同的方法,如子类重名的方法和父类可访问的重名的方法。编译器会获取所有可能被调用的候选方法。
- 查看调用方法时提供的参数类型。选择方法中提供的参数类型与方法 f 中提供的参数类型完全匹配的方法(重载解析)。编译器获取需要调用的方法名字和参数类型。
- 如果是private方法、static方法、final方法或者构造器,则编译器可以准确的知道调用那个方法。这种调用方式称为静态绑定。别的方法采用动态绑定的方式。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机会调用对象实际类型最合适的那个类的方法,如果实际类型没有这个方法,则去超类中寻找,以此类推。
阻止被继承和重写:final 类和方法
当类使用 final 时,就表示这个类不能扩展:
final class Executive extends Manager{
......
}
类的方法声明为 final ,它的子类就不能覆盖这个类的方法。(类声明为 final 时,类的方法自动成为 fiinal 方法,不包括域)
class Executive extends Manager{
public final String getName(){
return name;
}
}
目的:确保他们不会在子类中改变语义。
强制类型转换
可以将浮点型数值强制转换为整型数值,也可以将某个类的对象引用转换成另外一个类的对象引用。
double x = 3.14;
int nx = (int) x;
Manager boss = (Manager) staff[0];
原因:在暂时忽视对象的实际类型之后,使用对象的全部功能。
将一个值存入变量时,编译器将检查是否允许该操作。
- 将一个子类的引用赋给一个超类变量,编译器是允许的。
- 将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能通过检察。
如果在继承链上进行向下的类型转换,并且“谎报”有关对象包含的内容,运行程序时将发生异常:
Manager manager = new Manager();
Employee employee = new Employee();
manager = (Manager) employee;//Exception java.lang.ClassCastException
Manager manager = new Manager();
Employee employee = new Manager();
manager = (Manager) employee;//正确,多态的情况下
所以强制转换之前因该使用 instanceof 判断是否能够成功转换。
if(employee instanceof Manager){
manager = (Manager) employee;
}
总结:
- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前,使用 instanceof 进行检查。
应用场景:一般情况下不需要进行类型转换,实现多态性的动态绑定机制能够自动地找到相应的方法,只有在父类对象需要调用子类引用并要使用子类的特有方法时,才需要进行类型转换。
重新设计一个超类,添加子类特有的方法,实现重写,完全可以避免使用强制类型转换。一般情况下,尽量少用类型转换和 instanceof 运算符。
抽象类
当祖先类更加通用,人们只将它作为派生其他类的基类,而不想作为使用的特定的实例类,这是就可以使用关键字 abstract 关键字,将超类定义为抽象类。
例如,一个父类有两个子类,子类有同样的方法,子类各自实现这个方法很简单,那么父类该提供什么内容呢?可以让父类这方法返回一个空字符串,更好的方法是,使用关键字 abstract ,这样就不需要实现这个方法了。
abstract class Person{
private String name;
public Person(String n){
name = n;
}
public abstract String getDescription();
public String getName(){
retruen name;
}
}
包含一个或多个抽象方法的类本身必须声明为抽象的。
抽象类不仅包含抽象方法,还可以包含具体数据和具体方法。
子类不定义父类的抽象方法,那么子类也标记为抽象类;子类定义父类全部的抽象方法,子类就不是抽象类。
类即使不含抽象方法,也可以将类声明为抽象类。目的为不让其实例化,不能创建这个类的对象。
注意的是,虽然抽象类不能创建对象。但可以定义一个抽象类的对象变量,并且只能引用非抽象子类的对象(这点和接口一样)。
java Person P = new Student(); //此时和多态的个性一样
受保护访问
子类不能访问超类的私有域,如果想要允许,可以使用 protected 修饰符取代 private 修饰符。
不过子类调用的这个受保护方法只能访问超类被调用受保护方法的方法域,不能访问超类的其他域。这种机制有助于避免滥用受保护机制,是的子类只能获得访问受保护域的权利。
控制可见性的修饰符:
- 仅对本类可见 ------ private
- 对本包可见 ------ 默认
- 对本包和所有子类可见 ------ protected
- 对所有类可见 ------ public
Object 类
- Object 类是所有类的始祖,每个类都由它扩展而来,他们是默认继承的,不需要写出来。
- 没有明确的指出超类,那么这个类的超类就是 Object 类。
- 可用类型为 Object 类型的变量引用任何类型的变量。要具体操作,可以进行类型转换。
- java 中只有基础类型不是对象,如,数值、字符和布尔类型的值都不是对象。所有的数组,不管对象数组还是基本类型数组都扩展于 Object 类。
equals 方法
作用:Object 类中的 equals 方法,用于检测一个对象是否等于另一个对象,也就是判断两个对象是否具有相同的引用。
意义不大:比较两个 PrintStream 对象是否引用相等完全没有意义。
正常需求:需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。例如,有两个雇员实例对象,如果他们的 ID、姓名 、薪水和雇用日期都一样,那么就认为他们是相等的。
按需求在一个类中定义 equals 方法:
class Employee{
...
public boolean equals(Object otherObject){
if(this == otherObject) return true;
if(otherObject == null) return false;
if(getClass() != otherObject.getClass()) return false;//getClass获取的是当前运行实例所属的类
//getClass() 、 super.getClass() 、 this.getClass() 都一样的结果,属性为 final 不可重写。
//getClass().getName() 只返回类的名字
//getClass().getSuperclass() 返回这个类的超类信息
Employee other = (Employee) otherObject;
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);//判断想个对象的域是否相同
//采用以下方法,可以防止 name 和 hireDay 为 null 时的空指针异常,Objects.equals(a, b)对 null 值进行了判断。
//return Objects.equals(name,other.name)
// && salary == other.salary
// && Objects.equals(hireDay,other.hireDay);
}
}
//源码
// Object equals()
public boolean equals(Object obj) {
return (this == obj);
}
// Objects.equals()
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
在子类中定义 equals 方法:
class Manager extends Employee{
...
public boolean equals(Object otherObject){
if(!super.equlas(otherObject)) return false;//首先与超类比较,确定和超类的域都相等。
Manager other = (Manager) otherObject;
return bonus == other.bonus;
}
}
instanceof 用法
instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例。instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例。
用法:
result = object instanceof class
参数:
Result:布尔类型。
Object:必选项。任意对象表达式。
Class:必选项。任意已定义的对象类。
- 一个类的实例包括本身的实例,以及所有直接或间接子类的实例
- instanceof左边操作元显式声明的类型与右边操作元必须是同种类或右边是左边父类的继承关系,对象和他的子类进行instanceof判断为false
- 没有关系,即不同的继承关系下,编辑出错
//Person类所在的继承树是:Object<--Person<--Student<--Postgraduate
Person p = new Student();
if (p instanceof Postgraduate) System.out.println("p是类Postgraduate的实例");//false
if (p instanceof Person) System.out.println("p是类Person的实例");//true
if (p instanceof Student) System.out.println("p是类Student的实例");//true
if (p instanceof Object) System.out.println("p是类Object的实例");//true
if(p instanceof Animal){System.out.println("p是类Animal的实例");}//编译出错
相等测试与继承
问题:如果隐式和显式的参数不属于同一个类,equals 方法将如何处理呢?
方法一:发现类不匹配,equals 方法就返回 false。
方法二:使用 instanceof 进行检测。if(!(otherObject instanceof Employee)) return false;
方法二的缺点:不但没有解决 otherObject 是子类的情况,并且还有可能招致一些麻烦。
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。(x不能为空引用,会报错)
从两个角度看问题:
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用 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;
- 对所需要的域进行比较。使用 == 比较基本类型,使用 equals 比较对象域。
对象域使用Objects.equals(a, b)
数组使用Arrays(a, b)
java return field1 == other.field1 && Objects(field2, other.field2) && ...;
- 如果在子类中重新定义 equals,在子类的 equals 中调用
super.equals(other)
错误的实现 equals 方法:
public boolean equals(Empliyee other){
...
}
//正确
public boolean equals(Object other){
...
}
方法声明的显示参数类型是 Employee 结果并没有覆盖 Object 的 equals 方法。
hashCode 方法
散列码:由对象导出的一个整形值。它没有规律,两个不同的对象,x.hashCode() 和 y.hashCode() 基本不会相同。
hashCode 方法定义在 Object 中,即每个对象都有一个默认的散列码,值为对象的存储地址。
public native int hashCode();
//是一个本地方法,它的实现是根据本地机器相关的。
众多类都重写了 hashCode 方法,如 String、Integer、Double。。。。等等 。
String 中重写的 hashCode 方法如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
这是会出现两个如下情况:
String s = "OK";
String t = new String("OK");
System.out.println(s.hashCode() + " " + t.hashCode());
//结果他们的hashCode相同
因为字符串的散列码被重写了,它是由内容导出的。
- 如果重新定义 equals 方法,就必须重新定义 hashCode 方法,以便用户可以将对象插入到散列表中。
- 如果 x.equals(y) 返回 true ,那么 x.hashCode() 与 y.hashCode() 的值就必须相同。
例如,如果用定义的 Employee.equals 比较雇员的 ID,那么 hashCode 方法就需要散列 ID,而不是雇员的姓名或存储地址。
hashCode 方法返回一个整形数值(也可以是负数),合理的组合实例域的散列码,让不同对象产生的散列码更加均匀。
class Employee{
public int hashCode(){
retrun 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
}
}
//java 7 Objects.hashCode() 它是 null 安全的,为 null 返回 0
class Employee{
public int hashCode(){
retrun 7 * Objects.hashCode(name)
+ 11 * new Double(salary).hashCode()
+ 13 * Objects.hashCode(hireDay);
}
}
//更好的获取散列值方法
class Employee{
public int hashCode(){
retrun Objects.hash(name, salary, hireDay);
}
}
查看数组散列码的方法:
Arrays.hashCode(数组);
toString 方法
用于返回对象值的字符串。这个方法在很多地方都会自动调用。
- 使用
+
连接字符串 - 打印输出到控制台
为每个类重写 toString 方法,程序员可以通过日志记录支持中受益匪浅。
public String toString(){
return getClass().getName() + "[name=" + name + ", salary=" + salary + ", hireDay=" + hireDay + "]";
}
Object 类定义了 toString 方法,用来打印输出对象所属的类和散列码:
System.out.println(System.out);
//输出:[email protected]
数组也继承了 Object 的 toString 方法,如果想要输出数组的值:
Arrays.toString(数组);
Arrays.deepToString(多维数组);
泛型数组列表
java 能在运行时确定数组的大小。
int actualSize = ...;
Employee[] staff = new Employee[actualSize];
但是并没有完全解决运行时动态更改数组的问题。一旦确定了数组的大小,改变就不太容易了。
解决这个问题可以使用ArrayList
的类,使用有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。
ArrayList 是一个采用类型参数的泛型类,为了指定数组保存的类型对象,需要用一对尖括号将类名括起来加在后面。
ArrayList<Employee> staff = new ArrayList<Employee>();
Java 7 中,可以省略后面的括号内容:
ArrayList<Employee> staff = new ArrayList<>();
也可以定义初始容量
ArrayList<Employee> staff = new ArrayList<>(100);
//跟数组不用一样,数组分配100个元素的存储空间,数组就有100个空间位置可以使用。
//而容量为100个元素的数组列表只是拥有保存100个元素的潜力,实际上重新分配空间的话,将会超过100个。
此时,泛型类型为 Employee。
java 5.0之前没有泛型类,但有一个 ArrayList 类,保存类型为 Object 元素:
ArrayList arrayList = new ArrayList();
arrayList.add(new Object());
arrayList.add(new Object());
System.out.println(arrayList);//[java.lang.Object[email protected], [email protected]]
对应的动态数组,使用的是 Vector
类,不过现在 ArrayList 类更加有效。
添加元素
使用 add 方法可以将元素添加到数组列表中,数组列表管理着对象引用的一个内部数组,当数组空间用尽,添加时,数组列表将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
staff.add(new Employee("Harry Hacker", ...);
staff.add(new Employee("Tony Tester", ...);
分配对应个数的内部数组
使用 ensureCapacity 方法,分配 n 个对象的内部数组,然后调用 n 次 add 方法,而不用重新分配空间。
staff.ensureCapacity(100);
实际元素个数
size 方法返回数组列表中包含的实际元素数目。
staff.size();
清楚空的位置
一旦确认数组列表的大小不在发生变化,就可以调用 trimToSize 方法。它将存储区域的大小调整为当前元素数量所需要的存储空间数目。
staff.trimToSize();
访问数组列表元素
改变和访问数组列表元素
虽然数组列表自动扩展容量,但是增加了访问元素语法的复杂程度。使用 set 和 get 方法实现改变和访问数组元素的操作。
staff.set(i, harry);//等价 a[i] = harry;
Employee e = staff.get(i);//等价 Employee e = a[i];
结合他们两个的优点
集合添加完毕后,可以使用toArray方法,将数组列表元素拷贝到一个数组中。
ArrayList<X> list = new ArrayList<>();
while(...){
x = ...;
list.add(x);
}
X[] a = new X[list.size()];
list.toArray(a);
优点:
- 灵活的扩展数组
- 方便的访问数组
中间插入
使用带索引的 add 方法,可以在数组列表的中间添加元素,n 后面的元素都要向后退一格。
int n = staff.size() / 2;
staff.add(n, e);
删除元素
也可以删除一个元素,删除之后所有后面的元素向前移动一格。
Employee e = staff.remove(n);
数组列表的性能缺点
对数组实施插入和删除元素的操作其效率比较低,如果对数组存储的元素比较多,又要经常在中间位置插入、删除元素,就因该考虑使用链表,但链表对数组的查询比不上数组列表,所以看实际业务场景使用数组列表或链表。
对象包装器与自动装箱
有时需要将基本类型转换为对象,这是就要用到包装器。
种类:Integer、Long、Float、Double、Short、Byte(这6个派生于超类 Number)、Character、Void和Boolean
特点:对象包装器类是不可变的,一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装类还是fianl,因此不能定义他们的子类。
ArrayList<int> list = new ArrayList<>();//错误
ArrayList<Integer> list = new ArrayList<>();//正确
自动装箱:当数组列表添加基础类型对象时,会自动打包成包装器对象。
list.add(3);//自动执行下面这句
list.add(Integer.valueOf(3));
自动拆箱:当一个Integer对象赋值给一个int值时,会自动拆箱变成基本类型。
int n = list.get(i);
int n = list.get(i).intValue();
自动拆箱自动装箱:执行算术表达式时,会先拆箱,再执行运算,最后在装箱。
Integer n = 3;
n++;
假象:假象为,基本类型与它们的对象包装是一样的,只是它们的相等性不同。==
运算符可以应用于对象包装器对象,只不过检测的是对象是否指向同一个存储区域。因此下面的值因该为false
。
Integer i = 1000;
Integer j = 1000;
System.out.println(i == j);
java 有时却可以让他为true
,这不是我们希望的,解决这个方法的办法是比较两个包装器对象时调用 equals
方法。
Integer i1 = 200;
Integer j1 = 200;
System.out.println(i1 == j1);//false
Integer i2 = 100;
Integer j2 = 100;
System.out.println(i2 == j2);//true
自动装箱规范要求boolean
、byte
、char
<= 127,介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中。
包装器为 null
运行后会抛出异常,不像 int 不能赋值为 null 。
Integer n = null;
System.out.println(2 * n);//Throws NullPointerException
Integer 和 Double
Integer 首先拆箱,提升为 double ,再装箱为 Double:
Integer n = 1;
Double x = 2.0;
System.out.println(true ? n : x);//1.0
强调
装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
//.java
public class DataType {
public static void main(String[] args){
Integer n = 10;
}
}
//.class
public class DataType {
public DataType() {
}
public static void main(String[] args) {
Integer n = Integer.valueOf(10);
}
}
利用静态方法转换类型
想要将字符串转换为整合可以使用这个方法:
int x = Integer.parseInt(s);
错误认为包装器类可以用来实现修改数值参数的方法
public class DataType {
public static void triple(Integer x){
x = x * 3;
}
public static void main(String[] args){
Integer n = 10;
triple(n);
System.out.println(n);//10
}
}
Integer 对象是不可改变的:包含在包装器中的内容不会改变。不能使用这些包装器创建修改数值参数的方法。
public class DataType {
public static void triple(String x){
x = x + "0";
}
public static void main(String[] args){
String n = "10";
triple(n);
System.out.println(n);//10
}
}
public class DataType {
public static void triple(IntHolder x){
x.value = x.value * 3;
}
public static void main(String[] args){
IntHolder n = new IntHolder(10);
triple(n);
System.out.println(n.value);//30
}
}
参数数量可变的方法
举个例子,printf 方法:
System.out.printf("%d", n);
System.out.printf("%d %s", n, "widgets");
它可以调用好几个参数,但是他们都是同一个方法。
public class PrintStream{
public PrintStream printf(String format, Object ... args) {
return format(format, args);
}
}
int n = 10;
System.out.printf("%d %s", n, "widgets");
System.out.printf("%d %s", new Object[]{n, "widgets"});//可以写成 Object 类数组
省略号...
是java代码的一部分,它表明这个方法可以接收任意数量的对象。他是 Object[]
数组,基本类型会进行自动封装。
用户也可以自定义,功能为计算若干个数值的最大值:
public static double max(double... values){
double largest = Double.MIN_VALUE;
for(double v : values){
if(v > largest){
largest = v;
}
}
return largest;
}
double x = max(1.0, 2.0, 3.0, 4.0);
System.out.println(x);//4.0,找出最大数
枚举类
public enum Size{ SMALL, MEDIUM, LARGE, EXTRA_LARGE };
声明定义的类型是一个类,它有 4 个实例,比较两个枚举类型的值时,永远不要使用equals
,而直接使用==
就可以了。
可以在枚举类中添加构造器、方法和域。所有枚举类型都是 Enum 类的子类。
public enum Size{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation){
this.abbreviation = abbreviation;
}
public String getAbbreviation(){
return abbreviation;
}
}
常用的一些方法:
System.out.println(Size.SMALL.toString());//返回枚举类常量名
Size s = Enum.valueOf(Size.class, "SMALL");//toString 的逆方法是静态方法 valueOf
Size[] values = Size.values();//静态方法返回一个包含全部枚举值的数组
Size.MEDIUM.ordinal();//返回 1 ,返回 enum 中枚举常量的位置,位置从 0 开始计数
反射
反射库提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵 Java 代码的程序。
反射功能大量的应用在 JavaBeans 中,他是 Java 组件的体系结构。
反射可在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。
能够分析类能力的程序成为反射,反射机制可以用来:
- 在运行中分析类的能力
- 在运行中查看对象,如,编写一个 toString 方法给所有类使用
- 实现通用的数组操作码
- 利用 Method 对象,很像 C++ 的函数指针
使用反射的主要人员是工具构造者,而不是应用程序员。
Class 类
程序运行期间,系统为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方式执行。
方法 getClass() 获取 Class 实例
保存这些信息的类成为 Class。Object 中个 getClass() 方法会返回一个 Class 类型的实例。
Employee e;
Class cl = e.getClass();
Class 对象表示一个特定类的属性,getName 方法返回类的名字,如果类在包里,包的名字也作为类名的一部分:
Employee e = new Employee();
System.out.println(e.getClass());//class di3.Employee 返回了 类和类名(包名也作类名一部分)
System.out.println(e.getClass().getName());//di3.Employee 返回了类名(包名也作类名一部分)
静态方法 forName() 获取 Class 实例
只需提供类路径名,调用 Class 静态方法 forName 方法可以获得类对应的 Class 对象:
String className = "java.util.Random";
try {
Class cl = Class.forName(className);
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
因为存在提供的类路径名是错误的情况,这里必须添加异常处理器 try catch 语句,不然无法编译。
class 获取 Class 实例
如果 T 是任意的 java 类型(或 void 关键字),T.class 将代表匹配的类对象:
Class cl1 = Random.class;//class java.util.Random
Class cl2 = int.class;//int
Class cl3 = void.class;//void
Class cl4 = Employee.class;//class di3.Employee
Class cl5 = Enum.class;//class java.lang.Enum
Class cl6 = float.class;//float
Class Cl7 = Float.class;//class java.lang.Float
Class 实际上是一个泛型类,调用.class
不一定是类,如int
就不是类,但int.class
是一个类。
Class cl1 = Random.class;
Class cl2 = Integer.TYPE;
Class cl3 = Void.TYPE;
Class cl4 = Employee.class;
Class cl5 = Enum.class;
Class cl6 = Float.TYPE;
Class Cl7 = Float.class;
//Integer.TYPE 代码
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
//static native Class<?> getPrimitiveClass(String name);
//返回指定的虚拟机的Class对象
//原始类型
这是编译后的代码,可见:
Interger.TYPE == int.Class
是基本类型 int 的 Class 实例,int 是基本类型在虚拟机运行时就已经加载了他的常量静态变量 TYPE,返回了 Class 实例。- Interger.class 是包装器 Interger 的 Class 实例。
比较两个类对象
虚拟机为每个类都管理一个 Class 对象,所以才可以利用 ==
比较两个类对象:
Employee e = new Employee();
if(e.getClass() == Employee.class) ...
//true
动态创建实例
newInstance 方法调用默认的构造器初始化新创建的对象。这个类没有默认构造器,就会抛出一个异常。
String s = "java.util.Random";
try {
Object o = Class.forName(s).newInstance();
} catch (InstantiationException e1) {
e1.printStackTrace();
} catch (IllegalAccessException e1) {
e1.printStackTrace();
}
捕获异常
通常通过捕获异常的处理器对异常情况进行处理。
发现异常并没有提供处理器,程序会终止,并在控制台打印异常的类型。
异常的类型:
- 未检查异常,如访问 null 引用,编译器不会查看是否提供了处理器
- 已检查异常,编译器会检查是否提供处理器
当 try 语句块发生异常时,就执行 catch 语句块。
利用反射分析类的能力
反射用到的类的概述:
java.lang.Class //用于生成类的 Class 实例
java.lang.reflect.Field //用于描述类的域
java.lang.reflect.Constructor //用于描述类的构造器
java.lang.reflect.Method //用于描述类的方法
java.lang.reflect.Modifier //主要用于提供静态分析方法,判断、打印修饰符
反射用到的类的方法:
java.lang.Class //用于生成类的 Class 实例
Field[] getField() //返回包含 Field 对象的数组,这个类或超类的公有域
Field[] getDeclaredFields() //返回包含 Field 对象的数组,这个类的公有域
Method[] getMethods() //返回包含 Method 对象的数组,返回这个类和超类的公有方法。
Method[] getDeclarMethods() //返回包含 Method 对象的数组,返回这个类或接口的全部方法,不包括在超类继承的。
Constructor[] getConstructors() //返回包含 Constructor 对象的数组,包含这个类的所有公有构造器
Constructor[] getDeclaredConstructors() //返回包含 Constructor 对象的数组,包含这个类的所有构造器
java.lang.reflect.Field //用于描述类的域
java.lang.reflect.Constructor //用于描述类的构造器
java.lang.reflect.Method //用于描述类的方法
int getModifiers() //返回一个描述构造器、方法或域的修饰符的整形数值。
String getName() //返回构造器、方法或域名的字符串。
Class[] getParameterTypes() //Constructor 和 Method 类中,返回描述参数类型的 Class 对象数组。
Class getDeclaringClass() //返回描述类中定义的构造器、方法或域的 Class 对象。
Class[] getExceptionTypes() //Constructor 和 Method 类中,返回描述方法抛出的异常类型的 Class 对象数组。
Strng toString() //打印构造器、方法或域的整段。
Class getReturnType() // Method类中,返回描述返回类型的 Class 对象。
java.lang.reflect.Modifier //主要用于提供静态分析方法,判断、打印修饰符
static String toString(int modifiers) //打印整形数值的修饰符字符串
static boolean isAbstract(int modifiers)
static boolean isFinal(int modifiers)
static boolean isInterface(int modifiers)
static boolean isNative(int modifiers)
static boolean isPrivate(int modifiers)
static boolean isProtected(int modifiers)
static boolean isPublic(int modifiers)
static boolean isStatic(int modifiers)
static boolean isStrict(int modifiers)
static boolean isSynchronized(int modifiers)
static boolean isVolatile(int modifiers) //判断是否是这个修饰符
实现一个能加载任何类的程序:
package dirfanshe;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class ReflectionTest {
public static void main(String[] args) {
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 {
//获取输入的类的 Class 类实例
Class cl = Class.forName(name);
//获取输入的类的超类 Class 类实例
Class supercl = cl.getSuperclass();
//cl.getModifiers() 返回一个整形数值描述对应的修饰符,Modifier.toString 方法返回修饰符
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() + "{
");
//打印类的域
printFields(cl);
System.out.println();
//打印类的构造器
printConstructors(cl);
System.out.println();
//打印类的方法
printMethods(cl);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.exit(0);
}
private 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 + "(");
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(");");
}
}
private static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods) {
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
Class[] paraTypes = m.getParameterTypes();
for (int j = 0; j < paraTypes.length; j++) {
if (j > 0) System.out.print(", ");
System.out.print(paraTypes[j].getName());
}
System.out.println(");");
}
}
private 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());
System.out.println(type.getName() + " " + name + ";");
}
}
}
在运行时使用反射分析对象
利用反射机制可以查看编译时还不清楚的对象域,查看对象域的值的关键方法是 Field 类中的 get 方法。
Employee harry = new Employee("xuhongliang");
Class cl = harry.getClass();
try {
Field f = cl.getDeclaredField("name");
//末尾没有 s ,表示提供方法的域名指定获取的域的 Field 类型
Object v = f.get(harry);
//获取一个对象,其值为 harry 对象的 f 域的当前值
//如果f域的访问控制禁止访问(如为private,此语句不在一个泪下执行时),那么获取值时会抛出异常 IllegalAccessException。
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
反射机制的默认行为受限于 Java 的访问控制,如果 Java 程序没有受到安全管理器的控制,就可以覆盖访问控制。
达到这个目的的方法为 Field 、 Method 和 Constructor 对象的 setAccessible 方法:
f.setAccessible(true);//为 true 时,f.get(harry) 就不会存在异常,访问控制无效,可访问或修改域的值
Object v = f.get(harry);
获取不同类型数值的问题
name 类型为 String 他可以作为 Object 对象返回。但是如果值为 double 类型就有问题,java 中数值类型并不是对象。
解决方法为:
f.get(harry);//也可以,会将域值打包进对象包装器
f.getDouble(harry);//不用说明
修改域的值
使用 set 方法:
Employee harry = new Employee("xuhongliang");
Class cl = harry.getClass();
try {
Field f = cl.getDeclaredField("name");
//末尾没有 s ,表示提供方法的域名指定获取的域的 Field 类型
f.setAccessible(true);
f.set(harry, "sanded");
//获取一个对象,其值为 harry 对象的 f 域的当前值
System.out.println(f.get(harry).toString());//sanded
System.out.println(harry.getName());//sanded
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
API
//java.lang.reflect.AccessibleObject
void setAccessible(boolean flag) //设置可访问标记。true 表明屏蔽访问检察;false 反之。
boolean isAccessible() //返回反射对象的可访问标志的值。
static void setAccessible(AccessibleObject[] array, boolean flag) //设置对象数组可访问标志
//java.lang.Class
Field getField(String name) //获取指定名称的公有域
Field[] getField() //获取包含公有域的数组
Field getDeclaredField(String name) //获取指定名称的域
Field[] getDeclaredField(String name) //获取包含声明的全部域的数组
//java.lang.reflect.Field
Object get(Object obj) //返回 obj 对象中用 Field 对象表示的域值
void set(Object obj, Object newValue) //设置 obj 对象中 Field 对象表示的域的值
使用反射编写泛型数组代码
还记得扩大数组长度的方法吗:
int[] a = Arrays.copyOf(new int[2], 100);
System.out.println(a.length);//100
它的源码使用了重载,针对了不同类型的数组类型,针对不同类的源码如下:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
可见源码使用了泛型来实现。
我们不使用泛型,使用反射:
public static Object goodCopyOf(Object a, int newLength){
Class cl = a.getClass();
if (!cl.isArray()) return null;
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}
API
//java.lang.reflect.Array
static Object get(Object array, int index)
static xxx getXxx(Object array, int index)
// xxx 是 boolean byte char double float int long short
// 方法返回存储在给定位置上的给定数组的内容
static void set(Object array, int index, Object newValue)
static void setXxx(Object array, int index, xxx newValue)
// 将一个新值存储到给定位置上的给定数组中
static int getLength(Object array)
// 获取数组的长度
static Object newInstance(Class componentType, int length)
static Object newInstance(Class componentType, int[] lengths)
// 返回一个具有给定类型、给定维数的新数组
调用任意方法
反射机制允许你调用任意方法。
Method 类中的 invoke 方法,允许调用包装在当前 Method 对象中的方法:
Object invoke(Object obj, Object... args)
//第一个参数是隐式参数,其余的对象提供了显式参数
//如果没有隐式参数,使用 null 代替,比如静态方法没有隐式参数就设置为 null
假设 ml 代表 Employee 类的 getName 方法,下面显式了调用方法:
String n = (String) ml.invoke(harry);
//如果返回类型是基本类型,invoke 会返回其包装器类型。
因为方法可以重载,所以不能像域那样方便的获取。为了准确的得到想要的方法,必须提供想要的方法的参数类型,getMethod 的签名:
Method getMethod(String name, Class... parameterTypes)
获取类方法的方法指针:
Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);
用反射调用 Math.sqrt 静态方法:
Method f = Math.class.getMethod("sqrt", double.class);
double y = (Double) f.invoke(null, x);
总结:
- method 对象实现 C 语言中函数指针的所有操作。不过出错可能性大,调用方法引用错误的方法名或类型都会抛出异常。
- invoke 的参数和返回值必须是 Object 类型的。需要多次的类型转换,反射获取方法指针的代码要比仅仅直接调用方法明显慢一些。
继承设计的技巧
- 将公共操作或域放在超类。
- 不要使用受保护的域。
- 使用继承实现“is-a”关系。
- 除非所有继承的方法都有意义,否则不要使用继承。
- 在覆盖方法时,不要改变预期的行为。
- 使用多态,而非类型信息。
- 不要过多的使用反射。
以上是关于Java核心技术卷一 3. java继承多态和反射的主要内容,如果未能解决你的问题,请参考以下文章