第五章 继承

人们可以基于已存在的类构造一个新类。继承已存在的类就 是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域, 以满足新 的需求。

 

类、超类和子类

定义子类

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

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

 

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

关键字 extends 表明正在构造的新类派生于一个已存在的类。 已存在的类称为超类 ( superclass)、 基类(base class) 或父类(parent class); 新类称为子类(subclass)、 派生类 (derivedclass) 或孩子类(child class)。

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

在 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 并不一定适用。具体来说, Manager 类中的 getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override) 超类中的这个方法: public class Manager

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

 

应该如何实现这个方法呢? 只要返回 salary 和 bonus 域的总和就 可以了:

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

 

 

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

在子类中可以增加域、 增加方法或覆盖超类的方法,然而绝对 不能删除继承的任何域和方法。

子类构造器

我们来提供一个构造器。

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) 的构造器。

 

[提示]  : 在 Java 中, 不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如 果不希望让一个方法具有虚拟特征, 可以将它标记为 final。

程序清单 5-1 的程序展示了 Employee 对象(程序清单 5-2 ) 与 Manager (程序清单 5-3 )对象在薪水计算上的区別。 程序清单 5-1 inheritance/ManagerTest.java

//程序清单 5-1 inheritance/ManagerTest.java
package inheritance;
​
/**
 * This program demonstrates inheritance.
 * @version 1.21 2004-02-21
 * @author Cay Horstmann
 */
public class ManagerTest
{
   public static void main(String[] args)
   {
      // construct a Manager object
      var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
      boss.setBonus(5000);
​
      var staff = new Employee[3];
​
      // fill the staff array with Manager and Employee objects
​
      staff[0] = boss;
      staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
      staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
​
      // print out information about all Employee objects
      for (Employee e : staff)
         System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
   }
}
​
//程序清单 5-2 inheritance/Employee.java
package inheritance;
​
import java.time.*;
​
public class Employee
{
   private String name;
   private double salary;
   private LocalDate hireDay;
​
   public Employee(String name, double salary, int year, int month, int day)
   {
      this.name = name;
      this.salary = salary;
      hireDay = LocalDate.of(year, month, day);
   }
​
   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;
      salary += raise;
   }
}
​
//程序清单 5-3 inheritance/Manager.java
package inheritance;
​
public class Manager extends Employee
{
   private double bonus;
​
   /**
    * @param name the employee's name
    * @param salary the salary
    * @param year the hire year
    * @param month the hire month
    * @param day the hire day
    */
   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)。

多 态

有一个用来判断是否应该设计为继承 关系的简单规则, 这就是“ is-a” 规则, 它 表明子类的每个对象也是超类的对象。

“ 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; 

 

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

这意味着, 可以这样调用

boss.setBonus(5000); // OK 

但不能这样调用

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

 

理解方法调用

下面是调用过程的详细描述:

1 ) 编译器査看对象的声明类型和方法名。假设调用 x.f(param),且隐式参数 x声明为 C 类的对象。需要注意的是:有可能存在多个名字为 f, 但参数类型不一样的方法。

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

2 ) 接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在 一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重栽解析(overloading resolution)。

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

3 ) 如果是 private 方法、 static 方法、final 方法(有关 final 修饰符的含义将在下一节讲 述)或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称 为静态绑定(static binding)。

4 ) 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实 际类型最合适的那个类的方法。

Manager方法表稍微有些不同。其中有三个方法是继承而来的,一个方法是重新定义的, 还有一个方法是新增加的。

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

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

1 ) 首先,虚拟机提取 e 的实际类型的方法表。

2 ) 接下来, 虚拟机搜索定义 getSalary 签名的类。

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

 

阻止继承:final 类和方法

不允许扩展的类被称为 final 类。如果 在定义类的时候使用了 final 修饰符就表明这个类是 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];

 

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

这个过程简单地使用 instanceof操作符就可以实现。 例如:

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

 

如果这个类型转换不可能成功, 编译器就不会进行这个转换。

 

进行强制类型转换需要注意以下两点:

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

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

 

如果 x 为 null, 进行下列测试 x instanceof C 不会产生异常, 只是返回 false。之所以这样处理是因为 null 没有引用任何对象, 当 然也不会引用 C 类型的对象。

 

抽象类

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

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

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 p = new Student("Vinee Vu", "Economics"); 

 

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

//程序清单 5-4 abstractClasses/PersonTest.java
package abstractClasses;
​
/**
 * This program demonstrates abstract classes.
 * @version 1.01 2004-02-21
 * @author Cay Horstmann
 */
public class PersonTest
{
   public static void main(String[] args)
   {
      var 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());
   }
}

 


//程序清单 5-5 abstractClasses/Person.java
package abstractClasses;
​
public abstract class Person
{
   public abstract String getDescription();
   private String name;
​
   public Person(String name)
   {
      this.name = name;
   }
​
   public String getName()
   {
      return name;
   }
}
​

 

//程序清单 5-6 abstractClasses/Employee.java
package abstractClasses;
​
import java.time.*;
​
public class Employee extends Person
{
   private double salary;
   private LocalDate hireDay;
​
   public Employee(String name, double salary, int year, int month, int day)
   {
      super(name);
      this.salary = salary;
      hireDay = LocalDate.of(year, month, day);
   }
​
   public double getSalary()
   {
      return salary;
   }
​
   public LocalDate getHireDay()
   {
      return hireDay;
   }
​
   public String getDescription()
   {
      return String.format("an employee with a salary of $%.2f", salary);
   }
​
   public void raiseSalary(double byPercent)
   {
      double raise = salary * byPercent / 100;
      salary += raise;
   }
}

//程序清单 5-7 abstractClasses/Student.java
package abstractClasses;
​
public class Student extends Person
{
   private String major;
​
   /**
    * @param name the student's name
    * @param major the student's major
    */
   public Student(String name, String major)
   {
      // pass name to superclass constructor
      super(name);
      this.major = major;
   }
​
   public String getDescription()
   {
      return "a student majoring in " + major;
   }
}
​

 

受保护访问

大家都知道,最好将类中的域标记为 private, 而方法标记为 public。任何声明为private 的内容对其他类都是不可见的。前面已经看到, 这对于子类来说也完全适用,即子类也不能 访问超类的私有域。

受保护的方法更具有实际意义。如果需要限制某个方法的使用, 就可以将它声明为 protected。这表明子类(可能很熟悉祖先类)得到信任,可以正确地使用这个方法,而其他 类则不行。

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

1 ) 仅对本类可见 private。

2 ) 对所有类可见 public。

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

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

 

Object: 所有类的超类

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

public class Employee extends Object

如果没有明确地指出超类,Object 就被认为是这个类的超类。

equals 方法

Object 类中的 equals方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这 个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用, 它们一定是相 等的。

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

public class Employee{
​
...
​
public boolean equals(Object otherObject) {
​
 // a quick test to see if the objects are identical 
if (this == otherObject) return true;
​
// must return false if the explicit parameter is null 
if (otherObject == null) return false;
// if the classes don't match, they can't be equal 
if (getClassO != 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 
​
&& hi reDay.equals(other,hi reDay):
​
•   }
​
}

 

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方法就返冋 false: 但是,许多程序员 却喜欢使用 instanceof进行检测:

if (KotherObject instanceof Employee)) return false;

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

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

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。

 

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

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

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

 

hashCode 方法

散列码( hash code) 是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是 两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同。

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

int hash = 0;

for (int i = 0; i < length0;i++)

hash = 31 * hash + charAt(i);

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

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

程序清单 5-8 的程序实现了 Employee 类(程序清单 5-9 ) 和 Manager•类(程序清单 5-10 ) 的 equals、hashCode 和 toString方法。

//程序清单 5-8 equals/EqualsTest.java
package equals;
​
/**
 * This program demonstrates the equals method.
 * @version 1.12 2012-01-26
 * @author Cay Horstmann
 */
public class EqualsTest
{
   public static void main(String[] args)
   {
      var alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
      var alice2 = alice1;
      var alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
      var bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);
​
      System.out.println("alice1 == alice2: " + (alice1 == alice2));
​
      System.out.println("alice1 == alice3: " + (alice1 == alice3));
​
      System.out.println("alice1.equals(alice3): " + alice1.equals(alice3));
​
      System.out.println("alice1.equals(bob): " + alice1.equals(bob));
​
      System.out.println("bob.toString(): " + bob);
​
      var carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
      var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
      boss.setBonus(5000);
      System.out.println("boss.toString(): " + boss);
      System.out.println("carl.equals(boss): " + carl.equals(boss));
      System.out.println("alice1.hashCode(): " + alice1.hashCode());
      System.out.println("alice3.hashCode(): " + alice3.hashCode());
      System.out.println("bob.hashCode(): " + bob.hashCode());
      System.out.println("carl.hashCode(): " + carl.hashCode());
   }
}

 

//程序清单 5-9 equals/Employee.java
package equals;
​
import java.time.*;
import java.util.Objects;
​
public class Employee
{
   private String name;
   private double salary;
   private LocalDate hireDay;
​
   public Employee(String name, double salary, int year, int month, int day)
   {
      this.name = name;
      this.salary = salary;
      hireDay = LocalDate.of(year, month, day);
   }
​
   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;
      salary += raise;
   }
​
   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
      var 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 String toString()
   {
      return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" 
         + hireDay + "]";
   }
}

 
//程序清单 5-10 equals/Manager.java
package equals;
​
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 bonus)
   {
      this.bonus = bonus;
   }
​
   public boolean equals(Object otherObject)
   {
      if (!super.equals(otherObject)) return false;
      var other = (Manager) otherObject;
      // super.equals checked that this and other belong to the same class
      return bonus == other.bonus;
   }
​
   public int hashCode()
   {
      return java.util.Objects.hash(super.hashCode(), bonus);
   }
​
   public String toString()
   {
      return super.toString() + "[bonus=" + bonus + "]";
   }
}
​

 

 

泛型数组列表

ArrayList 是一个采用类型参数(type parameter) 的泛型类(generic class)。为了指定数 组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面, 例如,ArrayList <Employee>

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

ArrayList<Employee> staff = new ArrayList<Eniployee>0;

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

ArrayList<Employee> staff = new ArrayListoQ;

这被称为“ 菱形” 语法,因为空尖括号<>就像是一个菱形。

访问数组列表元素

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

使用 get 和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的 [ ]语法格式。 例如,要设置第 i 个元素,可以使用:

staff.set(i, harry):

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

a[i] = harry;

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

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

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

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

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

 

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

•不必指出数组的大小。

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

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

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

//程序清单 5-11 arrayList/ArrayListTestjava
package arrayList;
​
import java.util.*;
​
/**
 * This program demonstrates the ArrayList class.
 * @version 1.11 2012-01-26
 * @author Cay Horstmann
 */
public class ArrayListTest
{
   public static void main(String[] args)
   {
      // fill the staff array list with three Employee objects
      var staff = new ArrayList<Employee>();
​
      staff.add(new Employee("Carl Cracker", 75000, 1987, 12, 15));
      staff.add(new Employee("Harry Hacker", 50000, 1989, 10, 1));
      staff.add(new Employee("Tony Tester", 40000, 1990, 3, 15));
​
      // raise everyone's salary by 5%
      for (Employee e : staff)
         e.raiseSalary(5);
​
      // print out information about all Employee objects
      for (Employee e : staff)
         System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" 
            + e.getHireDay());
   }
}

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

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

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。

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

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

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

posted on 2020-08-08 21:36  ♌南墙  阅读(156)  评论(0)    收藏  举报