第5章 继承
5.1 类、超类和子类
5.1.1 定义子类
继承
- 利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。
- 在 Java 中, 所有的继承都是公有继承, 而没有 C++ 中的私有继承和保护继承 。
extends关键字
- 关键字 extends 表明正在构造的新类派生于一个已存在的类。
类的分类
- 已存在的类称为超类( superclass)、 基类(base class) 或父类(parent class);
- 新类称为子类(subclass) 、派生类(derived class) 或孩子类(child class)。
5.1.2 覆盖方法
子类访问超类私有类
- 子类方法不能够直接地访问超类的私有域。
- 可以使用特定的关键字 super 解决这个问题:
super.getSalary()
super关键字
- super 不是一个对象的引用, 它只是一个指示编译器调用超类方法的特殊关键字。
- 是“ 调用超类xxx的构造器” 的简写形式(使用super 调用构造器的语句必须是子类构造器的第一条语句)。
super(n, s, year, month, day)
5.1.3 子类构造器
子类构造器
- 如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数 )的构造器。
- 如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器’,则 Java 编译器将报告错误。
this的两个用途
- 一是引用隐式参数。
- 二是调用该类其他的构造器 。
super的两个用途
- 一是调用超类的方法。
- 二是调用超类的构造器。
5.1.4 继承层次
继承层次
- 由一个公共超类派生出来的所有类的集合被称为继承层次( inheritance hierarchy ),。
继承链
- 在继承层次中, 从某个特定的类到其祖先的路径被称为该类的继承链 ( inheritance chain) 。
5.1.5 多态
设计继承
- 有一个用来判断是否应该设计为继承关系的简单规则, 这就是“is-a” 规则, 它表明子类的每个对象也是超类的对象。
- “ is-a” 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。
多态
- 一个对象变量(例如, 变量 e ) 可以指示多种实际类型的现象被称为多态( polymorphism)。
- 对象变量是多态的。 一个 Employee 变量既可以引用一个Employee 类对象, 也可以引用一个 Employee 类的任何一个子类的对象。
- 然而,不能将一个超类的引用赋给子类变量。
Manager boss = new Manager(. . .);
Employee[] staff = new Employee[3];
staff[0] = boss;
boss.setBonus(5000); // OK
staff[0].setBonus(5000); // Error
关于数组转换问题
- 在 Java 中,子类数组的引用可以转换成超类数组的引用, 而不需要采用强制类型转换。
- 所有数组都要牢记创建它们的元素类型, 并负责监督仅将类型兼容的引用存储到数组中。
- 如果试图存储一个 Employee 类型的引用就会引ArrayStoreException 异常。
Manager[] managers = new Manager[10];
Employee[] staff = managers; // ok
staff[0] = new Employee("Harry Hacker", . . .);
managers[0].setBonus(U)00);//error
5.1.6 理解方法调用
方法调用
下面假设要调用 x.f(args,) 隐式参数 x 声明为类 C 的一个对象。
- 编译器査看对象的声明类型和方法名。
- 接下来,编译器将査看调用方法时提供的参数类型。
- 如果是 private 方法、 static 方法、 final 方法)或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定( static binding)。
- 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。
重载解析
- 如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重栽解析(overloadingresolution)。
- 允许类型转换(int 可以转换成 double, Manager 可以转换成 Employee, 等等)。
注意返回类型
- 方法的名字和参数列表称为方法的签名,返回类型不是签名的一部分。
- 在覆盖方法时, 一定要保证返回类型的兼容性。 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。
public Employee getBuddyO { . . . }
public Manager getBuddyO { . . . } // OK to change return type
我们说,这两个 getBuddy 方法具有可协变的返回类型。
覆盖方法时
- 一定要保证返回类型的兼容性。
- 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。
动态绑定
- 在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
方法表
- 虚拟机预先为每个类创建了一个方法表( method table),其中列出了所有方法的签名和实际调用的方法。
- 在真正调用方法的时候,虚拟机仅查找这个表就行了,如果调用 super.f(param), 编译器将对隐式参数超类的方法表进行搜索。
方法解析过程
- 首先,虚拟机提取 e 的实际类型的方法表。
- 接下来, 虚拟机搜索定义 xxx 签名的类。
- 最后,虚拟机调用方法。
5.1.7 阻止继承:final 类和方法
final类
- 不允许扩展的类被称为 final 类。
- final 类中的所有方法自动地成为final方法,但不包括域。
- 将方法或类声明为 final 主要目的是:确保它们不会在子类中改变语义。
内联
- 如果一个方法没有被覆盖并且很短, 编译器就能够对它进行优化处理, 这个过程为称为内联( inlining )。
5.1.8 强制类型转换
对象强制类型转换
- 对象引用的转换语法与数值表达式的类型转换类似, 仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。
- 进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
ClassCastException异常
Manager boss = (Manager) staff[1]; // Error
- 如果试图在继承链上进行向下的类型转换,运行这个程序时,Java 运行时系统将报告这个错误,并产生一个 ClassCastException异常。
使用instanceof操作符
- 在进行类型转换之前,先查看一下是否能够成功地转换。这个过程简单地使用instanceof 操作符就可以实现。
- 如果 x 为 null , 进行instanceof测试,不会产生异常, 只是返回false。
if (staff[1] instanceof Manager) {
boss = (Manager) staff[1]:
...
}
强制类型转换要求
- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前,应该使用 instanceof进行检查。
5.1.9 抽象类
缘由
- 如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。
抽象类
- 为了提高程序的清晰度, 包含一个或多个抽象方法的类本身必须被声明为抽象的。
- 除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
- 类即使不含抽象方法,也可以将类声明为抽象类。
- 可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象。
扩展抽象类
- 一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;
- 另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
抽象类方法调用
- 由于不能构造抽象类 Person 的对象, 所以变量 p 永远不会引用 Person 对象, 而是引用诸如 Employee 或 Student 这样的具体子类对象, 而这些对象中都定义了 getDescription 方法。
5.1.10 受保护访问
有实际意义
- 如果需要限制某个方法的使用, 就可以将它声明为protected。
- 事实上,Java 中的受保护部分对所有子类及同一个包中的所有其他类都可见。
4 个访问修饰符
- 仅对本类可见——private。
- 对所有类可见——public。
- 对本包和所有子类可见——protected。
- 对本包可见——默认 ,不需要修饰符。
5.2 Object: 所有类的超类
object类
- 在 Java中,每个类都是由 Object 类扩展而来的。
- 在 Java 中,只有基本类型 ( primitive types) 不是对象。
- 所有的数组类塱,不管是对象数组还是基本类型的数组都扩展了 Object 类。
5.2.1 equals 方法
equals方法
- Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。
- 在 Object 类中,这个方法将判断两个对象是否具有相同的引用。
5.2.2 相等测试与继承
equals方法要求特性
- 自反性:对于任何非空引用 x, x.equals(?0 应该返回 true。
- 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true , x.equals(y) 也应该返回 true。
- 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返 N true, y.equals(z) 返回 true,x.equals(z) 也应该返回 true。
- 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.eqimIS(y) 应该返回同样的结果。
- 对于任意非空引用 x, x.equals(null) 应该返回 false。
相等检测方法
- 如果子类能够拥有自己的相等概念, 则对称性需求将强制采用 getClass 进行检测。
- 如果由超类决定相等的概念,那么就可以使用 imtanceof进行检测, 这样可以在不同子类的对象之间进行相等的比较。
完美的 equals 方法的建议:
- 显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 变量。
- 检测 this 与 otherObject 是否引用同一个对象:
- 检测 otherObject 是否为 null。
- 比较 this 与 otherObject 是否属于同一个类,使用.getCIass())或instanceof 。
- 将 otherObject 转换为相应的类类型变量:
- 对所有需要比较的域比较,使用 == 比较基本类型域,使用 equals 比较对象域。如果在子类中重新定义 equals, 就要在其中包含调用 super.equals(other)。
比较方法相关
- 使用Objects.equa1s(fie1d2, other.field2)防止空指针。
- 使用Arrays.equals 方法检测相应的数组元素是否相等。
注意覆写equals 方法
public boolean equals(Employee other)
- 这个方法声明的显式参数类型是 Employee。其结果并没有覆盖 Object 类的 equals 方法, 而是定义了一个完全无关的方法,编译器会给出错误报告。
- 为了避免发生类型错误, 可以使用 @Override 对覆盖超类的方法进行标记:
©Override public boolean equals(Object other)
5.2.3 hashCode 方法
散列码
- 散列码( hash code ) 是由对象导出的一个整型值。
- 散列码是没有规律的。
hashCode方法
- 由于 hashCode方法定义在 Object 类中
- 每个对象都有一个默认的散列码,其值对象的存储地址。
- 如果重新定义 equals方法,就必须重新定义 hashCode 方法, 以便用户可以将对象插人到散列表中。
- hashCode 方法应该返回一个整型数值(也可以是负数) ,并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。
获得hashCode
-
最好使用 null 安全的方法 Objects.hashCode,如果其参数为 null,这个方法会返回 0, 否则返回对参数调用 hashCode 的结果。
-
使用静态方法 Double.hashCode 来避免创建 Double 对象
new Double(2.5).hashCode();
Double.hashCode(2.5); -
需要组合多个散列值时,可以调用 ObjeCtS.hash 并提供多个参数。
Objects,hash(name, salary, hireDay);
-
如果存在数组类型的域, 那么可以使用静态的 Arrays.hashCode 方法计算一个散列码。
注意equals和hashCode一致
- Equals 与 hashCode 的定义必须一致:如果 x.equals(y) 返回 true, 那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值。
5.2.4 toString 方法
toString 方法
- 在 Object 中还有一个重要的方法, 就是 toString方法, 它用于返回表示对象值的字符串。
- 绝大多数(但不是全部)的 toString方法都遵循这样的格式:类的名字,随后是一对号括起来的域值。
- 随处可见 toString方法的主要原因是:只要对象与一个字符串通过操作符“+” 连接起来,Java 编译就会自动地调用 toString方法,以便获得这个对象的字符串描述。
定义自己的toString 方法
- 最好通过调用 getClass( ).getName( ) 获得类名的字符串。
- 如果超类使用了 getClass( ).getName( ), 那么子类只要调用 super.toString( )就可以了。
小技巧
- 在调用 x.toString( ) 的地方可以用 “”+x 替代。
- 与 toString 不同的是,如果 x 是基本类型,这条语句照样能够执行。
Object类的toString方法
- Object 类定义了 toString 方法, 用来打印输出对象所属的类名和散列码。
System.out.println(System.out)//that is java.io.PrintStream@2f6684
数组的toString方法
-
数组继承了 object 类的 toString方法。
int[] luckyNumbers = { 2, 3, 5, 7 S llf 13 } ;
String s = “” + luckyNumbers; -
修正的方式是调用静态方法 Arrays.toString。代码:
String s = Arrays.toString(luckyNumbers);
[2,3,5,7,11,13]
5.3 泛型数组列表
ArrayList类
- ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class)。
- Java SE 7中, 可以省去右边的类型参数:
ArrayList<Employee> staff = new ArrayList<>();
初始值
-
如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调ensureCapacity方法:
staff.ensureCapacity(100);
-
还可以把初始容量传递给 ArrayList 构造器:
vArrayList<Employee> staff = new ArrayListo(lOO);
实际元素数目
- size方法将返回数组列表中包含的实际元素数目。
staff,size();
trimToSize方法
- 一旦能够确认数组列表的大小不再发生变化,就可以调用 trimToSize方法。
- 这个方法存储区域的大小调整为当前元素数量所需要的存储空间数目。
5.3.1 访问数组列表元素
访问和修改
- 使用 get 和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的 [ ]语法格式。
- 只有 i 小于或等于数组列表的大小时, 才能够调用 list.set(i,x)。
插入
- 除了在数组列表的尾部追加元素之外,还可以在数组列表的中间插入元素,使用带索参数的 add 方法。
int n = staff.sizeO / 2;
staff.add(n, e);
删除
- 同样地,可以从数组列表中间删除一个元素。
Employee e = staff.remove(n);
5.3.2 类型化与原始数组列表的兼容性
与没有使用类型参数的遗留代码交互操作
public class EmployeeDB {
public void update(ArrayList list) { . . . }
public ArrayList find(String query) { . . . }
}
ArrayList<Employee〉staff = . . .;
employeeDB.update(staff);
ArrayList<Employee> result = employeeDB.find(query);
// yields warning
- update方法在 Java 中增加泛型之前是一样的 。虚拟机的完整性绝对没有受到威胁。在这种情形下, 既没有降低安全性,也没有受益于编译时的检查。
- 将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告。
- 在这种情形下,不必做什么D 只要在与遗留的代码进行交叉操作时,研究一下编泽器的警告性提示,并确保这些警告不会造成太严重的后果就行了。
5.4 对象包装器与自动装箱
包装器
- 所有的基本类型都冇一个与之对应的类, 这些类称为包装器 ( wrapper ) 。
- 这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 、Void 和 Boolean (前6 个类派生于公共的超类 Number)。
- 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
- 对象包装器类还是 final , 因此不能定义它们的子类。
关于ArrayList<lnteger>
- 由于每个值分别包装在对象中, 所以 ArrayList 的效率远远低于 int[ ] 数组。
- 因此, 应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。
自动装箱/拆箱
- 当将一个 int 值赋给一个 Integer 对象时, 将会自动地装箱。
- 当将一个 Integer 对象赋给一个 int 值时, 将会自动地拆箱。
- 甚至在算术表达式中也能够自动地装箱和拆。
例如,可以将自增操作符应用于一个包装器引用:
Integer n = 3;
n++;
包装对象的比较
- 自动装箱规范要求 boolean、byte、char <= 127, 介于 -128 ~ 127 之间的 short 和int 被包装到固定的对象中。
例如,如果将Integer a 和Integer b 初始化为 100,对它们进行比较的结果一定成立。
几个注意点
- 如果将Integer a 和Integer b 初始化为 100,对它们进行比较的结果一定成立。
- 由于包装器类引用可以为 null, 所以自动装箱有可能会抛出一个 NullPointerException 异常。
- 如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱,提升为 double, 再装箱为 Double。
- 装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时, 插人必要的方法调用。虚拟机只是执行这些字节码。
转换字符串到数值
- 可以将某些基本方法放置在包装器中, 例如, 将一个数字字符串转换成数值。
int x = Integer.parselnt(s);//静态方法
修改数值参数
public static void triple(Integer x) // won 't work
{
...
}
- 问题是 Integer 对象是不可变的: 包含在包装器中的内容不会改变:。不能使用这些包装器类创建修改数值参数的方法。
- 如果想编写一个修改数值参数值的方法, 就需要使用在 org.omg.CORBA 包中定义的持有者( holder) 类型, 包括 IntHolder、BooleanHolder 等。
5.5 参数数量可变的方法
省略号参数
public PrintStream printf(String fmt , Object… args) { return format(fmt, args); }
- 这里的省略号 . . . 是 Java 代码的一部分,它表明这个方法可以接收任意数量的对象(除 fmt参数之外。)
- 允许将一个数组传递给可变参数方法的最后一个参数。
System.out.printf("%d %s’, new Object[] { new Integer(l), “widgets” } );
printf方法
- printf方法接收两个参数,一个是格式字符串, 另一个是 Object ] 数组。
- 其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值, 自动装箱功能将把它们转换成对象 )。
- Object…参数类型与 Object[ ]完全一样。
- 编译器需要对 printf 的每次调用进行转换, 以便将参数绑定到数组上,并在必要的时候进行自动装箱:
System.out.printf("%d %s", new ObjectO { new Integer(n), “widgets” } );
5.6 枚举类
典型的例子
public enuni Size { SMALL , MEDIUM, LARGE, EXTRAJARGE };
- 实际上,这个声明定义的类型是一个类, 它刚好有 4 个实例, 在此尽量不要构造新对象。
- 因此,在比较两个枚举类型的值时, 永远不需要调用 equals, 而直接使用“= =” 就可以了。
枚举类型
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; }
}
-
所有的枚举类型都是 Enum 类的子类。
-
toString方法能够返回枚举常量名。
-
toString 的逆方法是静态方法 valueOf。
Size s = Enum.valueOf(Size.class, “SMALL”);
-
每个枚举类型都有一个静态的 values 方法, 它将返回一个包含全部枚举值的数组。
例如,如下调用Size[] values = Size.values()
-
ordinal方法返冋enum声明中枚举常量的位置,位置从0开始计数。
5.7 反射
反射库
- 反射库( reflection library) 提供了一个非常丰富且精心设计的工具集, 以便编写能够动态操纵 Java 代码的程序。
反射
- 能够分析类能力的程序称为反射(reflective )。
反射用处
- 在运行时分析类的能力。
- 在运行时查看对象,例如,编写一个 toString 方法供所有类使用。
- 实现通用的数组操作代码。
- 利用 Method 对象,这个对象很像中的函数指针。
5.7.1 Class 类
class类
- 在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。
- 这个信息跟踪着每个对象所属的类,保存这些信息的类被称为 Class
- 如果类在一个包里,包的名字也作为类名的一部分。
- 一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int 不是类 但 int.class是一个 Class 类型的对象。
- 虚拟机为每个类型管理一个 Class 对象。 因此,可以利用 == 运算符实现两个类对象比较的操作。
获得class类对象的三个方法
-
Object 类中的 getClass( ) 方法将会返回一个 Class 类型的实例。
-
可以调用静态方法 forName 获得类名对应的 Class 对象,这个方法只有在 dassName 是类名或接口名时才能够执行。否则,forName 方法将抛出一个 checkedexception ( 已检查异常)。
String dassName = “java.util .Random”;
Class cl = Cl ass.forName(dassName); -
如果 T 是任意的 Java 类型(或 void 关键字),T.class 将代表匹配的类对象。
动态创建类实例
-
方法 newlnstance( ) 可以用来动态地创建一个类的实例.。
e.getClass0.newlnstance();
-
newlnstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。
-
如果需要以这种方式向希望按名称创建的类的构造器提供参数,必须使用 Constructor 类中的 newlnstance 方法。
5.7.2 捕获异常
异常处理器
- 并不是所有的错误都是可以避免的,抛出异常比终止程序要灵活得多,可以提供一个“ 捕获” 异常的处理器 (handler) 对异常情况进行处理。
- 利用Throwable 类的 printStackTrace 方法打印出栈的轨迹。
异常分类
- 已检查异常,编译器将会检查是否提供了处理器。(例如文件IO)
- 未检查异常,应该精心地编写代码来避免这些错误的发生, 而不要将精力花在编写异常处理器上。(例如访问 null 引用)
5.7.3 利用反射分析类的能力
检查类的结构
- 在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、方法和构造器。
- 这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。
- Field 类有一个 getType 方法, 用来返回描述域所属类型的 Class 对象。
- Method 和 Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。
- 这三个类还有一个叫做 getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。
分析修饰符
- 可以利用java.lang.refleCt 包中的 Modifiei•类的静态方法分析getModifiers 返回的整型数值。
- 可以使用 Modifier 类中的 isPublic、isPrivate 或 isFinal判断方法或构造器是否是public、private 或final。
- 可以利用 Modifier.toString方法将修饰符打印出来。
获得类信息
- Class类中的 getFields、getMethods 和 getConstructors 方 法 将 分 别 返 回 类 提 供 的public 域、 方法和构造器数组, 其中包括超类的公有成员。
- Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
5.7.4 在运行时使用反射分析对象
查看对象域
- 查看对象域的关键方法是 Field类中的 get 方法。
私有域问题
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass0;
// the class object representing Employee
Field f = cl .getDeclaredFieldC'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"
f.setAtcessible(true); // now OK to call f.get(harry);
- get 方法访问私有域将会抛出一个IllegalAccessException。
- 反射机制的默认行为受限于 Java 的访问控制。然而, 如果一个 Java 程序没有受到安全管理器的控制, 就可以覆盖访问控制。
- 为了达到这个目的, 需要调用 Field、 Method 或Constructor 对象的 setAccessible 方法。
获取基本类型问题
- 要想解决问题, 可以使用 Field 类中的 getDouble 方法。
- 也可以调用 get方法,此时, 反射机制将会自动地将这个域值打包到相应的对象包装器中。
设置域的新值
- 当然,可以获得就可以设置。 调用 f.set(obj,value) 可以将 obj 对象的 f 域设置成新值。
5.7.5 使用反射编写泛型数组代码
动态创建数组
- java.lang.reflect 包中的 Array 类允许动态地创建数组。
- 其中最关键的是 Array类中的静态方法 newlnstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。
Object newArray = Array.newlnstance(componentType , newLength);
数组两个参数-
- 可以通过调用 Array.getLength(a) 获得数组的长度, 也可以通过 Array 类的静态 getLength方法的返回值得到任意数组的长度。
- 获得新数组元素类型,首先获得 a 数组的类对象,确认它是一个数组,使用 Class 类(只能定义表示数组的类对象)的getComponentType 方法确定数组对应的类型。
复制数组
public static Object goodCopyOf(Object a, int newLength
{
Class cl = a.getClassO;
if (Icl .isArrayO) return null;
Class componentType = cl .getComponentType0;
int length = Array.getLength(a);
Object newArray = Array.newlnstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}
- 整型数组类型 int[] 可以被转换成 Object,但不能转换成对象数组。
- 为了能够扩展任意类型的数组, 而不仅是对象数组,应该将 goodCopyOf 的参数声明为 Object 类型,.而不要声明为对象型数组(Object[])。
int[] a = { 1,2, 3, 4, 5 };
a = (intD) goodCopyOf(a, 10);
5.7.6 调用任意方法
invoke方法
-
在 Method 类中有一个 invoke 方法, 它允许调用包装在当前 Method对象中的方法。
-
invoke 方法的签名是:Object invoke(Object obj, Object… args)。
-
第一个参数是隐式参数, 其余的对象提供了显式参数(在 Java SE 5.0 以前的版本中,必须传递一个对象数组, 如果没有显式参数就传递一个 null )。
-
对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null。
String n = (String) ml.invoke(harry);
-
如果返回类型是基本类型, invoke 方法会返回其包装器类型。
double s = (Double) m2,invoke(harry);
获得指定的方法
- 有可能存在若干个相同名字的方法,因此要格外小心,以确保能够准确地得到想要的那个方法。
- 有鉴于此,还必须提供想要的方法的参数类型。getMethod 的签名是:Method getMethod(String name, Class… parameterTypes)。
Math.class.getMethod("sqrt",double.class);
double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx) {
double y = (Double) f.invoke(null, x);
System.out.printf("X10.4f | %10.4f%n", x, y);
}
5.8 继承的设计技巧
-
将公共操作和域放在超类
-
不要使用受保护的域
原因主要有两点:
第一,子类集合是无限制的, 任何一个人都能够由某个类派生一个子类,并编写代码以直接访问 protected 的实例域,从而破坏了封装性。
第二, 在 Java 程序设计语言中,在同一个包中的所有类都可以访问 proteced 域,而不管它是否为这个类的子类。不过,protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。
-
使用继承实现“ is-a” 关系
-
除非所有继承的方法都有意义,否则不要使用继承
-
在覆盖方法时,不要改变预期的行为
关键在于, 在覆盖子类中的方法时,不要偏离最初的设计想法。
-
使用多态, 而非类型信息
使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
-
不要过多地使用反射
反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。