Java核心技术卷阅读随笔--第5章【继承】

继承

  第 4 章主要阐述了类和对象的概念, 本章将学习面向对象程序设计的另外一个基本概念: 继承(inheritance)。利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域, 以满足新的需求。这是 Java 程序设计中的一项核心技术。

  另外,本章还阐述了反射( reflection) 的概念。反射是指在程序运行期间发现更多的类及其属性的能力。这是一个功能强大的特性,使用起来也比较复杂。由于主要是开发软件工具的人员, 而不是编写应用程序的人员对这项功能感兴趣, 因此对于这部分内容,可以先浏览一下,待日后再返回来学习。

5.1 类、超类和子类

  现在让我们重新回忆一下在前一章中讨论的 Employee 类。假设你在某个公司工作,这 个公司中经理的待遇与普通雇员的待遇存在着一些差异。不过, 他们之间也存在着很多相同 的地方,例如, 他们都领取薪水。只是普通雇员在完成本职任务之后仅领取薪水, 而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承。这是因为需要为经理定义 一个新类 Manager, 以便增加一些新功能。但可以重用 Employee 类中已经编写的部分代码, 并将其中的所有域保留下来。从理论上讲, 在 Manager 与 Employee 之间存在着明显的“ isa”(是)关系,每个经理都是一名雇员:“ is-a” 关系是继承的一个明显特征

  5.1.1 定义子类

    下面是由继承 Employee 类来定义 Manager 类的格式, 关键字 extends 表示继承。

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

    C++ 注释:Java 与 C++ 定义继承类的方式十分相似。Java 用关键字 extends 代替了 C++ 中的冒号(:)。在 Java 中, 所有的继承都是公有继承, 而没有 C++ 中的私有继承和保 护继承 .

    关键字 extends 表明正在构造的新类派生于一个已存在的类。 已存在的类称为超类 ( superclass)、 基类( base class) 或父类(parent class); 新类称为子类(subclass、) 派生类 ( derived class) 或孩子类(child class)。 超类和子类是 Java 程序员最常用的两个术语,而了解 其他语言的程序员可能更加偏爱使用父类和孩子类,这些都是继承时使用的术语。 尽管 Employee 类是一个超类,但并不是因为它优于子类或者拥有比子类更多的功能。 实际上恰恰相反,子类比超类拥有的功能更加丰富。 例如, 读过 Manager 类的源代码之后就 会发现, Manageir类比超类 Employee 封装了更多的数据, 拥有更多的功能,

    注释: 前缀“ 超” 和“ 子” 来源于计算机科学和数学理论中的集合语言的术语。所有雇员组成的集合包含所有经理组成的集合。可以这样说, 雇员集合是经理集合的超集, 也可以说,经理集合是雇员集合的子集。

    在 Manager 类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:

public class Manager extends Employee
{
  private double bonus;
  ...   
public void setBonos(double bonus)   {     this.bonus = bonus;   } }

    这里定义的方法和域并没有什么特别之处。 如果有一个 Manager 对象, 就可以使用 setBonus 方法。

Manager boss = . . .;
boss.setBonus(5000);

    当然, 由于 setBonus 方法不是在 Employee 类中定义的,所以属于 Employee 类的对象不能使用它。

    然而, 尽管在 Manager 类中没有显式地定义 getName 和 getHireDay 等方法, 但属于 Manager 类的对象却可以使用它们,这是因为 Manager 类自动地继承了超类 Employee 中的 这些方法。

    同样, 从超类中还继承了 name、 salary 和 hireDay 这 3 个域。这样一来, 每个 Manager 类对象就包含了 4 个域:name、 salary、hireDay 和 bonus。     在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中, 而将具有特殊用途的方法放在子类中,这种将通用的 功能放到超类的做法,在面向对象程序设计中十分普遍。

  5.1.2 覆盖方法

    然而, 超类中的有些方法对子类 Manager 并不一定适用。具体来说, Manager 类中的 getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override) 超类中的这个方法:

public class Manager extends Employee
{
  ...   
public double getSalary()
  {
    ...
  }
  ...
}

    应该如何实现这个方法呢? 乍看起来似乎很简单, 只要返回 salary 和 bonus 域的总和就 可以了:

public double getSalary()
{
  return salary + bonus; // won't work
}

    然而,这个方法并不能运行。这是因为 Manager 类的 getSalary 方法不能够直接地访问超类的私有域。也就是说,尽管每个 Manager 对象都拥有一个名为 salary 的域, 但在 Manager 类的 getSalary方法中并不能够直接地访问 salary 域。只有 Employee 类的方法才能够访问私有部分。如果 Manager 类的方法一定要访问私有域, 就必须借助于公有的接口, Employee 类中的 公有方法 getSalary 正是这样一个接口。

    现在,再试一下。将对 salary 域的访问替换成调用 getSalary 方法。

public double getSalary()
{
  double baseSalary = getSalary();// still won't work
  return baseSalary + bonus;
}

    上面这段代码仍然不能运行。问题出现在调用 getSalary 的语句上,这是因为 Manager 类 也有一个 getSalary方法(就是正在实现的这个方法,) 所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。

    这里需要指出:我们希望调用超类 Employee 中的 getSalary 方法, 而不是当前类的这个方法。为此, 可以使用特定的关键字 super 解决这个问题:

super.getSalary()

    上述语句调用的是 Employee 类中的 getSalary 方法。下面是 Manager 类中 getSalary 方法的正 确书写格式:

public double getSalary()
{
  double baseSalary = super.getSalary();
  return baseSalary + bonus;
}

    注释: 有些人认为 super 与 this 引用是类似的概念, 实际上,这样比较并不太恰当。这是因为 super 不是一个对象的引用, 不能将 super 赋给另一个对象变量, 它只是一个指示编译器调用超类方法的特殊关键字。

    正像前面所看到的那样, 在子类中可以增加域、 增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法

    C++ 注释: 在 Java 中使用关键字 super 调用超类的方法,而在 C++ 中则采用超类名加 上::操作符的形式。例如, 在 Manager 类的 getSalary 方法中,应该将 super.getSalary 替 换为 Employee::getSalary

  5.1.3 子类构造器

    在例子的最后,我们来提供一个构造器。

public Manager(String name, double salary, int year, int month, int day)
{
  super(name, salary, year, month, day);
  bonus = 0;
}

    这里的关键字 super 具有不同的含义。语句

super(n, s, year, month, day);

    是“ 调用超类 Employee 中含有 n、s、year month 和 day 参数的构造器” 的简写形式。

    由于 Manager 类的构造器不能访问 Employee 类的私有域, 所以必须利用 Employee 类 的构造器对这部分私有域进行初始化,我们可以通过 super 实现对超类构造器的调用。使用 super 调用构造器的语句必须是子类构造器的第一条语句。

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

    注释: 回忆一下, 关键字 this 有两个用途: 一是引用隐式参数二是调用该类其他的构造器 , 同样,super 关键字也有两个用途:一是调用超类的方法二是调用超类的构造器。 在调用构造器的时候,这两个关键字的使用方式很相似。调用构造器的语句只能作为另 一个构造器的第一条语句出现构造参数既可以传递给本类( this) 的其他构造器也可以传递给超类(super ) 的构造器

     C++ 注释:在 C++ 的构造函数中, 使用初始化列表语法调用超类的构造函数, 而不调用 super。在 C++ 中, Manager 的构造函数如下所示:

Manager::Manager(String name , double salary, int year, int month, int day) // C++
: Employee(name, salary, year, month , day)
{
  bonus = 0;
}

    重新定义 Manager 对象的 getSalary 方法之后, 奖金就会自动地添加到经理的薪水中。

     下面给出一个例子,其功能为创建一个新经理,并设置他的奖金:

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.println (e.getName() + " " + e.getSalary());

    运行这条循环语句将会输出下列数据:

Carl Cracker 85000.0
Harry Hacker 50000.0
Tommy Tester 40000.0

    这里的 staff[l] 和 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 ) 可以指示多种实际类型的现象被称为多态( polymorphism)在运行时能够自动地选择调用哪个方法的现象称为动态绑定( dynamic binding) 。在本章中将详细地讨论这两个概念。

     C++ 注释: 在 Java 中, 不需要将方法声明为虚拟方法。动态绑定是默认的处理方式如果不希望让一个方法具有虚拟特征, 可以将它标记为 final (本章稍后将介绍关键字 final)。

  5.1.4 继承层次

    继承并不仅限于一个层次。 例如, 可以由 Manager 类派生 Executive 类。由一个公共超类派生出来的所有类的集合被称为继承层次( inheritance hierarchy ), 如图 5-1 所示。在继承 层次中, 从某个特定的类到其祖先的路径被称为该类的继承链 ( inheritance chain) 。

    通常, 一个祖先类可以拥有多个子孙 继承链。 例如, 可以由 Employee 类派生 出子类 Programmer 或 Secretary, 它 们 与 Manager 类没有任何关系(有可能它们彼此 之间也没有任何关系)。必要的话,可以将 这个过程一直延续下去。

     C++ 注释:Java 不支持多继承。有关 Java 中多继承功能的实现方式, 请参 看下一章 6.1 节有关接口的讨论。

  5.1.5 多态

    有一个用来判断是否应该设计为继承关系的简单规则, 这就是“ is-a” 规则, 它 表明子类的每个对象也是超类的对象。 例如, 每个经理都是雇员, 因此, 将 Manager 类设计为 Employee 类的子类是显而易见的,反之不然, 并不是每一名雇员都是经理。

    “ is-a” 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。

    例如, 可以将一个子类的对象赋给超类变量。

Employee e;
e = new Employee(. . .); // Employee object expected
e = new Manager(. . .); // OK, Manager can be used as well

    在 Java 程序设计语言中,对象变量是多态的。 一个 Employee 变量既可以引用一个 Employee 类对象, 也可以引用一个 Employee 类的任何一个子类的对象(例如, Manager、 Executive、Secretary 等)。

    从程序清单 5-1 中, 已经看到了置换法则的优点:

Manager boss = new Manager(. . .);
Employee[] staff = new Employee[3];
staff[0] = boss;

    在这个例子中,变量 staffl[0] 与 boss 引用同一个对象。但编译器将 staff[0]看成 Employee 对象。

    这意味着, 可以这样调用

boss.setBonus(5000); // OK

    但不能这样调用

staff[0].setBonus(5000); // Error

    这是因为 staff[0] 声明的类型是 Employee, 而 setBouns不是 Employee 类的方法。

    然而,不能将一个超类的引用赋给子类变量。例如,下面的赋值是非法的

Manager m = staff[i]; // Error

    原因很清楚:不是所有的雇员都是经理。 如果赋值成功,m 有可能引用了一个不是经理的 Employee 对象, 当在后面调用 m.setBonus(...) 时就有可能发生运行时错误。

    警告: 在 Java 中,子类数组的引用可以转换成超类数组的引用, 而不需要采用强制类型转换。例如, 下面是一个经理数组

Manager[] managers = new Manager[10];

    将它转换成 Employee[] 数组完全是合法的:

Employee[] staff = managers; // OK

    这样做肯定不会有问题, 请思考一下其中的缘由。 毕竟, 如果 manager[i] 是一个 Manager, 也一定是一个 Employee。然而, 实际上,将会发生一些令人惊讶的事情。要 切记 managers 和 staff 引用的是同一个数组。现在看一下这条语句:

staff[0] = new Employee("Harry Hacker", . . .);

    编译器竟然接纳了这个赋值操作。但在这里, staff[0] 与 manager[0] 引用的是同一个对象, 似乎我们把一个普通雇员擅自归入经理行列中了。这是一种很忌伟发生的情形, 当调用 managers[0].setBonus(1000) 的时候, 将会导致调用一个不存在的实例域, 进而搅 乱相邻存储空间的内容。

    为了确保不发生这类错误, 所有数组都要牢记创建它们的元素类型, 并负责监督仅 将类型兼容的引用存储到数组中。例如, 使用 new managers[10] 创建的数组是一个经理 数组。

    如果试图存储一个 Employee 类型的引用就会引发 ArrayStoreException 异常。

  5.1.6 理解方法调用

    弄清楚如何在对象上应用方法调用非常重要。下面假设要调用 x.f(args), 隐式参数 x 声明为类 C 的一个对象。下面是调用过程的详细描述:

    1 ) 编译器査看对象的声明类型和方法名。假设调用 x.f(param), 且隐式参数 x 声明为 C 类的对象。需要注意的是:有可能存在多个名字为 f, 但参数类型不一样的方法。例如,可 能存在方法 f(int) 和方法 (String) 。编译器将会一一列举所有 C 类中名为 f 的方法和其超类中 访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。

    至此, 编译器已获得所有可能被调用的候选方法

    2 ) 接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在 一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析( overloading resolution)。例如,对于调用 x.f(“ Hello” )来说, 编译器将会挑选 f(String),而不是 f(int)。由于允许类型转换( int 可以转换成 double, Manager 可以转换成 Employee, 等等,) 所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法, 或者发现经过类型转换后有多个方法与之匹配, 就会报告一个错误。  

    至此, 编译器已获得需要调用的方法名字和参数类型

    注释: 前面曾经说过, 法的名字和参数列表称为方法的签名。例如, f(int) 和 f(String) 是两个具有相同名字, 不同签名的方法。如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。

    不过,返回类型不是签名的一部分, 因此,在覆盖方法时, 一定要保证返回类型 的兼容性。 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。例如, 假设Employee 类有

public Employee getBuddy() { . . . }

    经理不会想找这种地位低下的员工。为了反映这一点, 在后面的子类 Manager 中, 可以按照如下所示的方式覆盖这个方法

public Manager getBuddy() { . . . } // OK to change return type

    我们说,这两个 getBuddy 方法具有可协变的返回类型。

    3 ) 如果是 private 方法、 static 方法、 final 方法(有关 final 修饰符的含义将在下一节讲 述)或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定( static binding )。 与此对应的是,调用的方法依赖于隐式参数的实际类型, 并且在运行时实现动态绑定。在我们列举的示例中, 编译器采用动态绑定的方式生成一条调用 f (String) 的指令。

    4 ) 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了 方法 f(String) ,就直接调用它;否则, 将在 D 类的超类中寻找 f(String) ,以此类推。

    每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表( method table), 其中列出了所有方法的签名和实际调用的方法。这样一来,在真正 调用方法的时候, 虚拟机仅查找这个表就行了。在前面的例子中, 虚拟机搜索 D 类的方法表, 以便寻找与调用 f(Sting) 相匹配的方法。这个方法既有可能是 D.f(String), 也有可能是 X.f(String), 这里的 X 是 D 的超类。这里需要提醒一点,如果调用 super.f(param), 编译器将 对隐式参数超类的方法表进行搜索。

    现在,查看一下程序清单 5-1 中调用 e.getSalary() 的详细过程。e 声明为 Employee 类型。 Employee 类只有一个名叫 getSalary 的方法, 这个方法没有参数。 因此, 在这里不必担心重载解析的问题。

    由于 getSalary 不是 private 方法、 static 方法或 final 方法,所以将采用动态绑定。虚拟机 为 Employee 和 Manager 两个类生成方法表。在 Employee 的方法表中, 列出了这个类定义的所有方法:

Employee:
getName()-> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)

    在运行时, 调用 e.getSalary() 的解析过程为:

    1 ) 首先,虚拟机提取 e 的实际类型的方法表。既可能是 Employee、 Manager 的方法表, 也可能是 Employee 类的其他子类的方法表。

    2 ) 接下来, 虚拟机搜索定义 getSalary 签名的类。此时,虚拟机已经知道应该调用哪个方法。

    3 ) 最后,虚拟机调用方法。

    动态绑定有一个非常重要的特性: 无需对现存的代码进行修改,就可以对程序进行扩展。 假设增加一个新类 Executive, 并且变量 e 有可能引用这个类的对象, 我们不需要对包含调用 e.getSalary() 的代码进行重新编译。如果 e 恰好引用一个 Executive 类的对象,就会自动地调 用 Executive.getSalary() 方法。

    警告:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是, 如果超类方法是 public, 子类方法一定要声明为 public。经常会发生这类错误:在声明子类方法的时 候, 遗漏了 public 修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限。

  5.1.7 阻止继承:final 类和方法

    有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类。如果 在定义类的时候使用了 final 修饰符就表明这个类是 final 类。例如, 假设希望阻止人们定义 Executive类的子类,就可以在定义这个类的时候 ,使用 final 修饰符声明。声明格式如下所示:

public final class Executive extends Manager
{
...
}

    类中的特定方法也可以被声明为 final。如果这样做,子类就不能覆盖这个方法final 类中的所有方法自动地成为 final 方法 ) 。例如

public class Employee
{
  ...
  public final String getName()
  {
    return name;
  }
  ...
}

    注释:前面曾经说过, 域也可以被声明为 final。 对于 final 域来说,构造对象之后就不允许改变它们的值了。不过, 如果将一个类声明为 final, 只有其中的方法自动地成为 final, 而不包括域

     将方法或类声明为 final 主要目的是: 确保它们不会在子类中改变语义。例如, Calendar 类中的 getTime 和 setTime 方法都声明为 final。这表明 Calendar 类的设计者负责实现 Date 类 与日历状态之间的转换, 而不允许子类处理这些问题。同样地, String 类也是 final 类,这意味着不允许任何人定义 String 的子类。换言之,如果有一个 String 的引用, 它引用的一定是 一个 String 对象, 而不可能是其他类的对象。

     有些程序员认为: 除非有足够的理由使用多态性, 应该将所有的方法都声明为 final。事 实上,在 C++ 和 C# 中, 如果没有特别地说明, 所有的方法都不具有多态性。这两种做法可 能都有些偏激。我们提倡在设计类层次时, 仔细地思考应该将哪些方法和类声明为 finaL

     在早期的 Java 中,有些程序员为了避免动态绑定带来的系统开销而使用 final 关键字。 如果一个方法没有被覆盖并且很短, 编译器就能够对它进行优化处理, 这个过程为称为内联 ( inlining )。例如,内联调用 e.getName( ) 将被替换为访问 e.name 域。这是一项很有意义的改进, 这是由于 CPU 在处理调用方法的指令时, 使用的分支转移会扰乱预取指令的策略, 所 以,这被视为不受欢迎的。然而,如果 getName 在另外一个类中被覆盖, 那么编译器就无法 知道覆盖的代码将会做什么操作,因此也就不能对它进行内联处理了。

     幸运的是, 虚拟机中的即时编译器比传统编译器的处理能力强得多。这种编译器可以准 确地知道类之间的继承关系, 并能够检测出类中是否真正地存在覆盖给定的方法。如果方法 很简短、 被频繁调用且没有真正地被覆盖, 那么即时编译器就会将这个方法进行内联处理。 如果虚拟机加载了另外一个子类,而在这个子类中包含了对内联方法的覆盖, 那么将会发生什么情况呢? 优化器将取消对覆盖方法的内联。这个过程很慢, 但却很少发生。

  5.1.8 强制类型转换

    第 3 章曾经讲过,将一个类型强制转换成另外一个类型的过程被称为类型转换。Java 程 序设计语言提供了一种专门用于进行类型转换的表示法。例如:

double x = 3.405;
int nx = (int) x ;

    将表达式 x 的值转换成整数类型, 舍弃了小数部分。

    正像有时候需要将浮点型数值转换成整型数值一样,有时候也可能需要将某个类的对象 引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似, 仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。例如:

Manager boss = (Manager) staff[0]:

    进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。 例如, 在 managerTest 类中,由于某些项是普通雇员, 所以 staff 数组必须是 Employee 对象 的数组。我们需要将数组中引用经理的元素复原成 Manager 类, 以便能够访问新增加的所有 变量(需要注意, 在前面的示例代码中, 为了避免类型转换, 我们做了一些特别的处理, 即 将 boss 变量存入数组之前,先用 Manager 对象对它进行初始化。而为了设置经理的奖金, 必须使用正确的类型)。

    大家知道,在 Java 中,每个对象变量都属于一个类型。类型描述了这个变量所引用的以 及能够引用的对象类型。例如,staff[i]引用一个 Employee 对象(因此它还可以引用 Manager 对象)。

    将一个值存入变量时, 编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量, 编译器是允许的。但将一个超类的引用赋给一个子类变量, 必须进行类型转换, 这样才能够通过运行时的检査。

    如果试图在继承链上进行向下的类型转换,并且“ 谎报” 有关对象包含的内容,会发生 什么情况呢?

Manager boss = (Manager) staff[1]; // Error

    运行这个程序时, Java 运行时系统将报告这个错误,并产生一个 ClassCastException 异常。 如果没有捕获这个异常,那么程序就会终止。因此,应该养成这样一个良好的程序 设计习惯:在进行类型转换之前,先查看一下是否能够成功地转换。这个过程简单地使用 instanceof 操作符就可以实现。 例如:

if (staff[1] instanceof Manager)
{
  boss = (Manager) staff[1]:
  ...
}

    最后, 如果这个类型转换不可能成功, 编译器就不会进行这个转换。例如,下面这个类 型转换:

String c = (String) staff[1];

    将会产生编译错误, 这是因为 String 不是 Employee 的子类。

    综上所述:

    •只能在继承层次内进行类型转换。

    •在将超类转换成子类之前,应该使用 instanceof进行检查。  

    注释: 如果 x 为 null , 进行下列测试

x instanceof C

    不会产生异常, 只是返回 false。之所以这样处理是因为 null 没有引用任何对象, 当 然也不会引用 C 类型的对象。

    实际上,通过类型转换调整对象的类型并不是一种好的做法。在我们列举的示例中, 大 多数情况并不需要将 Employee 对象转换成 Manager 对象, 两个类的对象都能够正确地调用 getSalary 方法,这是因为实现多态性的动态绑定机制能够自动地找到相应的方法。

    只有在使用 Manager 中特有的方法时才需要进行类型转换, 例如, setBonus 方法。如果 鉴于某种原因,发现需要通过 Employee 对象调用 setBonus 方法, 那么就应该检查一下超类的设计是否合理。重新设计一下超类,并添加 setBonus方法才是正确的选择。请记住,只要 没有捕获 ClassCastException 异常,程序就会终止执行。 在一般情况下,应该尽量少用类型转换和 instanceof 运算符。

    C++ 注释:Java 使用的类型转换语法来源于 C 语言“ 以往糟糕的日子”, 但处理过程却 有些像 C++ 的 dynamic_cast 操作。例如,

Manager boss = (Manager) staff[1]; // Java

    等价于

Manager* boss = dynamic_cast<Manager*>(staff[1]); // C++

    它们之间只有一点重要的区别: 当类型转换失败时, Java 不会生成一个 null 对象, 而是抛出一个异常。从这个意义上讲, 有点像 C++ 中的引用 ( reference) 转换。真是令 人生厌。在 C++ 中, 可以在一个操作中完成类型测试和类型转换。

Manager* boss = dynainic_ca.st<Manager*>(staff[1]).; // C++
if (boss != NULL) . . .

    而在 Java 中, 需要将 instanceof 运算符和类型转换组合起来使用:

if (staff[1] instanceof Manager)
{
Manager boss = (Manager) staff[1];
...
}

  5.1.9 抽象类

    如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加 抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。例如, 考虑一下对 Employee 类层次的扩展。一名雇员是一个人, 一名 学生也是一个人。下面将类 Person 和类 Student 添加到类的层次结构中。图 5-2 是这三个类 之间的关系层次图。

    为什么要花费精力进行这样高层次的抽象呢? 每个人都有一些诸如姓名这样的属性。学生与雇员都有姓名属性, 因此可以将 getName 方法放置在位于继承关系较高层次的通用超类中。

    现在, 再增加一个 getDescription 方法,它可以返回对一个人的简短描述。例如:

an employee with a salary of $50,000.00
a student majoring in computer science

    在 Employee 类和 Student 类 中 实 现 这个方法很容易。 但是在 Person类中 应该提供什么内容呢? 除了姓名之外, Person类一无所知。当然, 可以让 Person. getDescription() 返回一个空字符串。然而, 还有一个更好的方法, 就是使用 abstract 关 键字,这样就完全不需要实现这个方法了。

     

public abstract String getDescription();
// no implementation required

    为了提高程序的清晰度, 包含一个或多个抽象方法的类本身必须被声明为抽象的。

public abstract class Person
{
  ...
  public abstract String getDescription();
}

    除了抽象方法之外,抽象类还可以包含具体数据和具体方法。例如, Person 类还保存着 姓名和一个返回姓名的具体方法。

public abstract class Person
{
  private String name;
  public Person(String name)
  {
    this.name = name ;
  }
  public abstract String getDescription();

  public String getName()
  {
    return name;
  }
}

    提示:许多程序员认为,在抽象类中不能包含具体方法。建议尽量将通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象类)中。

    抽象方法充当着占位的角色, 它们的具体实现在子类中

    扩展抽象类可以有两种选择。:

      一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽 象类

      另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了

    例如, 通过扩展抽象 Person 类, 并实现 getDescription方法来定义 Student 类。由于在Student 类中不再含有抽象方法,所以不必将这个类声明为抽象的。

    类即使不含抽象方法,也可以将类声明为抽象类。

    抽象类不能被实例化。也就是说,如果将一个类声明为 abstract, 就不能创建这个类的对象。例如,表达式

new Person("yjk")

    是错误的, 但可以创建一个具体子类的对象。

    需要注意, 可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象。 例如,

Person p = new Student("yjk" , "yjk2");

    这里的 p 是一个抽象类 Person 的变量,Person 引用了一个非抽象子类 Student 的实例。

    C++ 注释:在 C++中, 有一种在尾部用 =0 标记的抽象方法, 称为纯虚函数, 例如:

class Person // C++
{
public:
  virtual string getDescription() = 0;
...
};

    只要有一个纯虚函数,这个类就是抽象类。在 C++ 中, 没有提供用于表示抽象类的 特殊关键字。

    下面定义一个扩展抽象类 Person 的具体子类 Student:

public class Student extends Person
{
  private String major;
  public Student(String name , String major)
  {
    super(name);
    this.major = major;
  }  
  public String getDescription()
  {
    return "a student majoring in " + major;
  }
}

    在 Student 类中定义了 getDescription 方法。因此,在 Student 类中的全部方法都是非抽 象的, 这个类不再是抽象类。

    在程序清单 5-4的程序中定义了抽象超类 Person(程序清单 5-5 ) 和两个具体子类 Employee(程 序清单 5-6 ) 和 Student (程序清单 5-7。) 下面将雇员和学生对象填充到 Person 引用数组。

Person[] people = new Person[2];
people[0] = new Enp1oyee(. . .);
people[1] = new Student(. . .);

    然后, 输出这些对象的姓名和信息描述:

for (Person p : people)
  System.out.println(p.getName () + ", " + p.getDescription());

    有些人可能对下面这个调用感到困惑:

p .getDescription()

    这不是调用了一个没有定义的方法吗? 请牢记,由于不能构造抽象类 Person 的对象, 所以变量 p 永远不会引用 Person 对象, 而是引用诸如 Employee 或 Student 这样的具体子类对象, 而这些对象中都定义了 getDescription 方法。

    是否可以省略 Person 超类中的抽象方法, 而仅在 Employee 和 Student 子类中定义 getDescription方法呢? 如果这样的话,就不能通过变量 p 调用 getDescription方法了。编译器只允许调用在类中声明的方法。

    在 Java 程序设计语言中,抽象方法是一个重要的概念。在接口(interface) 中将会看到 更多的抽象方法。有关接口的详细介绍请参看第 6 章。

     C++ 注释:事实上,Java 中的受保护部分对所有子类及同一个包中的所有其他类都可见。 这与 c++ 中的保护机制稍有不同, Java 中的 protected 概念要比 C++ 中的安全性差。 下面归纳一下 Java 用于控制可见性的 4 个访问修饰符:

     下面归纳一下 Java 用于控制可见性的 4 个访问修饰符:

    1 ) 仅对本类可见 private。

    2 ) 对所有类可见 public。

    3 ) 对本包和所有子类可见 protected。

    4 ) 对本包可见—默认(很遗憾) ,不需要修饰符。

5.2 Object: 所有类的超类

  Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。但是并不需 要这样写:

public class Employee extends Object

  如果没有明确地指出超类,Object 就被认为是这个类的超类。由于在 Java中,每个类都 是由 Object 类扩展而来的,所以, 熟悉这个类提供的所有服务十分重要。本章将介绍一些基本的内容, 没有提到的部分请参看后面的章节或在线文档(在 Object 中有几个只在处理线程 时才会被调用的方法,有关线程内容请参见第 14 章)。  

  可以使用 Object 类型的变量引用任何类型的对象:

Object obj = new Employee("Harry Hacker", 35000);

  当然, Object 类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的 操作, 还需要清楚对象的原始类型, 并进行相应的类型转换:

Employee e = (Employee) obj ;

  在 Java 中,只有基本类型 ( primitive types) 不是对象, 例如,数值、 字符和布尔类型的 值都不是对象。

  所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类

Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK

  C++ 注释:在 C++ 中没有所有类的根类,不过,每个指针都可以转换成 void* 指针。

  5.2.1 equals 方法

    Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这 个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用, 它们一定是相 等的。从这点上看,将其作为默认操作也是合乎情理的。然而,对于多数类来说, 这种判断 并没有什么意义。例如, 采用这种方式比较两个 PrintStream 对象是否相等就完全没有意义。 然而, 经常需要检测两个对象状态的相等性,如果两个对象的状态相等, 就认为这两个对象是相等的。

    例如, 如果两个雇员对象的姓名、 薪水和雇佣日期都一样, 就认为它们是相等的(在实 际的雇员数据库中,比较 ID 更有意义。利用下面这个示例演示 equals 方法的实现机制)。

    getClass 方法将返回一个对象所属的类,有关这个方法的详细内容稍后进行介绍。在检 测中, 只有在两个对象属于同一个类时, 才有可能相等。

    提示: 为了防备 name 或 hireDay 可能为 null 的情况, 需要使用 Objects.equals 方法。如 果两个参数都为 null, Objects.equals(a,b) 调用将返回 true ; 如果其中一个参数为 null , 则返回 false ; 否则, 如果两个参数都不为 null, 则调用 a.equals(b)。 利用这个方法, Employee.equals 方法的最后一条语句要改写为:

return Objects.equals(name , other.name)&& salary == other.salary&& Object.equals(hireDay, other.hireDay);

    在子类中定义 equals 方法时, 首先调用超类的 equals。如果检测失败, 对象就不可能相 等。如果超类中的域都相等, 就需要比较子类中的实例域。

public 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;
  }
}

  5.2.2 相等测试与继承

    如果隐式和显式的参数不属于同一个类, equals 方法将如何处理呢? 这是一个很有争议的问题。 在前面的例子中, 如果发现类不匹配, equals 方法就返冋 false: 但是, 许多程序员却喜欢使用 instanceof 进行检测:

if ( !(otherObject instanceof Employee)) return false;

    这样做不但没有解决 otherObject 是子类的情况,并且还有可能会招致一些麻烦。这就是建议不要使用这种处理方式的原因所在。Java 语言规范要求 equals 方法具有下面的特性:

    1 ) 自反性:对于任何非空引用 x, x.equals(x) 应该返回 true。

    2 ) 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true , x.equals(y) 也应该返 回 true。

    3 ) 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返回true, y.equals(z) 返回 true, x.equals(z) 也应该返回 true。

    4 ) 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果。

    5 ) 对于任意非空引用 x, x.equals(null) 应该返回 false。

    这些规则十分合乎情理,从而避免了类库实现者在数据结构中定位一个元素时还要考虑 调用 x.equals(y), 还是调用 y.equals(x) 的问题。

    然而, 就对称性来说, 当参数不属于同一个类的时候需要仔细地思考一下。请看下面这 个调用:

e. equals(m)

    这里的 e 是一个 Employee 对象, m 是一个 Manager 对象, 并且两个对象具有相同的姓名、 薪水和雇佣日期。 如果在 Employee.equals 中用 instanceof 进行检测, 则返回 true。然而这意味着反过来调用:

m .equals(e)

    也需要返回 true、 对称性不允许这个方法调用返回 false,或者抛出异常:

    这就使得 Manager 类受到了束缚。 这个类的 equals 方法必须能够用自己与任何一 个 Employee 对象进行比较, 而不考虑经理拥有的那部分特有信息! 猛然间会让人感觉 instanceof 测试并不是完美无瑕。

    某些书的作者认为不应该利用 getClass 检测, 因为这样不符合置换原则有一个应用 AbstractSet 类的 equals 方法的典型例子,它将检测两个集合是否有相同的元素。AbstractSet 类有两个具体子类: TreeSet 和 HashSet, 它们分别使用不同的算法实现查找集合元素的操作。 无论集合采用何种方式实现,都需要拥有对任意两个集合进行比较的功能。

    然而, 集合是相当特殊的一个例子, 应该将 AbstractSetequals 声明为 final , 这是因为没有任何一个子类需要重定义集合是否相等的语义(事实上,这个方法并没有被声明为 final。 这样做, 可以让子类选择更加有效的算法对集合进行是否相等的检测)

    下面可以从两个截然不同的情况看一下这个问题:

    • 如果子类能够拥有自己的相等概念, 则对称性需求将强制采用 getClass 进行检测。

    • 如果由超类决定相等的概念,那么就可以使用 intanceof进行检测, 这样可以在不同子类的对象之间进行相等的比较。

    在雇员和经理的例子中, 只要对应的域相等, 就认为两个对象相等。如果两个 Manager 对象所对应的姓名、 薪水和雇佣日期均相等, 而奖金不相等, 就认为它们是不相同的, 因 此,可以使用 getClass 检测。

    注 释: 在 标 准 Java 库 中 包 含 1 5 0 多 个 equals 方 法 的 实 现, 包 括 使 用 instanceof 检 测、 调用 getClass 检测、 捕获 ClassCastException 或 者 什 么 也 不 做。 可 以 查 看 java.sql. Timestamp 类的 API 文档, 在这里实现人员不无尴尬地指出,他们使自己陷入了困境。 Timestamp 类继承自 java.util.Date, 而 后 者 的 equals 方 法 使 用 了 一 个 instanceof 测试,这 样一来就无法覆盖实现 equals 使之同时做到对称且正确。

    下面给出编写一个完美的 equals 方法的建议:

    1 ) 显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 的变量。

    2 ) 检测 this 与 otherObject 是否引用同一个对象:

if (this = otherObject) return true;

    这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一 个一个地比较类中的域所付出的代价小得多。

    3 ) 检测 otherObject 是否为 null, 如 果 为 null, 返 回 false。这项检测是很必要的。

    4 ) 比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:

if (getClass() != otherObject.getClass()) return false;

    5 ) 将 otherObject 转换为相应的类类型变量:

ClassName other = (ClassName) otherObject

    6 ) 现在开始对所有需要比较的域进行比较了。使用 =比较基本类型域使用 equals 比 较对象域。如果所有的域都匹配, 就返回 true; 否 则 返 回 false。

return fieldl == other.field1
&& Objects.equa1s(fie1d2, other.field2)
&& ...;

    如果在子类中重新定义 equals, 就要在其中包含调用 super.equals(other)。

    提示:对于数组类型的域, 可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。

     警告: 下面是实现 equals 方法的一种常见的错误。 可以找到其中的问题吗?

public class Employee
{
  public boolean equals(Employee other)
  {
    return other != null
      && getClass() == other.getClass()
      && Objects.equals(name , other.name)
      && salary== other,salary
      && Objects.equals(hireDay, other.hireDay);
  }
  ...
}

    这个方法声明的显式参数类型是 Employee。其结果并没有覆盖 Object 类的 equals 方 法, 而是定义了一个完全无关的方法。

    为了避免发生类型错误, 可以使用 @Override 对覆盖超类的方法进行标记:

©Override public boolean equals(Object other)

    如果出现了错误, 并且正在定义一个新方法, 编译器就会给出错误报告。例如, 假 设将下面的声明添加到 Employee 类中:

@Override public boolean equals(Employee other)

    就会看到一个错误报告, 这是因为这个方法并没有覆盖超类 Object 中的任何方法。

     API java.util.Arrays 1.2

     • static Boolean equals(type[]a , type[] b)5.0

    如果两个数组长度相同, 并且在对应的位置上数据元素也均相同, 将返回 true。数组 的元素类型可以是 Object、 int、 long、 short、 char、 byte、 boolean、 float 或 double。

    API java.util.Objects 7

    • static boolean equals(Object a, Object b)

    如果 a 和 b 都为 null, 返回 true ; 如果只有其中之一为 null, 则返回 false ; 否 则 返 回 a.equals(b)。

  5.2.3 hashCode 方法

    散列码( hash code ) 是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是 两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同。 在表 5- 1 中列出 T 几个通 过调用 String 类的 hashCode 方法得到的散列码。

    String 类使用下列算法计算散列码:

int hash = 0;
for (int i = 0; i < length0;i++)
  hash = 31 * hash + charAt(i);

    由于 hashCode方法定义在 Object 类中因此每个对象都有一个默认的散列码,其值为对象的存储地址。来看下面这个例子。

String s = "Ok";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("Ok");
StringBuilder tb = new StringBuilder(t);
System.out.println(t.hashCode() + " "+ tb.hashCode());

    表 5-2 列出了结果。

     请注意, 字符串 s 与 t 拥有相同的散列码, 这是因为字符串的散列码是由内容导出 的。而字符串缓冲 sb 与 tb却有着不同的散列码, 这是因为在 StringBuffer 类中没有定义 hashCode 方法它的散列码是由 Object 类的默认 hashCode 方法导出的对象存储地址

    如果重新定义 equals方法,就必须重新定义 hashCode 方法, 以便用户可以将对象插入到散列表中(有关散列表的内容将在第 9 章中讨论)。

    hashCode 方法应该返回一个整型数值(也可以是负数,) 并合理地组合实例域的散列码, 以便能够让各个不同的对象产生的散列码更加均匀。

    例如, 下面是 Employee 类的 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 int hashCode()
{
  return 7 * Objects.hashCode(name)
    + 11 * Double.hashCode(salary)
    + 13 * Objects.hashCode(hireDay);
}

    还有更好的做法,需要组合多个散列值时,可以调用 Objects.hash 并提供多个参数。这 个方法会对各个参数调用 Objects.hashCode, 并组合这些散列值。这样 Employee.hashCode 方 法可以简单地写为:

public int hashCode()
{
  return Objects.hash(name, salary, hireDay);
}

    Equals 与 hashCode 的定义必须一致:如果 x.equals(y) 返回 true, 那么 x.hashCode( ) 就必 须与 y.hashCode( ) 具有相同的值。例如, 如果用定义的 Employee.equals 比较雇员的 ID,那 么 hashCode 方法就需要散列 ID,而不是雇员的姓名或存储地址。

    提示:如果存在数组类型的域, 那么可以使用静态的 Arrays.hashCode 方法计算一个散列码,这个散列码由数组元素的散列码组成。

    API java.util.Object 1.0

    • int hashCode( )

      返回对象的散列码。散列码可以是任意的整数, 包括正数或负数。两个相等的对象要求返回相等的散列码。

    API java.util.Objects 7

    • static int hash(Object . .. objects)

      返回一个散列码,由提供的所有对象的散列码组合而得到。

    • static int hashCode(Object a )

      如果 a 为 null 返回 0, 否则返回 a.hashCode() 。

    API java.lang.(lnteger|Long|Short|Byte|Double| Float|Character|Boolean) 1.0

    • static int hashCode((int|long|short|byte|double|float|char|boolean) value) 8

      返回给定值的散列码。

    API java.utii.Arrays 1.2

    • static int hashCode(type[] a ) 5.0

      计算数组 a 的散列码。组成这个数组的元素类型可以是 object,int,long, short, char, byte, boolean, float 或 double。

  5.2.4 toString 方法

    在 Object 中还有一个重要的方法, 就是 toString方法, 它用于返回表示对象值的字符串。下面是一个典型的例子。Point 类的 toString方法将返回下面这样的字符串:

java.awt.Point[x=10,y=20]

    绝大多数(但不是全部)的 toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。下面是 Employee 类中的 toString 方法的实现:

public String toString()
{
return "Employee[name=" + name
+ ".salary:
" + salary
+ ",hireDay=" + hireDay
+"]"
}

    实际上,还可以设计得更好一些。最好通过调用 getClass( ).getName( ) 获得类名的字符 串,而不要将类名硬加到 toString方法中。

public String toString()
{
return getClass().getName()
+ "[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}

    toString方法也可以供子类调用。

    当然,设计子类的程序员也应该定义自己的 toString 方法,并将子类域的描述添加进去。 如果超类使用了 getClass( ).getName( ), 那么子类只要调用 super.toString( )就可以了。例如, 下面是 Manager 类中的 toString 方法:

public class Manager extends Employee
{
...
  public String toString() 
  {
    
    return super.toString() + "[bonus=" + bonus + "]";
  }
}

    现在,Manager 对象将打印输出如下所示的内容:

Manager[name=...,salary=...,hireDay=...][bonus=...]

    随处可见 toString方法的主要原因是:只要对象与一个字符串通过操作符“ +” 连接起 来,Java 编译就会自动地调用 toString方法,以便获得这个对象的字符串描述。例如,

Point p = new Point(10, 20);
String message = "The current position is " + p;
// automatically invokes p.toString()

    提示:在调用 x.toString( ) 的地方可以用 ""+x 替代。这条语句将一个空串与 x 的字符串 表示相连接。这里的 x 就是 x.toString( ) 。与 toString 不同的是,如果 x 是基本类型,这 条语句照样能够执行。

    如果 x 是任意一个对象, 并调用

System.out.println(x);

    println方法就会直接地调用 x.toString( ) ,并打印输出得到的字符串

    Object 类定义了 toString 方法来打印输出对象所属的类名散列码。例如, 调用

System.out.println(System.out)

    将输出下列内容:

java.io.PrintStream@2f6684

    之所以得到这样的结果是因为 PrintStream 类的设计者没有覆盖 toString方法。

    警告: 令人烦恼的是, 数组继承了 object 类的 toString 方法,数组类型将按照旧的格式 打印。例如:

int[] luckyNumbers = { 2, 3, 5, 7 , ll , 13 } ;
String s = "" + luckyNumbers;

    生成字符串“ [I@la46e30 ” (前缀 [I 表明是一个整型数组)。修正的方式是调用静态方法 Arrays.toString。代码:

String s = Arrays.toString(luckyNumbers);

    将生成字符串“ [2,3,5,7,11,13]”。

    要想打印多维数组(即, 数组的数组)则需要调用 Arrays.deepToString 方法。

    toString方法是一种非常有用的调试工具。在标准类库中,许多类都定义了 toString方 法, 以便用户能够获得一些有关对象状态的必要信息。像下面这样显示调试信息非常有益:

System,out.println("Current position = " + position);

    读者在第 7 章中将可以看到,更好的解决方法是:

Logger.global.info("Current position = " + position);

    提示: 强烈建议为自定义的每一个类增加 toString 方法。这样做不仅自己受益, 而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

    API java.lang.Object 1.0

    • Class getClass( )

      返回包含对象信息的类对象。稍后会看到 Java 提供了类运行时的描述, 它的内容被封装在 Class 类中。

    • boolean equals(Object otherObject )

      比较两个对象是否相等, 如果两个对象指向同一块存储区域, 方法返回 true ; 否 则 方 法返回 false。在自定义的类中, 应该覆盖这个方法。

    • String toString( )

      返冋描述该对象值的字符串。在自定义的类中, 应该覆盖这个方法。

    API java.lang.Class 1.0

    • String getName( ) 返回这个类的名字。

    • Class getSuperclass( ) 以 Class 对象的形式返回这个类的超类信息。

5.3 泛型数组列表

  在许多程序设计语言中, 特别是在 C++ 语言中, 必须在编译时就确定整个数组的大小。 程序员对此十分反感, 因为这样做将迫使程序员做出一些不情愿的折中。例如,在一个部门 中有多少雇员? 肯定不会超过100 人。一旦出现一个拥有 150 名雇员的大型部门呢? 愿意为 那些仅有 10 名雇员的部门浪费 90 名雇员占据的存储空间吗?

  在 Java 中,情况就好多了。它允许在运行时确定数组的大小

int actualSize = . . .;
Employee[] staff = new Employee[actualSize];

  当然,这段代码并没有完全解决运行时动态更改数组的问题。一旦确定了数组的大小, 改 变它就不太容易了。在 Java 中, 解决这个问题最简单的方法是使用 Java 中另外一个被称为 ArrayList 的类。它使用起来有点像数组,但在添加或删除元素时, 具有自动调节数组容量的功能,而不需要为此编写任何代码。

  ArrayList 是一个采用类型参数( type parameter )泛型类( generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面, 例如,ArrayList<Employee> 。在第 8 章中将可以看到如何自定义一个泛型类, 这里并不需要了解任何技术细 节就可以使用 ArrayList 类型。

  下面声明和构造一个保存 Employee 对象的数组列表:

ArrayList<Employee> staff = new ArrayList<Eniployee>();

  两边都使用类型参数 Employee, 这有些繁琐。Java SE 7中, 可以省去右边的类型参数:

ArrayList<Employee> staff = new ArrayList<>();

  这被称为“ 菱形” 语法,因为空尖括号 <>就像是一个菱形。可以结合 new 操作符使用菱形语法。编译器会检查新值是什么。如果赋值给一个变量,或传递到某个方法,或者从某个方 法返回,编译器会检査这个变量、 参数或方法的泛型类型,然后将这个类型放在<>中。在 这个例子中,new ArrayListo()将赋至一个类型为 ArrayList 的变量, 所以泛型 类型为 Employee。

  注释:Java SE 5.0 以前的版本没有提供泛型类, 而是有一个 ArrayList 类, 其中保存类型 为 Object 的元素, 它是“ 自适应大小” 的集合。如果一定要使用老版本的 Java, 则需要 将所有的后缀 <. . .> 删掉。在 Java SE 5.0 以后的版本中,没有后缀 <...> 仍然可以使用 ArrayList, 它将被认为是一个删去了类型参數的“ 原始” 类型。

  注释: 在 Java 的老版本中, 程序员使用 Vector 类实现动态数组。 不过, ArrayList 类更加有效,没有任何理由一定要使用 Vector 类。

  使用 add 方法可以将元素添加到数组列表中。例如,下面展示了如何将雇员对象添加到 数组列表中的方法:

staff.add(new Employee("Harry Hacker", ...));
staff.add(new Eraployee("Tony Tester", . . .));

  数组列表管理着对象引用的一个内部数组。最终, 数组的全部空间有可能被用尽。这就 显现出数组列表的操作魅力: 如果调用 add 且内部数组已经满了,数组列表就将自动地创建 一个更大的数组并将所有的对象从较小的数组中拷贝到较大的数组中

  如果已经清楚或能够估计出数组可能存储的元素数量, 就可以在填充数组之前调用 ensureCapacity方法:

staff.ensureCapacity(lOO);

  这个方法调用将分配一个包含 100 个对象的内部数组。然后调用 100 次 add, 而不用重新分配空间。

  另外,还可以把初始容量传递给 ArrayList 构造器:

ArrayList<Employee> staff = new ArrayList<>(lOO);

  警告:分配数组列表, 如下所示:

new ArrayList<>(lOO) // capacity is 100

  它与为新数组分配空间有所不同:

new Employee[100] // size is 100

  数组列表的容量与数组的大小有一个非常重要的区别。如果为数组分配 100 个元素 的存储空间,数组就有 100 个空位置可以使用。 而容量为 100 个元素的数组列表只是拥有保存 100 个元素的潜力 ( 实际上, 重新分配空间的话,将会超过100 ), 但是在最初, 甚至完成初始化构造之后,数组列表根本就不含有任何元素。

  size方法将返回数组列表中包含的实际元素数目。例如,

staff,size()

  将返回 staff 数组列表的当前元素数量, 它等价于数组 a 的 a.length。

  一旦能够确认数组列表的大小不再发生变化,就可以调用 trimToSize方法。这个方法将 存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储 空间。

  一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确 认不会添加任何元素时, 再调用 trimToSize。

  C++ 注释:ArrayList 类似于 C++ 的 vector 模板。ArrayList 与 vector 都是泛型类型。 但 是 C++ 的 vector 模板为了便于访问元素重载了 [ ] 运算符。由于 Java 没有运算符重载, 所以必须调用显式的方法。此外,C++ 向量是值拷贝。如果 a 和 b 是两个向量, 賦值 操作 a = b 将会构造一个与 b 长度相同的新向量 a, 并将所有的元素由 b 拷贝到 a, 而在 Java 中, 这条赋值语句的操作结果是让 a 和 b 引用同一个数组列表。

  API java.util.ArrayList 1.2

  • ArrayList< E > ( )

    构造一个空数组列表。

  • ArrayList < E > ( int initialCapacity)

    用指定容量构造一个空数组列表。 参数:initalCapacity 数组列表的最初容量

  • booleanadd( E ob j )

    在数组列表的尾端添加一个元素。 永远返回 true。 参数:obj 添加的元素

  • int size( )

    返回存储在数组列表中的当前元素数量。(这个值将小于或等于数组列表的容量。)

  • void ensureCapacity( int capacity)

    确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。 参数:capacity 需要的存储容量   

  • void trimTosize( )

    将数组列表的存储容量削减到当前尺寸。

  5.3.1 访问数组列表元素

    很遗憾, 天下没有免费的午餐。 数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。 其原因是 ArrayList 类并不是 Java 程序设计语言的一部分;它只是一个由某些人编写且被放在标准库中的一个实用类。

    使用 get 和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的 [ ]语法格式。

    例如,要设置第 i 个元素,可以使用:

staff.set(i , harry):

    它等价于对数组 a 的元素赋值(数组的下标从 0 开始):

a[i] = harry;

    警告: 只有 i 小于或等于数组列表的大小时, 才能够调用 list.set(i,x)。 例如, 下面这段代 码是错误的:

ArrayList<Employee> list = new ArrayListo(100); // capacity 100,size 0
list.set(0, x); // no element 0 yet

    使用 add 方法为数组添加新元素, 而不要使用 set 方法, 它只能替换数组中已经存在 的元素内容。

    使用下列格式获得数组列表的元素:

Employee e = staff.get(i);

    等价于:

Employee e = a[i];

    注释: 没有泛型类时,原始的 ArrayList 类提供的 get 方法别无选择只能返回 Object, 因 此, get 方法的调用者必须对返回值进行类型转换:

Employee e = (Eiployee) staff.get(i);

    原始的 ArrayList 存在一定的危险性。它的 add 和 set 方法允许接受任意类型的对象。 对于下面这个调用

staff.set(i , "Harry Hacker");

    编译不会给出任何警告, 只有在检索对象并试图对它进行类型转换时, 才会发现有 问题。如果使用 ArrayList, 编译器就会检测到这个错误。

    下面这个技巧可以一举两得, 既可以灵活地扩展数组, 又可以方便地访问数组元素。首 先,创建一个数组, 并添加所有的元素。

ArrayList<X> list = new ArrayList<>();
while (. . .)
{
x =...;
list.add(x);
}

    执行完上述操作后,使用 toArray 方法将数组元素拷贝到一个数组中。

X[] a = new [list.size()];
list.toArray(a);

    除了在数组列表的尾部追加元素之外,还可以在数组列表的中间插入元素,使用带索引 参数的 add 方法。

int n = staff.size() / 2;
staff.add(n, e);

    为了插入一个新元素,位于 n之后的所有元素都要向后移动一个位置。如果插入新元素 后, 数组列表的大小超过了容量, 数组列表就会被重新分配存储空间。

    同样地,可以从数组列表中间删除一个元素。

Employee e = staff.remove(n);

    位于这个位置之后的所有元素都向前移动一个位置, 并且数组的大小减 1,

    对数组实施插入和删除元素的操作其效率比较低。对于小型数组来说,这一点不必担 心。但如果数组存储的元素数比较多, 又经常需要在中间位置插入、删除元素, 就应该考虑使用链表了。有关链表操作的实现方式将在第 9 章中讲述。

    可以使用“ foreach” 循环遍历数组列表:

for (Employee e : staff)
  do something with e

    这个循环和下列代码具有相同的效果

for (int i = 0; i < staff .size(); i ++)
{
  Employee e = staff.get(i);
  do something with e
}

    程序清单 5-11 是对第 4 章中 EmployeeTest 做出修改后的程序。在这里, 将 Employee[ ] 数组替换成了 ArrayList 。请注意下面的变化:

    •不必指出数组的大小。

    •使用 add 将任意多的元素添加到数组中。

    •使用 size() 替代 length 计算元素的数目。

    •使用 a.get(i) 替代 a[i] 访问元素。

    API java.util.ArrayList 1.2

    • void set(int index ,E obj)

      设置数组列表指定位置的元素值, 这个操作将覆盖这个位置的原有内容。

      参数: index 位置(必须介于 0 ~ size()-1 之间)

          obj 新的值

    • E get(int index)

      获得指定位置的元素值。

      参数:index 获得的元素位置(必须介于 0 ~ size()-l 之间)

    • void add(int index,E obj)

      向后移动元素,以便插入元素。

      参数:index 插入位置(必须介于 0 〜 size()-l 之间)

          obj 新元素

    • E removed (int index)

      删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。

      参数:index 被删除的元素位置(必须介于 0 ~ size()-1之间)

  5.3.2 类型化与原始数组列表的兼容性

     在你自己的代码中, 你可能更愿意使用类型参数来增加安全性。这一节中,你会了解如 何与没有使用类型参数的遗留代码交互操作。

     假设有下面这个遗留下来的类:

public class EmployeeDB
{
  public void update(ArrayList list) { . . . }
  public ArrayList find(String query) { . . . }
}

    可以将一个类型化的数组列表传递给 update 方法, 而并不需要进行任何类型转换。

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

    也可以将 staff 对象传递给 update 方法。

     警告: 尽管编译器没有给出任何错误信息或警告, 但是这样调用并不太安全。在 update 方法中, 添加到数组列表中的元素可能不是 Employee 类型。在对这些元素进行检索时就 会出现异常。 听起来似乎很吓人,但思考一下就会发现,这与在 Java 中增加泛型之前是 一样的 , 虚拟机的完整性绝对没有受到威胁。在这种情形下, 既没有降低安全性,也没 有受益于编译时的检查。

     相反地,将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告。

ArrayList<Employee> result = employeeDB.find(query); // yields warning

    注释: 为了能够看到警告性错误的文字信息,要将编译选项置为 -Xlint:unchecked。

     使用类型转换并不能避免出现警告。

ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query);
// yields another warning

    这样, 将会得到另外一个警告信息, 指出类型转换有误。

    这就是 Java 中不尽如人意的参数化类型的限制所带来的结果。鉴于兼容性的考虑, 编译 器在对类型转换进行检査之后, 如果没有发现违反规则的现象,就将所有的类型化数组列表 转换成原始 ArrayList 对象。 在程序运行时,所有的数组列表都是一样的,即没有虚拟机中 的类型参数。 因此, 类型转换( ArrayList) 和 ( ArrayList<Employee> ) 将执行相同的运行时 检查。 

    在这种情形下,不必做什么。 只要在与遗留的代码进行交叉操作时,研究一下编泽器的 警告性提示,并确保这些警告不会造成太严重的后果就行了。

    一旦能确保不会造成严重的后果,可以用@SuppressWarnings("unchecked") 标注来标记 这个变量能够接受类型转换, 如下所示: 

@SuppressWarnings("unchecked") ArrayList<Employee> result =
(ArrayList<Employee>) employeeDB.find(query); // yields another warning

5.4 对象包装器与自动装箱

   有时, 需要将 int 这样的基本类型转换为对象。 所有的基本类型都有一个与之对应的类。 例如,Integer 类对应基本类型 int。通常, 这些类称为包装器 ( wrapper ) 这些对象包装器类 拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 、Void 和 Boolean (前 6 个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不 允许更改包装在其中的值。同时, 对象包装器类还是 final , 因此不能定义它们的子类

  假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说, 不允许写成 ArrayList。这里就用到了 Integer 对象包装器类。我们可以声明一个 Integer 对象的数组列表。 

ArrayList<Integer> list = new ArrayList<>();

  警告: 由于每个值分别包装在对象中, 所以 ArrayList 的效率远远低于 int[ ] 数 组。 因此, 应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更 加重要。

   幸运的是, 有一个很有用的特性, 从而更加便于添加 int 类型的元素到 ArrayList<lnteger> 中。下面这个调用

list.add(3);

  将自动地变换成

list.add (Integer.valueof(3));

  这种变换被称为自动装箱(autoboxing)。

  注释: 大家可能认为自动打包 ( autowrapping) 更加合适, 而“ 装箱 ( boxing) ” 这个词源 自于 C#。

  相反地, 当将一个 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) . . .

  然而,Java 实现却有可能( may) 让它成立。如果将经常出现的值包装到同一个对象中, 这种比较就有可能成立。这种不确定的结果并不是我们所希望的。解决这个问题的办法是在 两个包装器对象比较时调用 equals 方法。

  注释: 自动装箱规范要求 boolean、byte、char 127, 介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中。例如,如果在前面的例子中将 a 和 b 初始化为 100,对它们 进行比较的结果一定成立。

  关于自动装箱还有几点需要说明。首先, 由于包装器类引用可以为 null, 所以自动装箱 有可能会抛出一个 NullPointerException 异常:

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); // Prints 1.0

  最后强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码 时, 插入必要的方法调用。虚拟机只是执行这些字节码。

  使用数值对象包装器还有另外一个好处。Java 设计者发现,可以将某些基本方法放置在包装器中, 例如, 将一个数字字符串转换成数值。

  要想将字符串转换成整型, 可以使用下面这条语句:

int x = Integer.parselnt(s);

  这里与 Integer 对象没有任何关系, parselnt 是一个静态方法。但 Integer 类是放置这个方法的 一个好地方。

  API 注释说明了 Integer 类中包含的一些重要方法。其他数值类也实现了相应的方法。

  警告: 有些人认为包装器类可以用来实现修改数值参数的方法, 然而这是错误的。在第 4 章中曾经讲到, 由于 Java 方法都是值传递, 所以不可能编写一个下面这样的能够增加 整型参数值的 Java 方法。

public static void triple(int x) // won't work
{
  x = 3 * x; // modifies local variable
}
将 int 替换成 Integer 又会怎样呢?
public static void triple(Integer x) // won 't work
{
  ...
}

  问题是 Integer 对象是不可变的: 包含在包装器中的内容不会改变。不能使用这些包 装器类创建修改数值参数的方法。

  如果想编写一个修改数值参数值的方法, 就需要使用在 org.omg.CORBA 包中定义的 持有者( holder) 类型, 包括 IntHolder、BooleanHolder 等。每个持有者类型都包含一个 公有 (!)域值,通过它可以访问存储在其中的值。

public static void triple(IntHolder x)
{
  x.value = 3 * x.value;
}

  API java.lang.Integer 1.0

  • int intValue( )

    以 int 的形式返回 Integer 对象的值(在 Number 类中覆盖了 intValue方法)。

  • static String toString(int i )

    以一个新 String 对象的形式返回给定数值 i 的十进制表示。

  • static String toString(int i ,int radix )

    返回数值 i 的基于给定 radix 参数进制的表示。

  • static int parselnt(String s)

  • static int parseInt(String s,int radix)

    返回字符串 s 表示的整型数值, 给定字符串表示的是十进制的整数(第一种方法) ,或者是 radix 参数进制的整数(第二种方法) 。

  • static Integer valueOf(String s)

  • static Integer value Of(String s, int radix)

    返回用 s 表示的整型数值进行初始化后的一个新 Integer 对象, 给定字符串表示的是十 进制的整数(第一种方法) ,或者是 radix 参数进制的整数(第二种方法。

  API java.text.NumberFormat 1.1

  • Number parse(String s)

    返回数字值,假设给定的 String 表示了一个数值。

5.5 参数数量可变的方法

  在 Java SE 5.0 以前的版本中,每个 Java 方法都有固定数量的参数。然而,现在的版本 提供了可以用可变的参数数量调用的方法(有时称为“ 变参” 方法)。

  前面已经看到过这样的方法:printfo 例如,下面的方法调用:

System.out.printf("%d", n);

System.out.printf("%d %s", n, "widgets");

  在上面两条语句中,尽管一个调用包含两个参数,另一个调用包含三个参数,但它们调用的 都是同一个方法。

  printf方法是这样定义的:

public class PrintStream
{
  public PrintStream printf(String fmt , Object... args) { return format(fmt, args); }
}

  这里的省略号 . . . 是 Java 代码的一部分,它表明这个方法可以接收任意数量的对象(除 fmt 参数之外)。

  实际上,printf方法接收两个参数,一个是格式字符串, 另一个是 Object [] 数组, 其中 保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值, 自动装箱功能将把它们转换成对象 )。现在将扫描 fmt字符串, 并将第 i 个格式说明符与 args[i] 的值匹配 起来。

  换句话说,对于 printf 的实现者来说,Object… 参数类型与 Object[ ]完全一样。

  编译器需要对 printf 的每次调用进行转换, 以便将参数绑定到数组上,并在必要的时候 进行自动装箱:

System.out.printf("%x %s", new Object[] { new Integer(n), "widgets" } );

  用户自己也可以定义可变参数的方法, 并将参数指定为任意类型, 甚至是基本类型。下 面是一个简单的示例:其功能为计算若干个数值的最大值。

public static double max (double... values)
{
  double largest = Double.NECATIVE_INFINITY;
  for (double v : values) if (v > largest) largest = v;
  return largest;
}

  可以像下面这样调用这个方法:

double m = max(3.1, 40.4, -5);

  编译器将 new double[ ] {3.1, 40.4,-5} 传递给 max 方法。

  注释: 允许将一个数组传递给可变参数方法的最后一个参数。 例如:

System.out.printf("%d %s", new Object[] { new Integer(l), "widgets" } );

  因此, 可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法, 而不会破坏任何已经存在的代码。例如, MessageFormat.format 在 Java SE 5.0 就采用了 这种方式。甚至可以将 main 方法声明为下列形式:

public static void main(String... args)

5.6 枚举类

  读者在第 3 章已经看到如何定义枚举类型。下面是一个典型的例子:

public enum 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, 这个方法能够返回枚举常量名。例如, Size.SMALL.toString( ) 将返回字符串 “ SMALL”。

  toString 的逆方法是静态方法 valueOf。例如, 语句:

Size s = 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。

  注释: 如同 Class 类一样, 鉴于简化的考虑, Enum 类省略了一个类型参数。 例如, 实 际上, 应该将枚举类型 Size扩展为 Enum <Size>。 类型参数在 compareTo 方法中使用 ( comPareTo 方法在第 6 章中介绍, 类型参数在第 8 章中介绍)。

  API java.Iang.Enum 5.0

  • static Enum valueOf(Class enumClass , String name )

    返回指定名字、给定类的枚举常量。

  • String toString( )

    返回枚举常量名。

  • int ordinal ( )

    返回枚举常量在 enum 声明中的位置,位置从 0 开始计数。

  • int compareTo( E other )

    如果枚举常量出现在 Other 之前, 则返回一个负值;如果 this=other,则返回 0; 否则, 返回正值。枚举常量的出现次序在 enum 声明中给出。

5.7 反射

  反射库( reflection library) 提供了一个非常丰富且精心设计的工具集, 以便编写能够动态操纵 Java 代码的程序。这项功能被大量地应用于 JavaBeans 中, 它是 Java组件的体系结构 (有关 JavaBeans 的详细内容在卷 II 中阐述)。使用反射, Java 可以支持 Visual Basic 用户习惯使用的工具。特别是在设计或运行中添加新类时, 能够快速地应用开发工具动态地查询新添加类的能力。

  能够分析类能力的程序称为反射(reflective )。反射机制的功能极其强大,在下面可以看 到, 反射机制可以用来:

  • 在运行时分析类的能力。

  • 在运行时查看对象, 例如, 编写一个 toString 方法供所有类使用。

  • 实现通用的数组操作代码。

  • 利用 Method 对象, 这个对象很像C++中的函数指针。

  反射是一种功能强大且复杂的机制。 使用它的主要人员是工具构造者,而不是应用程序员。如果仅对设计应用程序感兴趣, 而对构造工具不感兴趣, 可以跳过本章的剩余部分, 稍后再返回来学习。

  5.7.1 Class 类

    在程序运行期间Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识这个信息跟踪着每个对象所属的类虚拟机利用运行时类型信息选择相应的方法执行

    然而, 可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class, 这 个 名 字很容易让人混淆。Object 类中的 getClass( ) 方法将会返回一个 Class 类型的实例。

Employee e;
...
Class cl = e.getClass();

    如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特 定类的属性。最常用的 Class 方法是 getName。 这个方法将返回类的名字。例如,下面这条 语句:

System.out.println(e.getClass().getName() + " " + e.getName());

    如果 e 是一个雇员,则会打印输出

Employee Harry Hacker

    如果 e 是经理, 则会打印输出:

Manager Harry Hacker

    如果类在一个包里,包的名字也作为类名的一部分:

Random generator = new Random():
Class cl = generator.getClass();
String name = cl .getName(); // name is set to "java.util .Random"

    还可以调用静态方法 forName 获得类名对应的 Class 对象

String dassName = "java.util .Random";
Class cl = Class.forName(className);

    如果类名保存在字符串中, 并可在运行中改变, 就可以使用这个方法。当然, 这个方法 只有在 className 是类名或接口名时才能够执行。否则,forName 方法将抛出一个 checked exception ( 已检查异常)。无论何时使用这个方法, 都应该提供一个异常处理器( exception handler) 。如何提供一个异常处理器,请参看下一节。

    提示:在启动时, 包含 main 方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类, 以此类推。对于一个大型的应用程序来说, 这将会消耗很多时 间, 用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的幻觉。 不过,要确保包含 main 方法的类没有显式地引用其他的类。首先,显示一个启动画面; 然后,通过调用 Class.forName 手工地加载其他的类。

    获得 Class类对象的第三种方法非常简单。如果 T 是任意的 Java 类型(或 void 关键字),T.class 将代表匹配的类对象。例如:

Class dl = Random,class; // if you import java.util
Gass cl 2 = int.class;
Class cl 3 = Double[].class;

    请注意,一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如, int 不是类, 但 int.class 是一个 Class 类型的对象。

    注释: Class 类实际上是一个泛型类。例如, Employee.class 的类型是 Class 。 没有说明这个问题的原因是: 它将已经抽象的概念更加复杂化了。在大多数实际问题 中, 可以忽略类型参数, 而使用原始的 Class 类。有关这个问题更详细的论述请参看第 8 章。

    警告: 鉴于历史原 因,getName 方法在应用于数组类型的时候会返回一个很奇怪的名字

    • Double[ ] class.getName( ) 返回“ [Ljava.lang.Double;’’。

    • int[ ].class.getName( ) 返回“ [I ” 。

    虚拟机为每个类型管理一个 Class 对象。 因此,可以利用 =运算符实现两个类对象比较 的操作。 例如,

if (e.getClass() = Employee.class) . . .

    还有一个很有用的方法 newlnstance( ), 可以用来动态地创建一个类的实例例如,

e.getClass().newlnstance();

    创建了一个与 e 具有相同类类型的实例。 newlnstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器, 就会抛出一个异常

    将 forName 与 newlnstance 配合起来使用, 可以根据存储在字符串中的类名创建一个对象

String s = "java.util .Random";
Object m = Class.forName (s) .newInstance();

    注释:如果需要以这种方式向希望按名称创建的类的构造器提供参数, 就不要使用上面那条语句, 而必须使用 Constructor 类中的 newlnstance 方法

    C++ 注释:newlnstance 方法对应 C++ 中虚拟构造器的习惯用法。 然而,C++ 中的虚拟 构造器不是一种语言特性, 需要由专门的库支持。 Class 类与 C++ 中的 type_info 类相似, getClass 方法与 C++ 中的 typeid 运算符等价。 但 Java 中的 Class 比 C++ 中的 type_info 的功能强。C++ 中的 type_info 只能以字符串的形式显示一个类型的名字, 而不能创建那 个类型的对象。

  5.7.2 捕获异常

    我们将在第 7 章中全面地讲述异常处理机制,但现在时常遇到一些方法需要抛出异常。

    当程序运行过程中发生错误时,就会“ 抛出异常“。 抛出异常比终止程序要灵活得多, 这是因为可以提供一个“ 捕获” 异常的处理器 (handler) 对异常情况进行处理。

    如果没有提供处理器,程序就会终止,并在控制台上打印出一条信息, 其中给出了异常的 类型。可能在前面已经看到过一些异常报告, 例如, 偶然使用了 null 引用或者数组越界等。 

    异常有两种类型: 未检查异常已检查异常。 对于已检查异常, 编译器将会检查是否提 供了处理器。 然而,有很多常见的异常, 例如,访问 null 引用, 都属于未检查异常。编译 器不会査看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发 生, 而不要将精力花在编写异常处理器上。

    并不是所有的错误都是可以避免的。如果竭尽全力还是发生了异常, 编译器就要求提供 一个处理器。Class.forName 方法就是一个抛出已检查异常的例子。在第 7 章中, 将会看到几 种异常处理的策略现在, 只介绍一下如何实现最简单的处理器。

    将可能抛出已检査异常的一个或多个方法调用代码放在 try块中,然后在 catch 子句中提 供处理器代码。

try
{
  statements that might throwexceptions
}
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();
}

    如果类名不存在, 则将跳过 try 块中的剩余代码,程序直接进入catch 子句(这里,利用 Throwable 类的 printStackTrace 方法打印出栈的轨迹。Throwable 是 Exception 类的超类)。如 果 try块中没有抛出任何异常, 那么会跳过 catch 子句的处理器代码。

    API Java.lang.Class 1.0

     • static Class forName(String className)

      返回描述类名为 className 的 Class 对象。

    • Object newlnstance()

      返回这个类的一个新实例。

     API java.Iang.reflect.Constructor 1.1

    • Object newlnstance(Object[] args)

      构造一个这个构造器所属类的新实例。

      参数:args 这是提供给构造器的参数。有关如何提供参数的详细情况请参看 5.7.6 节的论述。

    API java.Iang.Throwable 1.0

    • void printStackTrace() 将 Throwable 对象和栈的轨迹输出到标准错误流。

  5.7.3 利用反射分析类的能力

    下面简要地介绍一下反射机制最重要的内容—检查类的结构

    在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、 方 法和构造器。 这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。Field类有一 个 getType 方法, 用来返回描述域所属类型的 Class 对象。Method 和 Constructor 类有能够 报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。这 三个类还有一个叫 做 getModifiers 的方法, 它将返回一个整型数值,用不同的位开关描述 public 和 static 这样 的修饰符使用状况。另外, 还可以利用java.lang.reflect 包中的 Modifier类的静态方法分析 getModifiers 返回的整型数值。例如, 可以使用 Modifier 类中的 isPublic、 isPrivate 或 isFinal 判断方法或构造器是否是 public、 private 或 final。 我们需要做的全部工作就是调用 Modifier 类的相应方法,并对返回的整型数值进行分析,另外,还可以利用 Modifier.toString方法将 修饰符打印出来

    Class类中的 getFields、 getMethods 和 getConstructors 方 法 将 分 别 返 回 类 提 供 的 public 域、 方法和构造器数组, 其中包括超类的公有成员Class 类的 getDeclareFields、 getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构 造器, 其中包括私有和受保护成员,但不包括超类的成员

    程序清单 5-13 显示了如何打印一个类的全部信息的方法。这个程序将提醒用户输入类 名,然后输出类中所有的方法和构造器的签名, 以及全部域名。假如输入

java.lang.Double

    程序将会输出:

public class java.lang.Double extends java.lang.Number
{
public java.lang.Double(java.lang.String);
public java.1ang.Double(double);
public int hashCode(); public int compareTo(java.1ang.Object); public int compareTo(java.lang.Double); public boolean equals(java.lang.Object); public java.lang.String toString(); public static java.lang.String toString(double); public static java.lang.Double valueOf(java.lang.String); public static boolean isNaN(double); public boolean isNaNO; public static boolean islnfinite(double); public boolean isInfinite(); public byte byteValue(); public short shortValue(); public int intValue(); public long longValue() ; public float floatValue(); public double doubleValue(); public static double parseDouble(java.lang.String); public static native long doubleToLongBits(double); public static native long doubleToRawLongBits(double); public static native double 1ongBitsToDouble(long); public static final double POSITIVE_INFINITY; public static final double NECATIVEJNFINITY; public static final double NaN; public static final double MAX_VALUE; public static final double MIN_VALUE; public static final java.Iang.Class TYPE; private double value; private static final long serialVersionUID; }

    值得注意的是:这个程序可以分析 Java 解释器能够加载的任何类, 而不仅仅是编译程序时可以使用的类。在下一章中, 还将使用这个程序查看 Java 编译器自动生成的内部类。

    程序清单 5-13 reflection/ReflectionTest.java

package reflection;

import java.util
import java.lang.reflect.*;

/**
* This program uses reflection to print all features of a class.
*@version 1.1 2004-02-21
* author Cay Horstmann
*/
public class ReflectionTest
{
    public static void main(String[] args)
    {
        // read class name from command line args or user input
        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
        {
            // print class name and superclass name (if != Object)
            Class cl = Class.forName (name);
            Class supercl = cl.getSuperclass();
            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());

            System.out.print("
{
");
            printConstructors(cl);
            System.out.println();
            printMethods(cl);
            System.out.println();
            printFields(cl);
            System.out.println("}");
        }
        catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }
        System.exit(0);
    }

    /**
    * Prints all constructors of a class
    * @param cl a class
    */
    public static void printConstructors(Class cl)
    {
        Constructor [] constructors = cl.getDedaredConstructors():
        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 + "(");
            
            // print parameter types
            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(");");
        }
    }

    /**
    * Prints all methods of a class
    * @param cl a class
    */
    public static void printMethods(Class cl)
    {
        Method[] methods = cl.getDedaredMethods() ;
        for (Method m : methods)
        {
            Class retType = m.getReturnType();
            String name = m. getName();
            
            System.out.print(" ");
            //print modifiers, return type and method name
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers +" ");
            System.out.print(retType.getName() +" "+ name + "(");
            //print parameter types
            Class [] paramTypes = m.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++)
            {
                if (j > 0) System.out.print(", ");
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }

    /**
    * Prints all fields of a class
    * @param cl a class
    */
    public static void printFields(Class cl)
    {
        Field[] fields = cl.getDedaredFields();
        for (Field f : fields)
        {
            Class type = f.getType();
            String name = f.getName();
            System.out.print(" ");
            String modifiers = Modifier.toString(f.getModifiers());
            if (modi fiers.length() > 0) System,out.print(modifiers + " ");
            System.out.println(type.getName() + " " + name +";");
        }
    }
}

     API java.lang.Class 1.0

    • Field[] getFields() 1.1

    • Filed[] getDeclaredFields() 1.1

      getFields 方法将返回一个包含 Field 对象的数组, 这些对象记录了这个类或其超类的公有域。getDeclaredField 方法也将返回包含 Field 对象的数组, 这些对象记录了这个 类的全部域。如果类中没有域, 或者 Class 对象描述的是基本类型或数组类型, 这些 方法将返回一个长度为 0 的数组。

    • Method[] getMethods() 1.1

    • Method[] getDeclareMethods() 1.1

      返回包含 Method 对象的数组:getMethods 将返回所有的公有方法, 包括从超类继承 来的公有方法;getDeclaredMethods 返回这个类或接口的全部方法, 但不包括由超类 继承了的方法。

    • Constructor[] getConstructors() 1.1

    • Constructor [] getDeclaredConstructors() 1.1

      返回包含 Constructor 对象的数组, 其中包含了 Class 对象所描述的类的所有公有构造 器(getConstructors) 或所有构造器(getDeclaredConstructors)。

     API java.lang.reflect.Field 1.1

     API java.lang.reflect.Method 1.1

     API java.lang.reflect.Constructor 1.1

     • Class getDeclaringClass( )

      返冋一个用于描述类中定义的构造器、 方法或域的 Class 对象。

    • Class[] getExceptionTypes ( ) ( 在 Constructor 和 Method 类 中)

      返回一个用于描述方法抛出的异常类型的 Class 对象数组。

    • int getModifiers( )

      返回一个用于描述构造器、 方法或域的修饰符的整型数值。使用 Modifier 类中的这个 方法可以分析这个返回值。

    • String getName( )

      返冋一个用于描述构造器、 方法或域名的字符串。

    • Class[] getParameterTypes ( ) ( 在 Constructor 和 Method 类 中)

      返回一个用于描述参数类型的 Class 对象数组。

    • Class getReturnType( ) ( 在 Method 类 中)

      返回一个用于描述返回类型的 Class 对象。

     API java.lang.reflect.Modifier 1.1

     •static String toString(int modifiers )

      返回对应 modifiers 中位设置的修饰符的字符串表示。

    • static boolean isAbstract(int modifiers ) 

    • static boolean isFinal (int modifiers )

    • static boolean  islnterface(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 )

      这些方法将检测方法名中对应的修饰符在 modffiers 值中的位。

  5.7.4 在运行时使用反射分析对象

     从前面一节中, 已经知道如何查看任意对象的数据域名称和类型:

    • 获得对应的 Class 对象。

    • 通过 Class 对象调用 getDeclaredFields。

    本节将进一步查看数据域的实际内容。当然, 在编写程序时, 如果知道想要査看的域名 和类型,查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的 对象域。

    查看对象域的关键方法是 Field类中的 get 方法。如果 f 是一个 Field 类型的对象(例如, 通过 getDeclaredFields 得到的对象) ,obj 是某个包含 f 域的类的对象,f.get(obj) 将返回一个 对象,其值为 obj 域的当前值。这样说起来显得有点抽象,这里看一看下面这个示例的运行。

Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass();
// the class object representing Employee
Field f = cl .getDeclaredField("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。只有利用 get 方法才能得到可访问域的值。除非拥有访问权限,否则 Java 安全机制只允许査看任意对象有哪些域, 而不允许读取它们的值。

    反射机制的默认行为受限于 Java 的访问控制。然而, 如果一个 Java 程序没有受到安全管理器的控制, 就可以覆盖访问控制。 为了达到这个目的, 需要调用 Field、 Method 或 Constructor 对象的 setAccessible 方法。例如,

f.setAtcessible(true); // now OK to call f.get(harry);

    setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 Field、 Method 和 Constructor 类的公共超类。这个特性是为调试、 持久存储和相似机制提供的。本书稍后将利用它编写一 个通用的 toString方法。

    get 方法还有一个需要解决的问题。name 域是一个 String, 因此把它作为 Object 返回 没有什么问题。但是, 假定我们想要查看 salary 域。它属于 double 类型,而 Java中数值类型不是对象。要想解决这个问题, 可以使用 Field 类中的 getDouble 方法,也可以调用 get 方法,此时, 反射机制将会自动地将这个域值打包到相应的对象包装器中,这里将打包成 Double。

    当然,可以获得就可以设置。 调用 f.set(obj,value) 可以将 obj 对象的 f 域设置成新值。

    程序清单 5-14 显示了如何编写一个可供任意类使用的通用 toString方法。 其中使用 getDeclaredFileds 获得所有的数据域, 然后使用 setAccessible 将所有的域设置为可访问的。 对 于每个域,获得了名字和值。程序清单 5-14 递归调用 toString方法, 将每个值转换成字符串。

    泛型 toString方法需要解释几个复杂的问题。循环引用将有可能导致无限递归。因此, ObjectAnalyzer 将记录已经被访问过的对象。 另外, 为了能够査看数组内部, 需要采用一种 不同的方式。有关这种方式的具体内容将在下一节中详细论述。

    可以使用 toString 方法查看任意对象的内部信息。例如, 下面这个调用:

ArrayList<Integer> squares = new ArrayList<>();
for (int i = 1; i <= 5; i++) squares.add(i*i);
System.out .println(new ObjectAnalyzer().toString(squares));

    将会产生下时的打印结果:

java.uti1.ArrayList[elementData=class java.1ang.Object[]{java.1ang.Integer[value=l][][],
java.1ang.Integer[value=4][][],java.1ang.Integer[value=9][] [],java.1ang.Integer[value=16][][],
java.1ang.Integer[value=25][][].null.null,null,null.null},size=5][modCount=5][][]

    还可以使用通用的 toString 方法实现自己类中的 toString 方法, 如下所示:

public String toString()
{
  return new ObjectAnalyzer().toString(this);
}

    这是一种公认的提供 toString 方法的手段, 在编写程序时会发现, 它是非常有用的。

    API java.Iang.reflect.AccessibleObject 1.2

    • void setAccessible(boolean flag)

      为反射对象设置可访问标志。flag 为 true 表明屏蔽 Java 语言的访问检查,使得对象的私有属性也可以被査询和设置。

    • boolean isAccessible( )

      返回反射对象的可访问标志的值。

    • static void setAccessible(AccessibleObject[] array,boolean flag)

      是一种设置对象数组可访问标志的快捷方法。

    API java.lang.Class 1.1

    • Field getField(String name )

    • Field[] getField()

      返回指定名称的公有域, 或包含所有域的数组

    • Field getDeclaredField(String name )

    • Field[] getDeclaredFields( )

      返回类中声明的给定名称的域, 或者包含声明的全部域的数组。

    API java.Iang.reflect.Field 1.1

    • Object get(Object obj)

      返回 obj 对象中用 Field 对象表示的域值。

    • void set(Object obj ,Object newValue )

      用一个新值设置 Obj 对象中 Field 对象表示的域。

  5.7.5 使用反射编写泛型数组代码

    java.lang.reflect 包中的 Array 类允许动态地创建数组。例如, 将这个特性应用到 Array 类中的 copyOf方法实现中, 应该记得这个方法可以用于扩展已经填满的数组。

Employee[] a = new Employee[100];
...
// array is full
a = Arrays.copyOf(a, 2 *a.length);

    如何编写这样一个通用的方法呢? 正好能够将 Employee[ ] 数组转换为 Object[ ] 数组, 这让人感觉很有希望。下面进行第一次尝试。

public static Object[] badCopyOf(Object[] a, int newLength) // not useful
{
  ObjectD newArray = new Object[newlength]:
  System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
  return newArray;

}

    然而, 在实际使用结果数组时会遇到一个问题。这段代码返回的数组类型是对象数组 (Object[]) 类型,这是由于使用下面这行代码创建的数组:

new Object[newLength]

    一个对象数组不能转换成雇员数组( Employee[ ]) 。如果这样做, 则在运行时 Java 将会 产生 ClassCastException 异常。前面已经看到,Java 数组会记住每个元素的类型, 即创建数 组时 new 表达式中使用的元素类型。将一个 Employee[ ]临时地转换成 Object[ ] 数组, 然后 再把它转换回来是可以的,但一 从开始就是 Object[] 的数组却永远不能转换成 Employe[ ]数组。 为了编写这类通用的数组代码, 需要能够创建与原数组类型相同的新数组。为此, 需要 java, lang.reflect 包中 Array 类的一些方法。其中最关键的是 Array类中的静态方法 newlnstance, 它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的 长度。

Object newArray = Array.newlnstance(componentType , newLength);

    为了能够实际地运行,需要获得新数组的长度和元素类型。

    可以通过调用 Array.getLength(a) 获得数组的长度, 也可以通过 Array 类的静态 getLength 方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行以下工作:

    1 ) 首先获得 a 数组的类对象。

    2 ) 确认它是一个数组。

    3 ) 使用 Class 类(只能定义表示数组的类对象)的 getComponentType 方法确定数组对应 的类型。

    为什么 getLength 是 Array 的方法,而 getComponentType 是 Class 的方法呢? 我们也不清楚。反射方法的分类有时确实显得有点古怪。下面是这段代码:

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.newlnstance(componentType, newLength):
  System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
  return newArray;
}

    请注意,这个 CopyOf 方法可以用来扩展任意类型的数组, 而不仅是对象数组。

int[] a = { 1,2, 3, 4, 5 };
a = (int[]) goodCopyOf(a, 10);

    为了能够实现上述操作,应该将 goodCopyOf 的参数声明为 Object 类型,而不要声明为 对象型数组(Object[])。整型数组类型 int[] 可以被转换成 Object,但不能转换成对象数组。

    程序清单 5-16 显示了两个扩展数组的方法。请注意, 将 badCopyOf 的返回值进行类型 转换将会抛出一个异常。

    API java.lang.reflect.Array 1.1

    • static Object get(Object array,int index)

    • static xxx getXxx(Object array,int index) ( xxx 是 boolean、byte、 char、 double、 float、 int、 long、 short 之中的一种基本类M。)

      这些方法将返回存储在给定位置上的给定数组的内容。

    • static void set(Object array,int index,Object newValue)

    • static setXxx(Object array,int index,xxx newValue) ( xxx 是 boolean、 byte、char、double、float、 int、 long、 short 之中的一种基本类型。)

      这些方法将一个新值存储到给定位置上的给定数组中。

    • static int getLength(Object array)

      返回数组的长度。

    • static Object newInstance(Class componentType,int length)

    • static Object newInstance(Class componentType,int[]lengths)

      返回一个具有给定类型、 给定维数的新数组。

  5.7.6 调用任意方法

    在 C 和 C++ 中, 可以从函数指针执行任意函数。从表面上看, Java 没有提供方法指针, 即将一个方法的存储地址传给另外一个方法, 以便第二个方法能够随后调用它。事实上, Java 的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。他们认为 Java 提供的 接口(interface ) (将在下一章讨论)是一种更好的解决方案。然而, 反射机制允许你调用任意方法

    注释: 微软公司为自己的非标准 Java 语 言 ( 以 及 后 来 的 C#) 增加了另一种被称为委 托( delegate ) 的方法指针类型, 它与本节讨论的 Method 类不同。然而, 在下一章中讨 论的内部类比委托更加有用。

    为了能够看到方法指针的工作过程, 先回忆一下利用 Field 类的 get 方法查看对象域的过程。与之类似, 在 Method 类中有一个 invoke 方法, 它允许调用包装在当前 Method 对象中 的方法。invoke 方法的签名是:

Object invoke(Object obj, Object... args)

    第一个参数是隐式参数, 其余的对象提供了显式参数(在 Java SE 5.0 以前的版本中,必须传递一个对象数组, 如果没有显式参数就传递一个 null )。

    对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null。

    例如, 假设用 ml 代表 Employee 类的 getName 方法,下面这条语句显示了如何调用这个 方法:

String n = (String) ml.invoke(harry);

    如果返回类型是基本类型, invoke 方法会返回其包装器类型。 例如, 假设 m2 表示 Employee 类的 getSalary 方法, 那么返回的对象实际上是一个 Double, 必须相应地完成类型转换。可以使用自动拆箱将它转换为一个 double:

double s = (Double) m2,invoke(harry);

    如何得到 Method 对象呢? 当然, 可以通过调用 getDeclareMethods 方法, 然后对返回 的 Method 对象数组进行查找, 直到发现想要的方法为止。 也可以通过调用 Class类中的 getMethod方法得到想要的方法。它与 getField 方法类似。getField 方法根据表示域名的字符串,返回一个 Field 对象。然而, 有可能存在若干个相同名字的方法,因此要格外小心, 以确保能够准确地得到想要的那个方法。有鉴于此,还必须提供想要的方法的参数类型。 getMethod 的签名是:

Method getMethod(String name, Class... parameterTypes)

    例如, 下面说明了如何获得 Employee 类的 getName 方法和 raiseSalary 方法的方法指针。

Method ml = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);

    到此为止,读者已经学习了使用 Method 对象的规则。 下面看一下如何将它们组织在一 起。程序清单 5-17 是一个打印诸如 Math.sqrt、 Math.sin 这样的数学函数值表的程序。打印的 结果如下所示:

public static native double java.lang.Math.sqrt(double)
1.0000 | 1.0000
10.0000 | 3.1623

    当然,这段打印数学函数表格的代码与具体打印的数学函数无关。

double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx)
{
  double y = (Double) f.invoke(null, x);
  System.out.printf("%10.4f | %10.4f%n", x, y);
}

    在这里,f 是一个 Method 类型的对象。由于正在调用的方法是一个静态方法, 所以invoke 的第一个参数是 null。

    为了将 Math.sqrt 函数表格化, 需要将 f 设置为:

Math.class.getMethod("sqrt",double.class)

    这是 Math 类中的一个方法, 通过参数向它提供了一个函数名 sqrt 和一个 double 类型的 参数。

    上述程序清楚地表明, 可以使用 method 对象实现 C (或 C# 中的委派)语言中函数指针 的所有操作。同 C 一样,这种程序设计风格并不太简便,出错的可能性也比较大。如果在调 用方法的时候提供了一个错误的参数,那么 invoke 方法将会抛出一个异常。

    另外,invoke 的参数和返回值必须是 Object 类型的。这就意味着必须进行多次的类型转 换。这样做将会使编译器错过检查代码的机会。因此, 等到测试阶段才会发现这些错误, 找 到并改正它们将会更加困难。不仅如此, 使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。

    有鉴于此,建议仅在必要的时候才使用 Method 对象,而最好使用接口以及 Java SE 8中 的 lambda 表达式(第 6 章中介绍)。特别要重申: 建议 Java 开发者不要使用 Method 对象的 回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。

    API java.Iang.reflect.Method 1.1

    •public Object invoke(Object implicitParameter,Object[] explicitParamenters)

      调用这个对象所描述的方法, 传递给定参数,并返回方法的返回值。对于静态方法, 把 null作为隐式参数传递。在使用包装器传递基本类型的值时, 基本类型的返回值必 须是未包装的。

5.8 继承的设计技巧

  在本章的最后,给出一些对设计继承关系很有帮助的建议。

  1. 将公共操作和域放在超类

    这就是为什么将姓名域放在 Person类中,而没有将它放在 Employee 和 Student 类中的原因。

  2. 不要使用受保护的域

    有些程序员认为,将大多数的实例域定义为 protected 是一个不错的主意,只有这样,子 类才能够在需要的时候直接访问它们。然而, protected 机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合是无限制的, 任何一个人都能够由某个类派生一个子类,并 编写代码以直接访问 protected 的实例域, 从而破坏了封装性。第二, 在 Java 程序设计语言 中,在同一个包中的所有类都可以访问 proteced 域,而不管它是否为这个类的子类。

    不过,protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。

  3. 使用继承实现“ is-a” 关系

    使用继承很容易达到节省代码的目的,但有时候也被人们滥用了。例如, 假设需要定义 一个钟点工类。钟点工的信息包含姓名和雇佣日期,但是没有薪水。他们按小时计薪,并且 不会因为拖延时间而获得加薪。这似乎在诱导人们由 Employee 派生出子类 Contractor, 然后 再增加一个 hourlyWage 域。

public class Contractor extends Employee
{
  private double hourlyWage;
  ...
}

    这并不是一个好主意。因为这样一来, 每个钟点工对象中都包含了薪水和计时工资这两个域。 在实现打印支票或税单方法的时候, 会带来无尽的麻烦, 并且与不采用继承,会多写很多代码。

    钟点工与雇员之间不属于“ is-a” 关系。钟点工不是特殊的雇员。

  4. 除非所有继承的方法都有意义,否则不要使用继承

    假设想编写一个 Holiday 类。毫无疑问,每个假日也是一日,并且一日可以用 Gregorian Calendar 类的实例表示,因此可以使用继承。

class Holiday extends CregorianCalendar { . . . }

    很遗憾, 在继承的操作中, 假日集不是封闭的。 在 GregorianCalendar 中有一个公有方法 add, 可以将假日转换成非假日:

Holiday Christmas;
Christmas.add(Calendar.DAY_OF_MONTH , 12);

    因此,继承对于这个例子来说并不太适宜。

    需要指出, 如果扩展 LocalDate 就不会出现这个问题。由于这个类是不可变的,所以没 有任何方法会把假日变成非假日。

  5. 在覆盖方法时,不要改变预期的行为

    置换原则不仅应用于语法, 而且也可以应用于行为,这似乎更加重要。在覆盖一个方法 的时候,不应该毫无原由地改变行为的内涵。就这一点而言,编译器不会提供任何帮助,即 编译器不会检查重新定义的方法是否有意义。例如,可以重定义 Holiday 类中 add方法“ 修 正” 原方法的问题,或什么也不做, 或抛出一个异常, 或继续到下一个假日。

    然而这些都违反了置换原则。语句序列

int dl = x.get(Calendar.DAY_OF_MONTH);
x.add(Calendar.DAY_OF_MONTH , 1);
int d2 = x.get(Calendar.DAY_OF_HONTH);
System.out.println(d2 - dl);

    不管 x 属于 GregorianCalendar 类, 还是属于 Holiday 类,执行上述语句后都应该得到预期的 行为。

    当然, 这样可能会引起某些争议。人们可能就预期行为的含义争论不休。例如, 有些人 争论说, 置换原则要求 Manager.equals 不处理 bonus 域,因为 Employee.equals 没有它。实际 上,凭空讨论这些问题毫无意义。关键在于, 在覆盖子类中的方法时,不要偏离最初的设计 想法。

  6. 使用多态, 而非类型信息

    无论什么时候,对于下面这种形式的代码

if (x is oftype1)
  action1(x);
else if (x is oftype 2)
  action2(x);

    都应该考虑使用多态性。

    action1与action2 表示的是相同的概念吗? 如果是相同的概念,就应该为这个概念定义一 个方法, 并将其放置在两个类的超类或接口中,然后, 就可以调用

x.action()

    以便使用多态性提供的动态分派机制执行相应的动作。

    使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。

  7. 不要过多地使用反射

    反射机制使得人们可以通过在运行时查看域和方法, 让人们编写出更具有通用性的程序。 这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的, 即编译器很难帮助人们发现程序中的错误, 因此只有在运行时才发现错误并导致异常。

    现在你已经了解了 Java 支持面向对象编程的基础内容:类、继承和多态。下一章中我们 将介绍两个髙级主题:接口和 lambda 表达式, 它们对于有效地使用 Java 非常重要。

恭喜,本章完!

原文地址:https://www.cnblogs.com/yangjingkang/p/14383194.html