利用继承, 人们可以基于已存在的类构造一个新类 。 继承已存在的类就是复用 (继承) 这些类的方法和域 。在此基础上, 还可以添加一些新的方法和域 , 以满足新的需求 。 这是 Java 程序设计中的一项核心技术。
反射是指在程序运行期间发现更多的类及其属性的能力 。 这是一个功能强大的特性, 使用起来也比较复杂。
类 、超类和子类
假设你在某个公司工作, 这个公司中经理的待遇与普通雇员的待遇存在着一些差异 。 不过, 他们之间也存在着很多相同的地方, 例如 , 他们都领取薪水 。 只是普通雇员在完成本职任务之后仅领取薪水, 而经理在
完成了预期的业绩之后还能得到奖金 。 这种情形就需要使用继承 。 这是因为需要为经理定义一个新类 Manager , 以便增加一些新功能 。 但可以重用 Employee 类中已经编写的部分代码,并将其中的所有域保留下来 。 从理论上讲, 在 Manager 与 Employee 之间存在着明显的 “ isa ” (是)关系, 每个经理都是一名雇员 :“ is a ” 关系是继承的一个明显特征 。
定义子类
下面是由继承 Employee 类来定义 Manager 类的格式, 关键字 extends 表示继承 。
public class Manager extends Employee
{
添加方法和域
}
关键字 extends 表明正在构造的新类派生于一个已存在的类 。 已存在的类称为超类( superclass ) 、 基类 ( base class ) 或父类 ( parent class ) ;新类称为子类 (subclass) 、派生类、( derived class ) 或孩子类 ( child class ) 。
尽管 Employee 类是一个超类, 但并不是因为它优于子类或者拥有比子类更多的功能 。实际上恰恰相反,子类比超类拥有的功能更加丰富 。 例如, 读过 Manager 类的源代码之后就会发现 , Manager类比超类 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 。
在通过扩展超类定义子类的时候 , 仅需要指出子类与超类的不同之处 。 因此在设计类的时候, 应该将通用的方法放在超类中, 而将具有特殊用途的方法放在子类中 , 这种将通用的功能放到超类的做法, 在面向对象程序设计中十分普遍 。
覆盖方法
然而, 超类中的有些方法对子类 Manager 并不一定适用 。 具体来说, Manager 类中的getSalary 方法应该返回薪水和奖金的总和 。 为此, 需要提供一个新的方法来覆盖 ( override )超类中的这个方法 :
public class Manager extends Employee
{
...
public double getSalary()
{
...
}
}
应该如何实现这个方法呢 ? 乍看起来似乎很简单 , 只要返回 salary 和 bonus 域的总和就可以了 :
public double getSalaryO
{
return salary + bonus; // won ' t work
}
然而, 这个方法并不能运行 。这是因为Manager类的getSalary方法不能够直接地访问超类的私有域。也就是说, 尽管每个 Manager 对象都拥有一个名为 salary 的域 , 但在 Manager 类的getSalary 方法中并不能够直接地访问 salary 域 。只有 Employee 类的方法才能够访问私有部分 。 如果 Manager 类的方法一定要访问私有域, 就必须借助于公有的接口, Employee 类中的公有方法 getSalary 正是这样一个接口 。
将对 salary 域的访问替换成调用 getSalary 方法 。
public double getSalary()
{
double baseSalary = getSalaryO ; // still won ' t work
return baseSalary + bonus ;
}
上面这段代码仍然不能运行 。 问题出现在调用 getSalary 的语句上 , 这是因为 Manager 类也有一个getSalary 方法, 所以这条语句将会导致无限次地调用自己 , 直到整个程序崩溃为止 。
这里需要指出 : 我们希望调用超类 Employee 中的 getSalary 方法 , 而不是当前类的这个方法,为此, 可以使用特定的关键字 super 解决这个问题 :
super.getSalary();
上述语句调用的是 Employee 类中的 getSalary 方法 。 下面是 Manager 类中 getSalary 方法的正确书写格式 :
public double getSalaryO
{
double baseSalary = super getSalary();
return baseSalary + bonus ;
}
有些人认为 super 与 this 引用是类似的概念 , 实际上, 这样比较并不太恰当 。 这是因为 super 不是一个对象的引用 , 不能将 super 赋给另一个对象变量, 它只是一个指示编译器调用超类方法的特殊关键字。
super不是引用,this是引用自身。
正像前面所看到的那样 , 在子类中可以增加域 、 增加方法或覆盖超类的方法 , 然而绝对不能删除继承的任何域和方法 。
子类构造器
提供一个构造器 。
public Manager (St ring 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 关键字也有两个用途 : 一是调用超类的方法 , 二是调用超类的构造器 。
重新定义 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
Carl Cracker 85000.0
Harry Hacker 50000.0
Tony Tester 40000.0
这里的 staff [1] 和 staff[2]仅输出了基本薪水 , 这是因为它们对应的是 Employee 对象 ,而 staff [ 0 ] 对应的是 Manager 对象 , 它的 getSalary 方法将奖金与基本薪水加在了一起 。
需要提到的是 ,
e.getSalary();
调用能够确定应该执行哪个 getSalary 方法 。 注意 , 尽管这里将 e 声明为 Employee 类型,但实际上 e 既可以引用 Employee 类型的对象, 也可以引用 Manager 类型的对象 。
一个对象变量 ( 例如 , 变量 e ) 可以指示多种实际类型的现象被称为多态 ( polymorphism ) 。
在运行时能够自动地选择调用哪个方法的现象称为动态绑定 ( dynamic binding)。
代码如下:
public class EmployTest {
public static final double CM_PER_INCH = 2.54 ;
public static void main(String[] args)throws Exception {
Manager boss = new Manager (" Carl Cracker " , 80000 , 1987 , 12 , 15 ) ;
boss.setBonus ( 5000 ) ;
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() ) ;
}
}
public class Employee {
public static int nextId = 1;
private int id;
private String name ;
private double salary ;
private LocalDate hireDay;
static {
Random generator = new Random();
nextId = generator.nextInt(10000) ;
}
{
this.id = Employee.nextId;
Employee.nextId ++ ;
}
public Employee() {
}
public Employee(String name , double s , int year , int month , int day ){
this.name = name ;
salary = s ;
hireDay = LocalDate.of( year , month , day ) ;
}
public void setld()
{
this.id = Employee.nextId ;
Employee.nextId++;
}
public String getName(){
return name ;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
this.salary += raise;
System.out.println(salary);
}
@Override
public String toString() {
String s = "name = " + this.getName ()+ " , salary = " + this.getSalary()+ " , hireDay = " + this.getHireDay();
return s;
}
public static int getNextId() {
return Employee.nextId;
}
public int getId() {
return this.id;
}
}
public class Manager extends Employee
{
private double bonus;
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus = b;
}
}
继承层次
继承并不仅限于一个层次 。 例如 , 可以由 Manager 类派生 Executive 类 。 由一个公共超类派生出来的所有类的集合被称为继承层次 ( inheritance hierarchy ) , 如图 5 - 1 所示 。 在继承层次中, 从某个特定的类到其祖先的路径被称为该类的继承链 (inheritance chain ) 。
通常, 一个祖先类可以拥有多个子孙继承链 。 例如, 可以由 Employee 类派生出子类 Programmer 或 Secretary , 它 们 与
Manager 类没有任何关系 ( 有可能它们彼此Employee之间也没有任何关系 )。
Java 不支持多继承。
多态
有一个用来判断是否应该设计为继承关系的简单规则 , 这就是 is a ” 规则 , Employee 继承层次表明子类的每个对象也是超类的对象 。
例如 , 每个经理都是雇员, 因此 , 将 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 等 ) 。
置换法则的优点 :
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 , 而 setBonus 不是 Employee 类的方法 。
然而 , 不能将一个超类的引用赋给子类变量 。 例如 , 下面的赋值是非法的
Manager m = staff [i ] ; / / Error
原因很清楚 : 不是所有的雇员都是经理 。 如果赋值成功 , m 有可能引用了一个不是经理的Employee 对象 , 当在后面调用 m.setBonus( . . . ) 时就有可能发生运行时错误 。
在 Java 中, 子类数组的引用可以转换成超类数组的引用 , 而不需要采用强制类型转换 。 例如 , 下面是一个经理数组
Manager[] managers = new Manager [10];
Employee[] staff = managers;
这样做肯定不会有问题 , 请思考一下其中的缘由 。 毕竟 , 如果 manager [ i ] 是一个Manager , 也一定是一个 Employee 。 然而 , 实际上 , 将会发生一些令人惊讶的事情 。 要切记managers 和 staff 引用的是同一个数组 。 现在看一下这条语句 :
staff [0] = new Employee (“Harry Hacker” ,. . . ) ;
编译器竟然接纳了这个赋值操作 。但在这里 , stafflO ] 与 manager[ 0 ] 引用的是同一个对象, 似乎我们把一个普通雇员擅自归入经理行列中了。这是一种很忌伟发生的情形,当调用managers[0].setBonus(1000 ) 的时候 , 将会导致调用一个不存在的实例域 , 进而搅乱相邻存储空间的内容 。
为了确保不发生这类错误 , 所有数组都要牢记创建它们的元素类型 , 并负责监督仅将类型兼容的引用存储到数组中 。 例如 , 使用 new managers[10] 创建的数组是一个经理数组。
理解方法调用
弄清楚如何在对象上应用方法调用非常重要 。 下面假设要调用 x.f( args) 隐式参数 x 声明为类 C 的一个对象 。 下面是调用过程的详细描述 :
1 ) 编译器査看对象的声明类型和方法名 。 假设调用 x.f( param ) ,且隐式参数 x 声明为 C类的对象 。 需要注意的是 : 有可能存在多个名字为 f , 但参数类型不一样的方法 。 例如 , 可能存在方法 f (int) 和方法 f(String) 。编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法 ( 超类的私有方法不可访问)。至此 , 编译器已获得所有可能被调用的候选方法 。
2 ) 接下来, 编译器将査看调用方法时提供的参数类型 。 如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配, 就选择这个方法 。 这个过程被称为重栽解析 ( overloading resolution ) 。 例如, 对于调用 x . f ( “ Hello ” )来说 , 编译器将会挑选 f( String ) 而不是 f( int )。由于允许类型转换 ( int 可以转换成 double , Manager 可以转换成 Employee , 等等 ,个过程可能很复杂 。 如果编译器没有找到与参数类型匹配的方法, 或者发现经过类型转换后有多个方法与之匹配 , 就会报告一个错误 。
3 ) 如 果 是 private 方法、 static 方法 、 final 方法 (有关 final 修饰符的含义将在下一节讲述 ) 或者构造器 , 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定 ( static binding) 。 与此对应的是, 调用的方法依赖于隐式参数的实际类型, 并且在运行时实现动态绑定 。 在我们列举的示例中 , 编译器采用动态绑定的方式生成一条调用f( String ) 的指令 。
4 ) 当程序运行 , 并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法 。 假设 x 的实际类型是 D , 它是 C 类的子类 。 如果 D 类定义了方法 。就直接调用它 ; 否则 , 将在 D 类的超类中寻找f (String) 以此类推。
每次调用方法都要进行搜索 , 时间开销相当大 。 因此, 虚拟机预先为每个类创建了一个方法表 ( method table ), 其中列出了所有方法的签名和实际调用的方法 。这样一来, 在真正调用方法的时候, 虚拟机仅查找这个表就行了。 在前面的例子中, 虚拟机搜索 D 类的方法表 , 以便寻找与调用 f ( Sting ) 相匹配的方法。
这个方法既有可能是 D.f ( String ) , 也 有 可 能 是X.f ( String ) , 这 里 的 X 是 D 的超类 。这里需要提醒一点, 如果调用 super .f ( param ) , 编译器将对隐式参数超类的方法表进行搜索 。
调用 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(doubl e)
实际上, 上面列出的方法并不完整 , 稍后会看到 Employee 类有一个超类 Object ,Employee 类从这个超类中还继承了许多方法 , 在此 , 我们略去了 Object 方法 。
Manager 方法表稍微有些不同 。 其中有三个方法是继承而来的, 一个方法是重新定义的,还有一个方法是新增加的 。
Manager :
getName()- > Employee.getName()
getSalary() - > Manager.getSalary()
getHireDay() - > Employee.getHireDay()
raiseSalary (double) - > Employee.raiseSalary (double)
setBonus (double) - > Manager.setBonus (double)
在运行时, 调用 e . getSalary()的解析过程为 :
1 ) 首先, 虚拟机提取 e 的实际类型的方法表(看new的对象是Employee还是Manager) 。 既可能是 Employee 、 Manager 的方法表,也可能是 Employee 类的其他子类的方法表 。
2 ) 接下来, 虚拟机搜索定义 getSalary 签名的类 。 此时 , 虚拟机已经知道应该调用哪个方法 。
3 ) 最后, 虚拟机调用方法 。
动态绑定有一个非常重要的特性 : 无需对现存的代码进行修改 , 就可以对程序进行扩展 。假设增加一个新类 Executive , 并且变量 e 有可能引用这个类的对象, 我们不需要对包含调用e . getSalary () 的代码进行重新编译 。 如果 e 恰好引用一个 Executive 类的对象 , 就会自动地调用 Executive . getSalary() 方法。
在覆盖一个方法的时候 , 子类方法不能低于超类方法的可见性 。 特别是 , 如果超类方法是 public , 子类方法一定要声明为 public 。 经常会发生这类错误 : 在声明子类方法的时, 遗漏了 public 修饰符 。 此时 , 编译器将会把它解释为试图提供更严格的访问权限 。
阻止继承: 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 ,而不包括域。
强制类型转换
Java 程序设计语言提供了一种专门用于进行类型转换的表示法 。 例如 :
double x = 3.405 ;
int nx = ( int ) x ;
将表达式 x 的值转换成整数类型 , 舍弃了小数部分。
对象引用的转换语法与数值表达式的类型转换类似, 仅需要用一对圆括号将目标类名括起来 , 并放置在需要转换的对象引用之前就可以了 。 例如 :
Manager boss = (Manager)staff [0] ;
进行类型转换的唯一原因是 : 在暂时忽视对象的实际类型之后, 使用对象的全部功能 。
将一个值存人变量时 , 编译器将检查是否允许该操作 。 将一个了 - 类的引用赋给一个超类变量 , 编译器是允许的 。 但将一个超类的引用赋给一个子类变量, 必须进行类型转换, 这样才能够通过运行时的检査。
如果试图在继承链上进行向下的类型转换, 并且 “ 谎报 ” 有关对象包含的内容, 会发生什么情况呢 ?运行这个程序时, Java 运行时系统将报告这个错误, 并产生一个 ClassCastException异常 。也就是说超类指向了超类的对象,却向下强制转型,就会产生错误。
案例如下:
public static void main(String[] args)throws Exception {
Manager boss = new Manager (" Carl Cracker " , 80000 , 1987 , 12 , 15 ) ;
boss.setBonus (5000) ;
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 );
Manager boss1 = (Manager)staff[0];
Manager boss2 = (Manager)staff[1]; // Error
}
因此 , 应该养成这样一个良好的程序设计习惯 : 在进行类型转换之前, 先查看一下是否能够成功地转换。 这个过程简单地使用instanceof 操作符(前者是否为后者类或者后者类子类的对象)就可以实现 。 例如 :
if ( staff[1] instanceof Manager)
{
boss = ( Manager) staff[1] ;
...
}
最后, 如果这个类型转换不可能成功, 编译器就不会进行这个转换 。 例如, 下面这个类型转换 :
String c = (String) staff [1] ;
将会产生编译错误, 这是因为 String 不是 Employee 的子类 。
综上所述 :
1. 只能在继承层次内进行类型转换 。
2. 在将超类转换成子类之前 , 应该使用 instanceof 进行检查 。
抽象类
如果自下而上在类的继承层次结构中上移, 位于上层的类更具有通用性, 甚至可能更加抽象 。 从某种角度看 , 祖先类更加通用, 人们只将它作为派生其他类的基类, 而不作为想使用的特定的实例类 。
例如, 考虑一下对 Employee 类层次的扩展 。 一名雇员是一个人 , 一名学生也是一个人 。 下面将类 Person 和类 Student 添加到类的层次结构中 。
为什么要花费精力进行这样高层次的抽象呢 ? 每个人都有一些诸如姓名这样的属性 。 学生与雇员都有姓名属性 , 因此可以将 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;
}
}
类中含有abstract方法,必须是抽象类
通过扩展抽象 Person 类, 并实现 getDescription 方法来定义 Student 类 。 由于在Student 类中不再含有抽象方法 , 所以不必将这个类声明为抽象的 。
类即使不含抽象方法, 也可以将类声明为抽象类 。
抽象类不能被实例化 。 也就是说, 如果将一个类声明为 abstract , 就不能创建这个类的对象 。 例如, 表达式
new Person (" Vi nee Vu " )
是错误的, 但可以创建一个具体子类的对象 。
需要注意, 可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象 。 例如,
Person p = new Student (" Vinee Vu " , " Economics " ) ;
这里的 p 是一个抽象类 Person 的变量, Person 引用了一个非抽象子类 Student 的实例 。
下面定义一个扩展抽象类 Person 的具体子类 Student :
public class Student extends Person
{
private String major ;
public Student ( String name , String major )
{
super (name) ;
this.major = major ;
}
public String getDescriptionO
{
return " a student majoring in" + major ;
}
}
在 Student 类中定义了 getDescription 方法 。 因此, 在 Student 类中的全部方法都是非抽象的 , 这个类不再是抽象类 。java中一个类继承一个抽象类,必须实现抽象类中的抽象方法
Person[] people = new Person[2];
people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
people[1] = new Student("Maria Morris", "computer science");
for (Person p : people)
System.out.println(p.getName() + ", " + p.getDescription());
由于不能构造抽象类 Person 的对象 , 所以变量 p 永远不会引用 Person 对象 , 而是引用诸如Employee 或 Student 这样的具体子类对象 , 而这些对象中都定义了 getDescription 方法 。
是否可以省略 Person 超类中的抽象方法 , 而仅在 Employee 和 Student 子类中定义getDescription 方法呢 ? 如果这样的话, 就不能通过变量 p 调用 getDescription 方法了 。 编译器只允许调用在类中声明的方法 。
在 Java 程序设计语言中, 抽象方法是一个重要的概念 。 在接口 ( interface ) 中将会看到更多的抽象方法。
案例程序:
public class PersonTest
{
public static void main(String[] args)
{
Person[] people = new Person[2];
// fill the people array with Student and Employee objects
people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
people[1] = new Student("Maria Morris", "computer science");
// print out names and descriptions of all Person objects
for (Person p : people)
System.out.println(p.getName() + ", " + p.getDescription());
}
}
public abstract class Person {
private String name ;
public Person(){
}
public Person (String name )
{
this.name = name ;
}
public abstract String getDescription();
public String getName()
{
return name;
}
}
public class Student extends Person
{
private String major;
/**
* @param nama the student's name
* @param major the student's major
*/
public Student(String name, String major)
{
// pass n to superclass constructor
super(name);
this.major = major;
}
public String getDescription()
{
return "a student majoring in " + major;
}
}
public class Employee extends Person {
public static int nextId = 1;
private int id;
private String name ;
private double salary ;
private LocalDate hireDay;
public String getDescription() {
return "I am a Employee";
}
static {
Random generator = new Random();
nextId = generator.nextInt(10000) ;
}
{
this.id = Employee.nextId;
Employee.nextId ++ ;
}
public Employee() {
super();
}
public Employee(String name , double s , int year , int month , int day ){
super(name);
this.name = name ;
salary = s ;
hireDay = LocalDate.of( year , month , day ) ;
}
public void setld()
{
this.id = Employee.nextId ;
Employee.nextId++;
}
public String getName(){
return name ;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
this.salary += raise;
System.out.println(salary);
}
@Override
public String toString() {
String s = "name = " + this.getName ()+ " , salary = " + this.getSalary()+ " , hireDay = " + this.getHireDay();
return s;
}
public static int getNextId() {
return Employee.nextId;
}
public int getId() {
return this.id;
}
}
受保护访问
类中的域标记为 private , 而方法标记为 public 。 任何声明为 private的内容对其他类都是不可见的 。 前面已经看到, 这对于子类来说也完全适用 , 即子类也不能访问超类的私有域 。
然而 , 在有些时候, 人们希望超类中的某些方法允许被子类访问 , 或允许子类的方法访问超类的某个域 。 为此 , 需要将这些方法或域声明为 protected 。 例如, 如果将超类 Employee中的 hireDay 声明为 proteced , 而不是私有的 , Manager 中的方法就可以直接地访问它 。
不过 , Manager 类中的方法只能够访问 Manager 对象中的 hireDay 域, 而不能访问其他Employee 对象中的这个域 。 这种限制有助于避免滥用受保护机制, 使得子类只能获得访问受保护域的权利 。
在实际应用中, 要谨慎使用 protected 属性 。 假设需要将设计的类提供给其他程序员使用, 而在这个类中设置了一些受保护域, 由于其他程序员可以由这个类再派生出新类, 并访问其中的受保护域 。 在这种情况下 , 如果需要对这个类的实现进行修改 , 就必须通知所有使用这个类的程序员 。 这违背了 OOP 提倡的数据封装原则 。
Java 用于控制可见性的 4 个访问修饰符 :
1 ) 仅对本类可见----private
2 ) 对所有类可见------public
3 ) 对本包和所有子类可见---------protected 。
4) 对本包可见--------默认,不需要修饰符
Object : 所有类的超类,父类
Object 类是 Java 中所有类的始祖 , 在 Java 中每个类都是由它扩展而来的 。 但是并不需要这样写 :
public class Employee extends Object
如果没有明确地指出超类 , Object 就被认为是这个类的超类 。 由于在 Java 中, 每个类都是由 Object 类扩展而来的 , 所以 , 熟悉这个类提供的所有服务十分重要 。
可以使用 Object 类型的变量引用任何类型的对象 :
Object obj = new EmployeeC ' 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
equals方法
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象 。 在 Object 类中 , 这个方法将判断两个对象是否具有相同的引用 。 如果两个对象具有相同的引用 , 它们一定是相等的 。 从这点上看, 将其作为默认操作也是合乎情理的 。 然而, 对于多数类来说, 这种判断并没有什么意义 。 例如 , 采用这种方式比较两个 PrintStream 对象是否相等就完全没有意义 。然而 , 经常需要检测两个对象状态的相等性 , 如果两个对象的状态相等 , 就认为这两个对象是相等的 。
例如 , 如果两个雇员对象的姓名 、 薪水和雇佣日期都一样 , 就认为它们是相等的 (在实际的雇员数据库中, 比较 ID 更有意义 。 利用下面这个示例演示 equals 方法的实现机制 )。
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 ;
// if the classes don ' t match , they can ' t be equal
if ( getClass() != otherObject.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);
}
getClass 方法将返回一个对象所属的类,在检测中 , 只有在两个对象属于同一个类时, 才有可能相等 。
为了防 备 name 或 hireDay 可 能 为 null 的 情 况, 需 要 使 用 Objects.equals 方 法 。 如果 两 个 参 数 都 为 null , Objects.equals( a , b) 调 用 将 返 回 true ; 如 果 其 中 一 个 参 数 为 null ,则 返 回 false ; 否 则 , 如 果 两 个 参 数 都 不 为 null , 则 调 用 a .equals( b ) 利用这个方法,Employee.equals方法的最后一条语句要改写为 :
Employee.equals 方法的最后一条语句要改写为 :
return Objects . equals ( name , other.name )
&& salary == other.salary
&& Object.equals (hireDay , other.hireDay ) ;
在Manager子类中定义 equals 方法时 , 首先调用超类的 equals 。 如果检测失败 , 对象就不可能相等 。 如果超类中的域都相等 , 就需要比较子类中的实例域 。
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 ;
}
相等测试与继承
如果隐式和显式的参数不属于同一个类 , 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 ) 返 N true , y . equals ( z ) 返回 true ,x.equals ( z ) 也应该返回 true 。
4 ) 一致性: 如果 x 和 y 引用的对象没有发生变化 , 反复调用 x . eqimIS ( 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 , 或者抛出异常 :
下面可以从两个截然不同的情况看一下这个问题 :
- 如果子类能够拥有自己的相等概念 , 则对称性需求将强制采用 getClass 进行检测。
- 如果由超类决定相等的概念, 那么就可以使用 imtanceof 进行检测, 这样可以在不同子类的对象之间进行相等的比较 。
在雇员和经理的例子中, 只要对应的域相等, 就认为两个对象相等 。 如果两个 Manager对象所对应的姓名 、 薪水和雇佣日期均相等 , 而奖金不相等, 就认为它们是不相同的, 因此, 可以使用 getClass 检测 。
但是, 假设使用雇员的 ID 作为相等的检测标准 , 并且这个相等的概念适用于所有的子类, 就可以使用 instanceof 进行检测, 并应该将 Employee.equals 声明为 final 。
在 标 准 Java 库 中 包 含 150 多个equals 方 法 的 实 现 ,包 括 使 用 instanceof 检测 、调用 getClass 检测 、 捕获 ClassCastException 或 者 什 么 也 不 做 。 可 以 查 看 java.sql .Timestamp 类的 API 文档 , 在这里实现人员不无尴尬地指出 , 他们使自己陷入了困境 。Timestamp 类继承自java.util.Date , 而 后者的equals 方 法 使 用 了 一 个 instanceof 测试 , 这样一来就无法覆盖实现 equals 使之同时做到对称且正确 。
下面给出编写一个完美的 equals 方法的建议 :
- 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other 的变量 。
- 检测 this 与 otherObject 是否引用同一个对象 :
if (this = otherObject) return true ;
这条语句只是一个优化 。 实际上 , 这是一种经常采用的形式 。 因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多 。
3 ) 检测 otherObject 是否为 null , 如 果 为 null , 返 回 false 。 这项检测是很必要的 。
if (otherObject == null ) return false;
4 ) 比较 this 与 otherObject 是否属于同一个类 。 如果 equals 的语义在每个子类中有所改变, 就使用 getClass 检测 :
if (this.getClass() != otherObject.getClass() return false; // getClass返回运行时的类名
如果所有的子类都拥有统一的语义 , 就使用 instanceof 检测 :
if (! ( otherObject instanceof ClassName)) return false;
5 ) 将 otherObject 转换为相应的类类型变量 :
ClassName other = (ClassName) otherObject
6 ) 现在开始对所有需要比较的域进行比较了 。 使用 = 比较基本类型域 , 使用 equals 比较对象域 。 如果所有的域都匹配 , 就返回 true ; 否则返回 false 。
return field = = other field
&& Objects.equals(fie1d2 , other. ield 2)
&& ...
如果在子类中重新定义 equals , 就要在其中包含调用 super.equals(other) 。
下面是实现 equals 方法的一种常见的错误。
public class Employee
{
public boolean equals ( Employee other)
{
return other != null
&& getClassO == other . getClass 0
&& Objects . equals ( name , other . name )
&& salary
other , sal ary
&& Objects . equals ( hireDay , other.hireDay ) ;
}
}
这个方法声明的显式参数类型是 Employee 。 其结果并没有覆盖 Object 类的 equals 方法 , 而是定义了一个完全无关的方法 。为了避免发生类型错误, 可以使用 @ Override 对覆盖超类的方法进行标记 :
@Override public boolean equals ( Object other )
hashCode 方法
散列码 ( hash code)是由对象导出的一个整型值。散列码是没有规律的 如果 x 和 y 是两个不同的对象 , x.hashCode( ) 与 y.hashCode( ) 基本上不会相同 。在表 中列出几个通过调用 String 类hashCode 方法得到的散列码 。
String 类使用下列算法计算散列码 :
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
由于 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 (1) ;
System.out.println(t.hashCode() + " " + tb.hashCode ()) ;
请注意, 字符串 s 与 t 拥有相同的散列码, 这是因为字符串的散列码是由内容导出的 。 而字符串缓冲 sb 与 tb 却有着不同的散列码, 这是因为在 StringBuffer 类中没有定义hashCode 方法 , 它的散列码是由 Object 类的默认 hashCode 方法导出的对象存储地址 。
如果重新定义 equals 方法 , 就必须重新定义 hashCode 方法, 以便用户可以将对象插人到散列表中
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 方法计算一个散列值, 这个散列码由数组元素的散列码组成 。
toString 方法
在 Object 中还有一个重要的方法, 就是 toString 方法, 它用于返回表示对象值的字符串 。 下面是一个典型的例子 。 Point 类的 toString 方法将返回下面这样的字符串 :
Point po = new Point();
System.out.println(people[0].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 方法 , 并将子类域的描述添加进去 。如果超类使用了 getClass( ) . getName( ) , 那么子类只要调用 super.toString( ) 就可以了 。 例如,下面是 Manager 类中的 toString 方法 :
public String toString()
{
...
return super.toString()
+ "[bonus = " + bonus
+ "]";
}
现在 , Manager 对象将打印输出如下所示的内容 :
javabase.Manager [name = Harry Hackersalary: 500.0 , hireDay = 1989-10-01][bonus = 0.0]
随处可见 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 , 11, 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 ) ;
更好的解决方法是 :
Logger.global.info ( " Current position = " + position ) ;
强烈建议为自定义的每一个类增加 toString 方法 。 这样做不仅自己受益 , 而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅 。
程序实现了 Employee 类 和 Manager 类 的 equals 、 hashCode 和 toString 方法 。
public class Employee extends Person {
public static int nextId = 1;
private int id;
private String name ;
private double salary ;
private LocalDate hireDay;
public String getDescription() {
return "I am a Employee";
}
static {
Random generator = new Random();
nextId = generator.nextInt(10000) ;
}
{
this.id = Employee.nextId;
Employee.nextId ++ ;
}
public Employee() {
super();
}
public Employee(String name , double s , int year , int month , int day ){
super(name);
this.name = name ;
salary = s ;
hireDay = LocalDate.of( year , month , day ) ;
}
public void setld()
{
this.id = Employee.nextId ;
Employee.nextId++;
}
public String getName(){
return name ;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
this.salary += raise;
System.out.println(salary);
}
@Override
public String toString()
{
return getClass().getName()
+ " [name = " + name
+ "salary: " + salary
+ " , hireDay = " + hireDay
+ "]";
}
public static int getNextId() {
return Employee.nextId;
}
public int getId() {
return this.id;
}
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 ;
// if the classes don ' t match , they can ' t be equal
if ( this.getClass() != otherObject.getClass ())
return false ;
// now we know otherObject is a non - null Employee
Employee other = (Employee) otherObject ;
// test whether the fields have identical values
return Objects.equals(name , other.name)
&& salary == other. salary
&& Objects.equals(hireDay, other.hireDay);
}
public int hashCode()
{
return Objects.hash( name , salary, hireDay) ;
}
}
public class Manager extends Employee
{
private double bonus;
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus = b;
}
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 ;
}
public String toString()
{
return super.toString()
+ "[bonus = " + bonus
+ "]";
}
}
泛型数组列表
在许多程序设计语言中, 特别是在 C ++ 语言中, 必须在编译时就确定整个数组的大小 。程序员对此十分反感 , 因为这样做将迫使程序员做出一些不情愿的折中 。例如 , 在一个部门中有多少雇员 ? 肯定不会超过100 人 。 一旦出现一个拥有 150 名雇员的大型部门呢 ? 愿意为那些仅有 10 名雇员的部门浪费 90 名雇员占据的存储空间吗?
在 Java 中 , 情况就好多了 。 它允许在运行时确定数组的大小 。
int actual Size = . . . ;
Employee[] staff = new Employee [actual Size ] ;
当然, 这段代码并没有完全解决运行时动态更改数组的问题 。 一旦确定了数组的大小, 改变它就不太容易了 。 在 Java 中 , 解决这个问题最简单的方法是使用 Java 中另外一个被称为ArrayList 的类 。 它使用起来有点像数组 , 但在添加或删除元素时 , 具有自动调节数组容量的功能 , 而不需要为此编写任何代码 。
ArrayList 是一个采用类型参数 ( type parameter ) 的泛型类 ( generic class ) 。 为了指定数组列表保存的元素对象类型, 需要用一对尖括号将类名括起来加在后面, 例如 , ArrayList< Employee >
下面声明和构造一个保存 Employee 对象的数组列表 :
ArrayList<Employee> staff = new ArrayList< Eniployee> () ;
两边都使用类型参数 Employee , 这有些繁琐 。 Java SE 7 中, 可以省去右边的类型参数 :
ArrayList<Employee> staff = new ArrayList() ;
这被称为 “ 菱形 ” 语法 ,因为空尖括号 <> 就像是一个菱形 。可以结合 new 操作符使用菱形语法 。 编译器会检查新值是什么 。 如果赋值给一个变量 , 或传递到某个方法, 或者从某个方法返回 , 编译器会检査这个变量 、 参数或方法的泛型类型 ,然后将这个类型放在 <>中 。在这个例子中 , new ArrayList<> 将赋至一个类型为 ArrayList < Employee > 的变量 , 所以泛型类型为 Employee 。
使用 add 方法可以将元素添加到数组列表中 。 例如 , 下面展示了如何将雇员对象添加到数组列表中的方法 :
staff . add ( new Employee ("Harry Hacker" , ...)) ;
staff . add ( new Eraployee ("Tony Tester" , . . . )) ;
数组列表管理着对象引用的一个内部数组 。 最终, 数组的全部空间有可能被用尽 。 这就显现出数组列表的操作魅力 : 如果调用 add 且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中 。
如果已经清楚或能够估计出数组可能存储的元素数量 , 就可以在填充数组之前调用ensureCapacity 方法 :
staff ensureCapacity(100) ;
这个方法调用将分配一个包含 100 个对象的内部数组 。 然后调用 100 次 add , 而不用重新分配空间 。
另外 , 还可以把初始容量传递给 ArrayList 构造器 :
ArrayList < Employee > staff = new ArrayList(100) ;
分配数组列表 , 如下所示 :
new ArrayListo (100) // capacity is 100
它与为新数组分配空间有所不同 :
new Employee[ 100] / / size is 100
数组列表的容量与数组的大小有一个非常重要的区别 。 如果为数组分配 100 个元素的存储空间 , 数组就有 100 个空位置可以使用 。 而容量为 100 个元素的数组列表只是拥有保存 100 个元素的潜力 ( 实际上 , 重新分配空间的话 , 将会超过100 ) , 但是在最初 ,甚至完成初始化构造之后, 数组列表根本就不含有任何元素 。
size 方法将返回数组列表中包含的实际元素数目 。 例如 ,
staff.size()
将返回 staff 数组列表的当前元素数量, 它等价于数组 a 的 a.length 。
一旦能够确认数组列表的大小不再发生变化, 就可以调用 trimToSize 方法 。 这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目 。 垃圾回收器将回收多余的存储空间 。
访问数组列表元素
很遗憾 , 天下没有免费的午餐 。 数组列表自动扩展容量的便利增加了访问元素语法的复杂程度 。 其原因是 ArrayList 类并不是 Java 程序设计语言的一部分; 它只是一个由某些人编写且被放在标准库中的一个实用类 。
使用 get 和 set 方法实现访问或改变数组元素的操作 , 而不使用人们喜爱的 [ ] 语法格式 。
例如, 要设置第 i 个元素, 可以使用 :
staff.set (i , harry);
它等价于对数组 a 的元素赋值 ( 数组的下标从 0 开始 ):
a[i] = harry;
只有 i 小于或等于数组列表的大小时, 才能够调用list.set ( i ,x )。
使用下列格式获得数组列表的元素 :
Employee e = staff.get (i);
等价于 :
Employee e = a [i] ;
除了在数组列表的尾部追加元素之外, 还可以在数组列表的中间插入元素 , 使用带索引参数的 add 方法:
int n = staff.size() // 2 ;
staff.add(n,e);
为了插人一个新元素, 位于 n 之后的所有元素都要向后移动一个位置 。 如果插人新元素后 , 数组列表的大小超过了容量 , 数组列表就会被重新分配存储空间 。
同样地, 可以从数组列表中间删除一个元素 。
Employee e = staff.remove(n) ;
可以使用 “ foreach ” 循环遍历数组列表 :
for(Employee e : al){
do something with e;
}
类型化与原始数组列表的兼容性
在你自己的代码中 , 你可能更愿意使用类型参数来增加安全性 。 这一节中, 你会了解如何与没有使用类型参数的遗留代码交互操作 。
假设有下面这个遗留下来的类 :
public class EmployeeDB
{
public void update ( ArrayList list ) {. . . }
public ArrayList find (String query ) {.. . }
}
可以将一个类型化的数组列表传递给 update 方法 , 而并不需要进行任何类型转换 。
ArrayList<Employee 〉 staff = ...;
employeeDB.update (staff);
相反地, 将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告 。
ArrayList<Employee> result = employeeDB find ( query ) ; // yields warning
使用类型转换并不能避免出现警告 。
ArrayList < Employee > result = ( ArrayLi st < Empl oyee >) employeeDB . find ( query ) ;
这样 , 将会得到另外一个警告信息 , 指出类型转换有误 。
这就是 Java 中不尽如人意的参数化类型的限制所带来的结果 。 鉴于兼容性的考虑 , 编译器在对类型转换进行检査之后, 如果没有发现违反规则的现象 , 就将所有的类型化数组列表转换成原始 ArrayList 对象 。 在程序运行时, 所有的数组列表都是一样的, 即没有虚拟机中的类型参数 。
对象包装器与自动装箱
有时, 需要将 int 这样的基本类型转换为对象 。 所有的基本类型都有一个与之对应的类 。
例如,
Integer 类对应基本类型 int 。通常 , 这些类称为包装器 ( wrapper ) 这些对象包装器类拥有很明显的名字 : Integer 、 Long 、 Float 、 Double 、 Short 、 Byte 、 Character 、 Void 和 Boolean ( 前6 个类派生于公共的超类 Number ) 。对象包装器类是不可变的, 即一旦构造了包装器, 就不允许更改包装在其中的值 。 同时, 对象包装器类还是 final , 因此不能定义它们的子类 。
假设想定义一个整型数组列表 。 而尖括号中的类型参数不允许是基本类型 , 也就是说 ,不允许写成 ArrayList < int > 。 这里就用到了 Integer 对象包装器类 。 我们可以声明一个 Integer对象的数组列表 。
ArrayList <Integer > list = new ArrayList <> ( ) ;
幸运的是 , 有一个很有用的特性 , 从而更加便于添加 int 类型的元素到 ArrayList中 。 下面这个调用
list.add (3) ;
将自动地变换成
list.add (Integer . value0f (3 )) ;
这种变换被称为自动装箱 ( autoboxing)。
相反地 , 当将一个 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.printing(2 * n) ;
另外, 如果在一个条件表达式中混合使用 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 类中包含的一些重要方法 。 其他数值类也实现了相应的方法 。
参数数量可变的方法
在 Java SE 5.0 以前的版本中, 每个 Java 方法都有固定数量的参数 。 然而, 现在的版本提供了可以用可变的参数数量调用的方法 (有时称为 “ 变参 ” 方法 )。
前面已经看到过这样的方法 : printf 例如, 下面的方法调用 :
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 ( "%d %s" , new Object[] {new Integer (n) , " widgets" } ) ;
用户自己也可以定义可变参数的方法, 并将参数指定为任意类型 , 甚至是基本类型 。 下面是一个简单的示例 : 其功能为计算若干个数值的最大值 。
public static double max (double... values )
{
double largest = Double NECATIVEJNFINITY ;
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(1) , " widgets " } ) ;
因此 , 可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码 。 例如 , MessageFormat . format 在 Java SE 5.0 就采用了这种方式 。 甚至可以将 main 方法声明为下列形式 :
public static void main (String... args)
枚举类
如何定义枚举类型 。 下面是一个典型的例子 :
public enum Size { SMALL , MEDIUM , LARGE , EXTRAJARGE } ;
实际上, 这个声明定义的类型是一个类 , 它刚好有 4 个实例 , 在此尽量不要构造新对象 。
编译之后相当于:
public final class Size extends Enum{
}
因此 , 在比较两个枚举类型的值时 , 永远不需要调用 equals , 而直接使用 “ == ” 就可以了 。
如果需要的话 , 可以在枚举类型中添加一些构造器 、 方法和域。 当然 , 构造器只是在构造枚举常量的时候被调用 。 下面是一个示例 :
如果需要的话 , 可以在枚举类型中添加一些构造器 、 方法和域。 当然 , 构造器只是在构造枚举常量的时候被调用 。 下面是一个示例 :
public enum Size
{
SMALLfS("S") , MEDIUMC("M") , LARGEfL("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 ” 。
反射
反射库 ( reflection library ) 提供了一个非常丰富且精心设计的工具集 , 以便编写能够动态操纵 Java 代码的程序 。
能够分析类能力的程序称为反射 ( reflective ) 。 反射机制的功能极其强大 , 在下面可以看到, 反射机制可以用来 :
• 在运行时分析类的能力 。
• 在运行时查看对象 , 例如, 编写一个 toString 方法供所有类使用 。
• 实现通用的数组操作代码 。
• 利用 Method 对象, 这个对象很像中的函数指针 。
反射是一种功能强大且复杂的机制 。 使用它的主要人员是工具构造者, 而不是应用程序员
Class 类
在程序运行期间, Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识 。这个信息跟踪着每个对象所属的类 。 虚拟机利用运行时类型信息选择相应的方法执行 。
然而, 可以通过专门的 Java 类访问这些信息 。 保存这些信息的类被称为 Class , 这个名字很容易让人混淆 。
Object 类中的 getClass( ) 方法将会返回一个 Class 类型的实例 。
如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性 。 最常用的 Class 方法是getName 。 这个方法将返回类的名字 。
Employee e = new Employee("aaa", 110);
System.out.println(e.getClass().getName()); // 完整类名
还可以调用静态方法 forName 获得类名对应的 Class 对象 。
Employee e = new Employee("aaa", 110);
try {
Class cls = Class.forName("org.javacore.Employee");
System.out.println(cls.getName());
} catch (Exception e2) {
// TODO: handle exception
}
还可以使用类关键字class
利用反射分析类的能力
在 java.lang.reflect 包中有三个类 Field 、 Method 和 Constructor 分别用于描述类的域 、 方法和构造器 。
这三个类都有一个叫做 getName 的方法, 用来返回名称 。
Field 类有一个 getType 方法, 用来返回描述域所属类型的 Class 对象 。
**这三个类还有一个叫做 getModifiers 的方法 , 它将返回一个整型数值, 用不同的位开关描述 public 和 static 这样的修饰符使用状况 。**它将返回一个整型数值, 用不同的位开关描述 public 和 static 这样的修饰符使用状况 。
还可以利用 java.lang. reflect包中的 Modifier 类的静态方法分析getModifiers 返回的整型数值 。Class类也有getModifiers方法
例如, 可以使用 Modifier 类中的 isPublic 、 isPrivate 或 isFinal判断方法或构造器是否是 public ,private 或 final 。 我们需要做的全部工作就是调用 Modifier类的相应方法, 并对返回的整型数值进行分析, 另外 , 还可以利用 Modifier.toString(getModifiers )方法将修饰符打印出来 。
例如:
public class Main {
public static void main(String[] args) {
Employee e = new Employee("aaa", 110);
try {
Class cls = Class.forName("org.javacore.Employee");
System.out.println(cls.getName());
} catch (Exception e2) {
// TODO: handle exception
}
Class cls = Employee.class;
System.out.println(cls.getName());
System.out.println(Modifier.toString(cls.getModifiers()));
}
}
// org.javacore.Employee
// org.javacore.Employee
Class 类中的 getFields 、 getMethods 和 getConstructors 方 法 将 分 别 返 回 类 提 供 的public 域 、 方法和构造器数组, 其中包括超类的公有成员 。
Class 类的 getDeclareFields 、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域 、 方法和构造器, 其中包括私有和受保护成员, 但不包括父类的成员 。
案例:
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 final int id = 1;
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);
// 除去父类Object的父类
if (supercl != null && supercl != Object.class) System.out.print(" extends "
+ supercl.getName());
System.out.print("\n{\n");
// 打印
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.getDeclaredConstructors();
for (Constructor c : constructors)
{
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(name + "(");
// 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.getDeclaredMethods();
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.getDeclaredFields();
for (Field f : fields)
{
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.println(type.getName() + " " + name + ";");
}
}
}
在运行时使用反射分析对象
查看任意对象的数据域名称和类型 :
• 获得对应的 Class 对象
• 通过 Class 对象调用 getDeclaredFields
查看数据域的实际内容 。 在编写程序时, 如果知道想要査看的域名和类型, 查看指定的域是一件很容易的事情 。 而利用反射机制可以查看在编译时还不清楚的对象域 。
查看对象域的关键方法是 Field 类中的 get 方法 。也就是查看对象中的属性值。
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) ;
实际上 , 这段代码存在一个问题 。 由于 name 是一个私有域, 所以 get 方法将会抛出一个IllegalAccessException 。 只有利用 get 方法才能得到可访问域的值 。 除非拥有访问权限, 否则Java 安全机制只允许査看任意对象有哪些域, 而不允许读取它们的值 。
反射机制的默认行为受限于 Java 的访问控制 。 然而 , 如果一个 Java 程序没有受到安全管理器的控制 , 就可以覆盖访问控制 。 为了达到这个目的, 需要调用 Field 、 Method 或Constructor 对象的 setAccessible 方法 。 例如,
f . setAtcessible (true) ; / / now OK to call f . get ( harry) ;
public static void main(String[] args) throws Exception
{
/** ArrayList<Integer> squares = new ArrayList<>();
for (int i = 1; i <= 5; i++)
squares.add(i * i);
System.out.println(new ObjectAnalyzer().toString(squares));
**/
Employee ee = new Employee("llll" , 5000 , 1972 , 12 ,12);
Class cls1 = ee.getClass();
Field f1 = cls1.getDeclaredField("name");
f1.setAccessible(true);
Object obj = f1.get(ee);
System.out.println(obj);
}