Java学习笔记(3):继承

"is a"关系是继承的一个明显特征。

关键字extends表示继承,格式:

class Manager extends Employee
{
	添加方法和域
}

Java中,所有的继承都是公有继承。

对于子类,超类中需要修改的方法可以通过重写一个新的同名同参数的方法来覆盖。

注意:子类的方法不能直接访问超类的私有域。只能通过超类提供的公有接口。关键字super用来访问超类中的方法。

public double getSalary() // override
{
	// return salary + bonus; // won't work
	
	//double baseSalary = getSalary(); // still won't work
	double baseSalary = super.getSalary();
	return baseSalary + bonus;
}
super与this不是类似的概念,super不是一个对象的引用,不能将super赋给另一个对象变量。


super在构造器中的应用:

public Manager(String n, double s, int year, int month, int day)
{
	super(n, s, year, month, day);
	bonus = 0;
}
子类的构造器不能访问超类的私有域,所以必须利用超类的构造器对这部分私有域进行初始化。使用super调用构造器的语句必须是子类构造器的第一条语句。

如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有默认构造器,且在子类构造器中没有显式调用超类其他构造器,编译器将报错。


Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);

Employee[] stuff = 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); 

一个对象变量可以引用多种实际类型的现象被称为多态。在运行时能够自动地选择调用哪个方法的现象称为动态绑定。


继承层次:由一个公共超类派生出来的所有类的集合。

与C++不同,Java不支持多继承。


动态绑定

调用对象方法的执行过程:

  1. 编译器查看对象的声明类型和方法名。
  2. 编译器查看调用方法时提供的参数类型。如果在所有同名方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析。
  3. 如果是private方法、static方法、final方法或者构造器,那么编译器可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定。
  4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与所引用对象实际类型最合适的那个类的方法。

在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。

阻止继承:final类和方法
不允许扩展的类被称为final类:
final class Executive extends Manager
{
	...
}
类中的方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中所有方法自动地成为final方法):
class Employee
{
	...
	public final String getName()
	{
		return name;
	}
	...
}
域也可以被声明为final。对于final域来说,构造对象之后就不允许改变其值了。不过,如果一个类声明为final,只有其中的方法自动成为final,而不包括域。

强制类型转换
double x = 3.405;
int nx = (int)x;

Manager boss = (Manager)staff[0];

进行类型转换的惟一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。

如果试图在继承链上进行向下的类型转换,并且“谎报”有关对象包含的内容,Java运行时系统将报告这个错误:
Manager boss = (Manager)staff[1]; //ERROR
因此,应在进行类型转换之前,使用instanceof运算符查看一下是否能够成功地转换:
if (staff[1] instanceof Manager)
{
	boss = (Manager)staff[1];
}
如果类型转换不可能成功,编译器就不会进行这个转换:
Date c = (Date)staff[1];
综上所述:
  • 只能在继承层次内进行类型转换。
  • 在将超类转换成子类之前,应该使用instanceof进行检查。

抽象类
如果一个方法不需实现,可使用abstract关键字:
public abstract String getDescription();
	// no implementation required
为了提高程序清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的:
abstract class Person
{
	...
	public abstract String getDescription();
}
抽象类还可以包含具体数据和具体方法:
abstract class Person
{
	public Person(String n)
	{
		name = n;
	}
	
	public abstract String getDescrition();
	
	public String getName()
	{
		return name;
	}
	
	private String name;
}
建议尽量将通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象的)中。

扩展抽象类可以有两种选择:
  • 在子类中定义部分抽象方法(或抽象方法也不定义)。这样必须将子类也标记为抽象类。
  • 在子类中定义全部抽象方法。这样子类就不是抽象的了。
类即使不含抽象方法,也可以声明为抽象类。
抽象类不能实例化。
注意:可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象:
Person p = new Student("Vince Vu", "Economics");

受保护访问

若希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,为此,需要将这些方法或域声明为protected。

但是子类中的方法只能访问该子类对象中的protected域,而不能访问其它超类对象中的这个域。

Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。


访问修饰符归纳:

  • private:仅对本类可见
  • public:对所有类可见
  • protected:对本包和所有子类可见
  • 默认:对本包可见

Object:所有类的超类
在Java中,每个类都是由Object类扩展而来。
可以使用Object类型的变量引用任何类型的对象:
Object obj = new Employee("Harry Hacker", 35000);
Object类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换:
Employee e = (Employee)obj;
在Java中,只有基本类型(如数值、字符和布尔类型)不是对象。所有的数组类型,都扩展于Object类:
Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK

Equals方法
Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。然而,对于多数类来说,这种判断没有意义。(两个对象状态相等,不代表具有相同的引用)
例:
class Employee
{
	...
	public boolean equals(Object otherObject)
	{
		// a quick test to see if the objects are identical
		if (this == otherObject) return true;
		
		// must return false if the explicit parameter is null
		if (otherObject == null) return false;
		
		// uf the classes don't match, they can't be equal
		if (getClass() != otherObject.getClass()) //getClass方法将返回一个对象所属的类
			return false;
			
		// now we know otherObject is a non-null Employee
		Employee other = (Employee)otherObject;
		
		// test whether the fields have identical values
		return name.equals(other.name)
			&& salary == other.salary
			&& hireDay.equals(other.hireDay);
	}
}
在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域:
class Manager extends Employee
{
	...
	public boolean equals(Object otherObject)
	{
		if (!super.equals(otherObject)) return false;
		
		// super.equals checked that this and otherObject belong to the same class
		Manager other = (Manager)otherObject;
		return bonus == other.bonus;
	}
}

相等测试与继承

不建议使用instanceof检测隐式和显式参数是否属于同一类:

if (!(otherObject instanceof Employee)) return false; // 不建议这么做
Java语言规范要求equals方法具有下面的特性:

  1. 自反性:x.equals(x)应返回true。
  2. 对称性:若y.equals(x)返回true,则x.equals(y)应返回true。
  3. 传递性:若x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)应返回true。
  4. 一致性:若x和y引用的对象没有发生变化,反复调用x.equals(y)应返回同样的结果。
  5. 对于任意非空引用x,x.equals(null)应返回false。
设e是一个Employee对象,m是一个Manager对象,且两对象具有相同姓名、薪水和雇佣日期。考虑下式:
e.equals(m)
若在Employee.equals中用instanceof进行检测,则返回true。但这要求调用:
m.equals(e)
同样返回true。这就使Manager类受到了束缚。这个类的equals方法必须能够用自己与任何一个Employee对象进行比较,切不考虑自己的特有信息。
可以从两个截然不同的情况看这个问题:
如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
如果由超类决定相等的概念,那么可以使用instanceof进行检测,这样可以在不同子类对象之间比较。

完美的equals方法建议:
  1. 显示参数命名为otherObject。
  2. 检测this与otherObject是否引用同一个对象:
    if (this == otherObject) return true;
  3. 检测otherObject是否为null:
    if (otherObject == null) return false;
  4. 比较this与otherObject是否属于同一个类:如果equals的语义在每个子类中有所改变,就是用getClass检测:
    if (getClass() != otherObject.getClass()) return false;
    如果所有的子类都拥有统一的语义,就是用instanceof检测:
    if (!(otherObject instanceof ClassName)) return false;
  5. 将otherObject转换为相应的类类型变量:
    ClassName other = (ClassName)otherObject
  6. 对所有需要比较的域进行比较。使用==比较基本类型域,使用equals比较对象域。
如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。
对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。

HashCode方法
散列码是由对象导出的一个整型值。
String类使用下列算法计算散列码:
int hash = 0;
for (int i = 0; i < length(); i++)
	hash = 31 * hash + charAt(i);
例:
		String s = "Ok";
		StringBuilder sb = new StringBuilder(s);
		System.out.println(s.hashCode() + " " + sb.hashCode()); // 2556 7726332
		String t = new String("Ok");
		StringBuilder tb = new StringBuilder(t);
		System.out.println(t.hashCode() + " " + tb.hashCode()); // 2556 811720

字符串s和t散列码相同,因为字符串的散列码是由内容导出的。而字符串缓冲sb和tb散列码不同,因为StringBuffer类没有定义hasCode方法,它的散列码是由Object类的默认hashCode方法导出的对象存储地址。
如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。
例如,下面是Employee类的hashCode方法:
class Employee
{
	public int hashCode()
	{
		return 7 * name.hashCode()
			+ 11 * new Double(salary).hashCode()
			+ 13 * hireDay.hashCode();
	}
	...
}
Equals与hashCode的定义必须一致。若x.equals(y)返回true,则x.hashCode()应与y.hashCode()相等。
如果存在数组类型的域,可以使用静态的Arrays.hashCode方法计算散列码。

ToString方法
Object中还有一个重要的方法,就是toString方法。它用于返回表示对象值的字符串。
绝大多数的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。
下面是Employee类的toString方法的实现:
public String toString()
{
	return "Employee[name=" + name
		+ ",salary=" + salary
		+ ",hireDay=" + hireDay
		+ "]";
}
实际上,最好通过getClass().getName()获得类名的字符串:
public String toString()
{
	return getClass().getName()
		+ "[name=" + name
		+ ",salary=" + salary
		+ ",hireDay=" + hireDay
		+ "]";
}
子类也应定义自己的toString方法,并将子类域的描述添加进去,如果超类使用了getClass().getName(),那么子类只要调用super.toString()就可以了,例:
class Manager extends Employee
{
	...
	public String toString()
	{
		return super.toString()
			+ "[bonus=" + bonus
			+"]";
	}
}
现在,Manager对象的打印输出如下:
Manager[name=...,salary=...,hireDay=...][bonus=...]
随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,就会自动调用toString方法,以便获得这个对象的字符串描述。
如果x是任意一个对象,并调用
System.out.println(x);
println方法就会直接地调用x.toString()。
Object类定义了toString方法,用来打印输出对象所属的类名和散列码。
在调用x.toString()的地方可以用“”+x替代。与toString不同的是,如果x是基本类型,这条语句照样能够执行。

数组继承了object类的toString方法,数组类型将按照旧的格式打印,如:
int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
String s = "" + luckyNumbers;

将返回[I@af993e。(前缀[I表明是一个整型数组)。修正方式是调用静态方法Arrays.toString:
String s = Arrays.toString(luckyNumbers);
将返回[2, 3, 5, 7, 11, 13]。
若想打印多维数组,需调用Arrays.deepToString方法。

强烈建议为自定义的每一个类增加toString方法。

泛型数组列表
Java允许在运行时确定数组大小(但不能动态改变):
int actualSize = ...;
Employee[] staff = new Employee(actualSize);

若需动态改变数组大小,则需使用ArrayList类(看上去有点像C++的vector)。
ArrayList是一个采用 类型参数泛型类
下面声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
使用add方法可将元素添加到数组列表中:
staff.add(new Employee("Harry Hacker", ...));
staff add(new Employee("Tony Tester", ...));
如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法确定数组容量:
staff.ensureCapacity(100);
另外,还可把初始容量传递给ArrayList构造器:
ArrayList<Employee> staff = new ArrayList<Employee>(100);
size方法返回数组列表中包含的实际元素数目(等价于数组a的a.length):
staff.size()
一旦能够确认数组列表大小不再发生变化,可以调用trimToSize方法,将存储区域的大小调整为当前元素数量所需的存储空间数目。

访问数组列表元素
使用get和set方法实现访问或改变数组元素的操作(不使用[]语法格式):
staff.set(i, harry); // 设置第i个元素为harry
staff.get(i);
不能使用set方法添加新元素,要使用add:
ArrayList<Employee> list = new ArrayList<Employee>(100); // capacity 100, size 0
list.set(0, x); // no element 0 yet

下面的技巧既可以灵活地扩展数组,又可以方便地访问数组元素:
首先,创建一个数组,并添加所有元素:
ArrayList<X> list = new ArrayList<X>();
while (...)
{
	x = ...;
	list.add(x);
}

其次,使用toArray方法将数组元素拷贝到一个数组中:
X[] a = new X(list.size());
list.toArray(a);

可以使用带索引参数的add方法在数组列表中间插入元素:
int n = staff.size() / 2;
staff.add(n, e);

使用remove方法删除一个元素:
Employee e = staff.remove(n);

对数组实施插入和删除元素操作的效率较低。若数组较大,或插入删除操作较频繁,请考虑使用链表实现。

可使用for each循环遍历数组列表:
for (Employee e : staff)
	do something with e

Java SE 5.0以前版本没有泛型类,原始的ArrayList保存类型为Object的元素。若现存程序中使用了原始的ArrayList类,可讲一个类型化的ArrayList传递给需要原始ArrayList类的方法,而不需要进行类型转换:
public class EmployeeDB
{
	public void update(ArrayList list) { ... }
	public ArrayList find(String query) { ... }
}

ArrayList<Employee> staff = ...;
employeeDB.update(staff);

相反,将一个原始ArrayList赋给一个类型化ArrayList会得到一个警告:
ArrayList<Employee> result = employeeDB.find(query); // yields warning
使用类型转换并不能避免出现警告:
ArrayList<Employee> result = (ArrayList<Employee>)employeeDB.find(query); // yields another warning

对象包装器与自动打包
所有基本类型都有一个与之对应的类,这些类通常称为“包装器”:
Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6个派生于公共超类Number)。
对象包装器是不可变的,同时,对象包装器类还是final。
假设想定义一个整型数组列表,而尖括号中类型参数不允许是基本类型(不能写成ArrayList<int>)。这时,可以声明一个Integer对象的数组列表:
ArrayList<Integer> list = new ArrayList<Integer>();

注:由于每个值分别包装在对象中,所有ArrayList<Integer>的效率远低于int[]数组。因此应用它构造小型集合。

自动打包
ArrayList<Integer>的add方法支持直接传入一个整数:
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);

参数数量可变的方法
printf就是一个参数数量可变的方法,它是这样定义的:
public class PrintStream
{
	public PrintStream printf(String fmt, Object... args) { return format(fmt, args); }
}

这里的省略号...是Java代码的一部分,它表明这个方法可以接收任意数量的对象。
实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组。
即,对printf的实现者来说,Obejct...参数类型与Object[]完全一样。
示例:
// 计算若干个数值的最大值
public static double max(double... values)
{
	double largest = Double.MIN_VALUE;
	for (double v : values) if (v > largest) largest = v;
	return largest;
}

注:允许将一个数组传递给可变参数方法的最后一个参数:
System.out.printf("%d %s", new Object[] {new Integer(1), "widgets"});

枚举类
下面是一个典型的枚举类型:
public enum Size{ SMALL, MEDIUM, LARGE, EXTRA_LARGE };
实际上,这个声明定义的类型是一个类,刚好有4个实例。
如果需要的话,可以在枚举类型中添加一些构造器、方法和域:
enum Size
{
	SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
	
	private Size(String abbreviation) { this.abbreviaion = abbreviation; }
	public String getArreviation() { return abbreviation; }
	
	private String abbreviation;
}
所有枚举类型都是Enum类的子类。它们继承了这个类的许多方法。
toString方法返回枚举常量名:
Size.SMALL.toString() // 返回字符串“SMALL”
toString的逆方法是静态方法valueOf:
Size s = (Size)Enum.valueOf(Size.class, "SMALL"); // 将s设置成Size.SMALL
每个枚举类型都有一个静态的values方法,它返回一个包含全部枚举值的数组:
Size[] values = Size.values();
ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数:
Size.MEDIUM.ordinal() // return 1

反射
反射库(reflection library) 提供了一个非常丰富且精心设计的工具集。
能够分析类能力的程序被称为反射(reflective)。
可以使用反射机制:
  • 在运行中分析类的能力
  • 在运行中查看对象,例如,编写一个toString方法供所有类使用
  • 实现数组的操作代码
  • 利用Method对象,这个对象很像C++中的函数指针

Class类
程序运行期间,Java运行时系统始终为所有的对象维护一个称为运行时的类型标识。这个信息保存着每个对象所属的类足迹。虚拟机利用运行时信息选择相应的方法执行。
可以通过专门的Java类访问这些信息。保存这些信息的类称为Class。
Object类中的getClass()方法返回一个Class类型的实例:
Employee e;
...
Class cl = e.getClass();
一个Class对象表示一个特定类的属性。最常用的Class方法是getName。这个方法将返回类的名字:
System.out.println(e.getClass().getName() + " " + e.getName()); // 可能输出Employee Harry Hacker
如果类在一个包里,包的名字也可以作为类名的一部分:
Date d = new Date();
Class cl = d.getClass();
String name = cl.getName(); // name is set to "java.util.Date"
还可以调用静态方法forName获得类名对应的Class对象:
String className = "java.util.Date";
Class cl = Class.forName(className);
另一种获得Class类对象的方法:如果T是任意Java类型,T.class将代表匹配的类对象:
Class cl1 = Date.class; // if you import java.util.*;
Class cl2 = int.Class;
Class cl3 = Double[].class;

注:鉴于历史原因,getName方法在应用于数组类型时会返回一个奇怪的名字:
Double[].class.getName()返回“[Ljava.lang.Double;”。
int[].class.getName()返回“[I”。
可以利用==运算符实现两个类对象比较的操作:
if (getClass() == Employee.class) ...
newInstance()方法可以用来快速创建一个类的实例:
e.getClass().newInstance(); // 调用默认构造器创建一个与e具有相同类类型的实例
通过forName与newInstance配合使用,可以根据存储在字符串中的类名创建一个对象:
String s = "java.util.Date";
Object m = Class.forName(s).newInstance();

捕获异常
当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序要灵活得多,因为可以提供一个“捕获”异常的处理器对异常情况进行处理。
异常有两种类型: 未检查异常和 已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。
将可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后在catch子句中提供处理器代码。
try
{
	statements that might throw exceptions
}
catch(Exception e)
{
	handler action
}
示例:
try
{
	String name = ... // get class name
	Class cl = Class.forName(name); // might throw exception
	... // do something with cl
}
catch(Exception e)
{
	e.printStackTrace();
}
在这里,利用Throwable类的printStackTrace方法打印出栈的轨迹。如果try块没有抛出任何异常,将会跳过catch子句的处理器代码。

利用反射分析类的能力
检查类的结构是反射机制最重要的内容。
在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。
这三个类都有一个叫作getName的方法,用来返回项目的名称。
Field类有一个getType方法,用来返回描述域所属类型的Class对象。
Method和Constructor类有能够报告参数类型的方法getParameterTypes,Method类还有一个可以报告返回类型的方法getGenericReturnType。
这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的开关描述public和static这样的修饰符使用状况。

在运行时使用反射分析对象
利用反射机制可以查看在编译时还不清楚的对象域,其使用的关键方法是Field类中的get方法。如果f是一个Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值:
Employee harry = new Employee("Hary Hacker", 35000, 1989, 10, 1);
Class cl = harry.getClass();
	// the class object representing Employee
Field f = cl.getDeclareField("name");
	// the name field of the Employee class
Object v = f.get(harry);
	// the value of the name field of the harry object
	// i.e., the String object "Harry Hacker"

实际上,这段代码存在问题。name是一个私有域,所以get方法将会抛出一个IllegalAccessException。

反射机制的默认行为受限于Java的访问控制。可以通过调用Field、Method或Constructor对象的setAccessable方法,覆盖访问控制:
f.setAccessable(true); // now OK to call f.get(harry);
注:实际上setAccessible是启用和禁用访问安全检查的开关,并不是为true就能访问为false就不能访问。
由于JDK的安全检查耗时较多,所以通过setAccessible(true)的方式关闭安全检查可以达到提升反射速度的目的。

get方法还有一个要解决的问题。name域是一个String,因此把它作为Object返回没有问题。但是,如果想要查看salary域。它属于double类型,而Java中数值类型不是对象。
可以使用Field类中的getDouble方法,也可以调用get方法,这个域值将会自动打包成Double。
调用f.set(obj, value)可以将obj对象的f域设置成新值。

使用反射编写泛型数组代码
java.lang.reflect包中的Array类允许动态创建数组。
假设有一个元素为某种类型且已经被填满数组。现希望扩展其长度,但不想手工编写扩展、拷贝元素的代码,而是想编写一个用于扩展数组的通用方法:
Employee[] a = new Employee[100];
...
// array is full
a = (E,pmployee[])arragGrow(a);

下面是一个通用的方法,其功能是将数组扩展到10%+10个元素:
static Object[] badArrayGrow(Object[] a) // not useful
{
	int newLength = a.length * 11 / 10 + 10;
	Object[] newArray = new Object[newLength];
	System.arraycopy(a, 0, newArray, 0, a.length);
	return newArray;
}

实际使用中会遇到一个问题:这段代码返回的数组类型是对象数组(Object[])类型,一个对象数组不能转换成雇员数组(Employee[])。
因此,需要能够创建与原数组类型相同的新数组。为此,需要java.lang.reflect包中Array类的一些方法。其中最关键的是Array类中的静态方法newInstance,它能够构造新数组:
Object newArray = Array.newInstance(componentType, newLength);

可以通过调用Array.getLength(a)获得数组长度,也可以通过Array类的静态getLength方法的返回值得到任意数组的长度。而要获得新数组元素类型,需要进行以下工作:
1)首先获得a数组的类对象
2)确认它是一个数组
3)使用Class类的getComponentType方法确定数组对应的类型
下面是代码:
static Object goodArrayGrow(Object a) // useful
{
	Class cl = a.getClass();
	if (!cl.isArray()) return null;
	Class componentType = cl.getComponentType();
	int length = Array.getLength(a);
	int newLength = length * 11 / 10 + 10;
	Object newArray = Array.newInstance(componentType, newLength);
	System.arraycopy(a, 0, newArray, 0, length);
	return newArray;
}
arrayGrow方法可以用来扩展任意类型的数组,而不仅是对象数组:
int[] a = { 1, 2, 3, 4 };
a = (int[])goodArrayGrow(a);

方法指针
在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法,其签名是:
Object invoke(Object obj, Object... args)
第一个参数是隐式参数,其余的对象提供了显式参数。
对于静态方法,第一个参数可以忽略,即可以将它设置为null。
例,假设m1代表Employee类的getName方法:
String n = (String)m1.invoke(harry);
如果返回类型是一种基本类型,则invoke方法返回包装器类型。假设m2代表Employee类的getSalary方法:
double s = (Double)m2.invoke(harry);

可以通过调用Class类中的getMethod方法得到Method对象。然而,有可能存在若干相同名字的方法,因此要小心,以确保能够准确得到想要的方法。
getMethod的签名是:
Method getMethod(String name, Class... parameterTypes)
下面的例子说明了如何获得Employee类的getName方法和raiseSalary方法的方法指针:
Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);

继承设计的技巧
  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现“is-a”关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为
  6. 使用多态,而非类型信息
  7. 不要过多地使用反射





猜你喜欢

转载自blog.csdn.net/angel_lys/article/details/8532025