5继承
5.1 类、超类和子类
重用部分代码,并保留所有域。“is-a”关系,用extends表示。
已存在的类被称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。子类比父类封装了更多的数据,拥有更多的功能。
在设计类时,尽可能将通用的功能放到父类中,具体特殊用途的放到子类中。有时需要提供一个新的方法来覆盖(override)父类中的方法。例:
class Manager extends Employee{
/**
Manager继承于Employee类,添加了bonus属性和setBonus方法。
/
private double bonus;
...
public void setBonus(double b){
bonus = b;
}
}
Manager类中添加方法覆盖Employee中的getSalary()方法:
public double getSalary(){
return salary+bonus;//won't work
}
该方法无法运行,因为子类中的方法不能直接地访问父类的私有域*,如果要访问,必须借助公有的接口。改为:
public double getSalary(){
double baseSalary = getSalary(); //still won't work
return baseSalary + bonus;
}
仍然无法运行,因为Manager类也有一个getSalary方法,所以该语句会无限次调用自己。所以正确的格式为:
public double getSalary(){
//super关键字指明调用父类中的getSalary方法,而不是当前类的这个方法
double baseSalary = super.getSalary();
return baseSalary + bonus;
}在子类中可以增加域、增加方法或覆盖超类的方法,然而绝不能删除任何继承的方法和域。
super在构造器中的应用:
public Manager(String n, double s, int year, int month, int day){
super(n,s,year,month,day);
bonus = 0;
}
super指“调用超类Employee中含有n、s、year、month和day参数的构造器”。我们可以通过super实现对超类构造器的调用,使用super调用构造器的语句必须是子类构造器的第一条语句。如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则编译报错。this和super很像。this的两个功能:1)引用隐式参数;2)调用该类其他结构的构造器。super的两个用途:1)调用超类的方法;2)调用超类的构造器。
Manager boss = new Manager("Carl", 80000, 1987, 12, 15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony", 40000, 1990, 3, 15);这里的staff[1]和[2]都是Employee对象,staff[0]是Manager对象。(Li:子类对象可以被认为是父类对象,反之不可。)
boss引用Employee对象时,boss.getSalary()调用的是Employee中的getSalary方法;当boss引用Manager对象时,boss.getSalary()调用的是Manager中的getSalary方法。一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
.1继承层次
- 继承不仅限于一个层次。由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。在继承层次中,某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。
- 通常,一个祖先可以拥有多个子孙继承链。Java不支持多继承。
.2多态
- 一个判断是否应该设计为继承关系的简单规则,就是“is-a"规则,它表明子类的每个对象也是超类的对象。“is-a"规则的另一种表述法是置换法则,它表明程序中出现超类的对象的任何地方都可以用子类对象置换。例如,可以将一个子类对象赋值给超类变量:
Employee e;
e = new Employee(...);//Employee object expected
e = new Manager(...);//can be used as well - 在Java中,对象变量是多态的,一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象。
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss; - 变量boss和staff[0]引用同一个对象,但编译器将staff[0]看成Employee对象。这意味着
boss.setBonus(5000);
成立,而staff[0].setBonus(5000);
无法调用。不能将一个超类的引用赋值给予子类变量。
.3动态绑定
- 调用对象方法的执行过程:
- 1)编译器查看对象的声明类型和方法名,一一列举该类中所有同名方法和其超类中同名的public方法。至此,编译器已获得所有可能被调用的候选方法。
- 2)接下来,编译器将查看调用方法时提供的参数类型。如果在之前获得的方法中存在一个与提供的参数完全匹配,就选择该方法。这个过程被称为重载解析(overloading resolution)。由于允许类型转换,所以这个过程可能会很复杂。如果编译器没有找到与参数类型匹配的方法,或者经过类型转换之后有多个方法与之匹配,就会报告一个错误。至此,编译器已获得需要调用的方法名字和参数类型。
- 注:方法的名字和签名。如果子类中定义了一个与超类方法相同的方法,那么子类中的这个方法就覆盖了超类中的这个签名相同的方法。返回类型不是签名的一部分。因此在覆盖方法时,一定要保证返回类型的兼容性。允许子类覆盖方法的返回类型定义为原返回类型的子类型。
- 3)如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
- 4)当程序运行,并采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
- 每次调用都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一类仅需查表即可。
- 动态绑定有一个非常重要的特征:无需对现存的代码进行修改,就可以对程序进行拓展。
- 注:在覆盖一个方法时,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定声明为public。如果遗漏,编译器会解释为更严格的访问权限。
.4阻止继承:final类和方法
- 不允许扩展的类被称为final类,定义时用final修饰符表明。
- 类中特定的方法也可以被声明为final,子类将不能覆盖这个方法(final类中的所有方法自动地成为final方法)。
- 域也可以被声明为final。对于final域来说,构造对象之后就不允许改变它们的值了。不过,如果将类声明为final,只有其中的方法自动地成为final,而不包括域。
- 将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。
- 动态绑定会带来更多的系统开销。如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程称为内联(inlining)。例如,内联调用e.getName()将被替换为访问e.name域。
.5强制类型转换
double x = 3.405;
int nx = (int)x;
将x的值转换成整数类型,舍弃了小数部分。
有时需要将某个类的对象引用转换成为另一个类的对象引用。进行强制类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
将一个值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行强制类型转换,这样才能通过检查。如果试图在继承链上进行向下的类型转换,并且谎报有关对象包含的内容,会产生ClassCastException异常。
一个良好的习惯,在进行类型转换时,先查看一下是否能够转换成功:
if(staff[1] instanceof Manager){
//不可转换便不会执行
boss = (Manager)staff[1];
}Date c = (Date) staff[1];
将会产生编译错误,因为Date不是Employee的子类。综上:1)只能在继承层次内进行类型转换;2)在将超类转换成子类之前,应该使用instanceof进行检查。(实际上通过类型转换调整对象的类型并不是一种好的做法)
.6抽象类
如果自下而上在类的继承层次中上移,位于上层的类更具有通用性,甚至可能更抽象。从某种角度看,祖先类更加通用。
-
使用abstract关键词,使一个类完全不需要实现某个方法。为了提高清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。除了抽象方法以外,抽象类还可以包含具体数据和具体方法。
abstract class Person{
private String name;
public Person(String n){
name = n;
}
public abstract String getDescription();
public String getName(){
return name;
}
}- Tips:许多程序员认为,在抽象类中布恩那个包含具体方法。建议尽量使用通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象类)中。
抽象方法充当占位的角色,它们的具体实现在子类中。扩展抽象类可以有两个选择:1)在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记成为抽象类;2)定义全部的抽象方法,这样一来子类就不是抽象的了。
类即使不含抽象方法,也可以将类声明为抽象类。抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。但是可以创建一个具体子类的对象。需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。如:
Person p = new Student("Vince Vu", "Economics");
这里的p是一个抽象类Person的变量,Person引用了一个非抽象子类Student的实例。下面通过抽象类Person扩展一个具体子类Student,在该类中的全部方法都是非抽象方法,所以不再是抽象类:
class Student extends Person{
private String major;
public Student(String n, String m){
super(n);
major = m;
}
public String getDesription(){
return "A student majoring in " + major;
}
}测试程序清单:
Person[] people = new Person[2];
people[0] = new Employee(...);
people[1] = new Student(...);
for(Person p : people){
System.out.println(p.getName()+","+p.getDescription());
}
由于不能构造抽象类Person的对象,所以变量永远不会引用Person对象,而是引用诸如Employee或Student这样的具体子类对象,而这些对象中都定义了getDescription方法。如果不声明抽象方法,则无法使用p调用方法,
.7受保护访问
- 有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法和域声明为protected。在实际使用中,要谨慎使用protected属性,否则会违背OOP的数据封装原则。声明为protected的方法,只能被子类调用,其他类无法使用。例,Object类中的clone方法。
- 控制可见性的访问修饰符:
- 仅对本类可见————private
- 对所有类可见————public
- 对本包和所有子类可见————protected
- 对本包可见————默认,无需修饰符
5.2 Object:所有类的超类
- Object类是Java中的所有类的始祖,在Java中每个类都是由它扩展而来的。可以使用Object类型的变量引用类型的对象:
Object obj = new Employee("Harry", 35000);
当然,Object类型的变量只能用于作为各种值得通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换:
Employee e = (Employee) obj;
在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展于Object类。
.1equals方法
-
Object类中的equals方法用于检测一个对象是否等于另一个对象。这个方法将判断两个对象是否具有相同的引用,似乎合情合理,然而对于多数类来说,这种判断并没有什么意义。如果两个对象的状态相等,就认为这两个对象是相等的。利用下面这个示例演示equals方法的实现机制:
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(getClass() != otherObject.getClass()) return false; //now we know otherObject is a non-null Employee Employee other = (Employee)otehrObject; //test whether the fields have identical values return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay); } }
getClass方法返回一个对象所属的类。
Tips:为了防备name或hireDay可能为null的情况,需要使用Objects.equals方法。如果两个参数都为null,Objects.equals(a,b)调用将返回ture;如果其中一个参数为null,则返回false;否则,如果两个参数都不为null,则调用a.equals(b)。利用这个方法,最后一句要改写为:
return Objects.equals(name, other.name)
&& salary == other.salary
&& Objects.equals(hireDay, other.hireDay);在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。
class Manager extends Employee{
...
public boolean equals(Object otherObject){
if(!super.equals(otherObject)) return false;
//super.equals checked that this and otherObject belong to the same class
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}
}
.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
- 这些规则似乎合情合理,但还是有一些特殊情况。就对称性来说,但当参数不属于同一个类的时候需要仔细地思考一下。考虑这个调用:
e.equals(m);
。这里的e是一个Employee对象,m是一个Manager对象,并且两个对象具有相同的姓名、薪水和雇佣日期。如果在Employee.equals中用instanceof检测,则返回true。然而当调用:m.equals(e);
也需要返回true。对称性不允许这个方法调用返回false,或者抛出异常。这就使得Manager类受到了束缚,这个类的equals方法必须能够用自己与任何一个Employee对象进行比较,而不必考虑经理拥有的那部分特有的信息。 - 可以从两个截然不同的情况看一下这个问题:
如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测
如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较
Tips:在Java库中包含150多个equals方法的实现,包括使用instanceof检测、调用getClass检测、捕获ClassCastException或者什么也不做。好像陷入了一种困境。
-
下面给出编写一个完美的equals方法的建议:
- 1)显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量
- 2)检测this与otherObject是否引用同一个对象:
if(this == otherObject) return true;
这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多 - 3)检测otherObject是否为null,如果为null,则返回false。这项检查时很必要的。
if(otherObject == null) return false;
- 4)比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:
if(getClass() != otherObject.getClass()) return false;
如果所有的子类都拥有统一的语义,就使用instanceof检测
`if(!(otherObject instanceof ClassName)) return false; - 5)将otherObject转换为相应的类类型变量:
ClassName other = (ClassName) otherObject;
- 6)现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。
return field1 == other.field1
&& Object.equals(field2, other.field2)
&& ...;
如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。 - Tips:对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等
.3hashCode方法
散列码(hash code)是由对象导出的一个整数值。散列码是没有规律的。如果x和y是两个不同的对象,x.hashCode()与y.hashCode()基本不会相同。
String类使用下列算法计算散列码:
int hash = 0;
for(int i=0; i<length(); 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.hasCode()+" "+tb.hashCode());对象 散列码 s 256 sb 20526976 t 256 tb 20527144
注意,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不用的散列码,这是因为在StringBuffer类中没有定义hashCode方法,它的散列码是由Object类的默认hashCode方法导出的对象存储地址。
- 如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入散列表中。
- hashCode方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,以便能能够让各个不同的对象产生的散列码更加均匀。
- 在Java7中还可以做两个改进。首先,最好使用null安全的方法Objects.hashCode。如果其参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。还有更好的做法,需要组合多个散列值时,可以调用Objects.hash并提供多个参数。这个方法会对各个参数调用Objects.hashCode,并组合这些散列值。
- Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。
- Tips:如果存在数组类型的域,那么可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成。
.4toString方法
用于返回表示对象值的字符串。绝大多数(但不是全部)的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方法:
Class Manager extends Employee{
...
public String toString(){
return super.toString()
+"[bonus="+bonus
+"]";
}
}-
随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,Java编译就会自动调用toString方法,以便获得这个对象的字符串描述。例如:
Point p = new Point(10, 20);
String message = "The current position is "+p;
//automatically invokes p.toString()- Tips:在调用x.toString()的地方可以用""+x替代。这条语句是将一个空字符串与x的字符串表示相连接。这里的x就是x.toString()。与toString不同的是,如果x是基本类型,这条语句照样能够执行。如果x是任意一个对象,并调用
System.out.println(x);
,println方法就会直接地调用x.toString(),并打印输出得到的字符串。
- Tips:在调用x.toString()的地方可以用""+x替代。这条语句是将一个空字符串与x的字符串表示相连接。这里的x就是x.toString()。与toString不同的是,如果x是基本类型,这条语句照样能够执行。如果x是任意一个对象,并调用
-
Object类定义了toString方法,用来打印输出对象所属的类名和散列码。例如,调用
System.out.println(System.out);
,将输出:java.io.PrintStream@2f6684。之所以得到这样的结果是因为PrintStream类的设计者没有覆盖toString方法。- warning:令人烦恼的是,数组继承了Object类的toString方法,数组类型将按照旧的格式打印。例如:
int[] luckyNumbers = {2,3,5,7,11,13};
String s = ""+luckyNumbers;
生成字符串“[I@1a46e30"(前缀[I表明是一个整型数组)。修正的方式是调用静态方法Arrays.toString。代码:
String s = Arrary.toString(luckyNumber);
将生成字符串”[2,3,5,7,11,13]"。想要打印多维数组(即,数组的数组)则需要调用Arrays.deepToString方法。
- warning:令人烦恼的是,数组继承了Object类的toString方法,数组类型将按照旧的格式打印。例如:
-
toString方法是一种非常有用的调试工具。在标准类库中,许多类都定义了toString方法,以便用户能够获得一些有关对象状态的必要信息。(之后会有Log的介绍。)
- Tips:强烈建议为自定义的每一个类增加toString方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从日志记录中受益匪浅。
5.3泛型数组列表
在许多语言中,必须在编译时就确定整个数组的大小。在Java中情况就好多了。它允许在运行时确定数组的大小。
int actualSize = ...;
Employee[] staff = new Employee[actualSize];
当然,这段代码并没有完全解决运行时动态更改数组的问题。一旦确定了数组的大小,改变它就太不容易了。在Java中,解决这个问题最简单的方法是使用Java中另一个被称为ArrayList的类。它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面。例如:ArrayList<Employee>。
-
声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
两边都使用类型参数Employee,有些繁琐。Java7中,可以省去右边的类型参数:
ArrayList<Employee> staff = new ArrayList<>();
这被称为“菱形”语法,因为空尖括号<>就像一个菱形。可以结合new操作符使用菱形语法。编译器会检查新值是什么。如果赋值给一个变量,或者传递到某个方法,或者从某个方法返回,编译器会检查这个变量
、参数或者方法的泛型类型,然后将这个类型放在<>中。- Tips:Java SE5.0以前的版本没有提供泛型类,而是有一个ArrayList类,其中保存类型为Object元素,它是“自适应大小”的集合。如果一定要使用老版本Java,则需要删掉所有的后缀<...>。在新版中,没有后缀的ArrayList会被认为是一个删除了类型参数的“原始”类型,仍然可以使用。
使用add方法可以将元素添加到数组列表中去。例如,下面展示了如何将雇员对象添加到数组列表中的方法:
staff.add(new Employee("Harry", ...));
staff.add(new Employee("Tony", ...));
数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。这就显现出数组列表的操作魅力:如果调用add且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并且将所有的对象从较小数组中拷贝到较大数组中。如果足够清楚数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法:
staff.ensureCapacity(100);
,这个方法将分配一个包含100个对象的内部数组。然后至多调用100次add,而不用重新分配空间。-
另外,还可以把初始容器传递给ArrayList构造器:
ArrayList<Employee> staff = new ArrayList<>(100);
- Warning:分配数组列表:
new ArrayList<>(100);//capacity is 100
,它与为新数组分配空间有所不同:new Employee[100];//size is 100
数组列表的容量与数组的大小有一个非常重要的区别。如果为数组分配100个元素的存储空间,数组就有100个空位置可以使用。而容量为100个元素的数组列表只是拥有保存100个元素的潜力(实际上重新分配空间的话,将会超过100),但是在最初,甚至是完成初始化构造之后,数组列表根本就不含任何元素。
- Warning:分配数组列表:
size方法将返回数组列表中包含的实际元素数目。例如:
staff.size();
,将返回staff数组列表的当前元素数量,它等价于数组a的a.length。一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法。这个方法将存储区域的大小调整为当前元素所需要的存储空间数目。垃圾回收器将回收多余的存储空间。一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用trimToSize。
.1访问数组列表元素
数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。其原因是ArrayList类并不是Java程序设计语言的一部分,它只是一个由某些人编写且被放在标准库中的一个实用类
-
使用get和set方法实现访问或改变数组元素的操作,而不使用数组中的[]语法格式。例如,要设置第i个元素,可以使用:
staff.set(i, harry);
, 等价于对数组a的元素赋值(下标从0开始),a[i] = harry;
。- Tips:只有i小于或等于数组列表的大小时,才能够调用list.set(i,x)。例如,下面这段代码是错误的:
ArrayList<Employee> list = new ArrayList<>(100);//capacity is 100,size 0
list.set(0,x);//no element 0 yet
使用add方法为数组添加新元素,而不要使用set方法,它只能替换数组中已经存在的元素内容。
- Tips:只有i小于或等于数组列表的大小时,才能够调用list.set(i,x)。例如,下面这段代码是错误的:
-
使用:
Employee e = staff.get(i);
,获得数组列表的元素,等价于:Employee e = a[i];
。- Tips:没有泛型类时,原始的ArrayList类提供的get方法别无选择只能返回Object,因此,get方法的调用者必须对返回值进行类型转换:
Employee e = (Employee)staff.get(i);
。原始的ArrayList存在一定的危险性。它的add和set方法允许接受任意类型的对象。对于这个调用:staff.set(i, new Date());
,编译不会给出任何警告,只有在检索对象并试图对它进行类型转换时,才会发现有问题。如果使用ArrayList<Employee>,编译器就会检测到这个错误。
- Tips:没有泛型类时,原始的ArrayList类提供的get方法别无选择只能返回Object,因此,get方法的调用者必须对返回值进行类型转换:
该技巧可以一举两得,既可以灵活地扩展数组,又可以方便地访问数组元素。首先,创建一个数组,并添加所有的元素。
ArrayList<x> list = new ArrayList<>();
while(...){
x=...;
list.add(x);
}
执行完上述操作后,使用toArray方法将数组元素拷贝到一个数组中。
x[] a = new X[list.size()];
list.toArray(a);
除了在数组列表的尾部追加元素之外,还可以在数组列表的中间插入元素,使用带索引参数的add方法。
int n = staff.size()/2;
staff.add(n, e);
为了插入一个新元素,位于n之后的所有元素都要向后移动一个位置。如果插入新元素后,数组列表的大小超过了容量,数组列表就会被重新分配存储空间。同样的,可以从数组列表中删除一个元素:
Employee e = staff.remove(n);
位于这个之后的所有元素都向前移动一个位置,并且对数组的大小减1。对数组实施插入和删除元素的操作其效率比较低。对于小型数组来说,这一点不必担心。但如果数组存储的元素比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表了。这将在之后讨论。使用“for each”循环遍历数组列表:
for(Employee e : staff){
do something with e
}
和for(int i=0; i<staff.size(); i++)
效果相同。
.2类型化与原始数组列表的兼容性
-
假设有遗留代码:
public class EmployeeDB{
public void update(ArrayList list){...}
public ArrayList find(String query){...}
}
可以将一个类型化的数组列表传递给update方法,而不需要进行任何类型转换。
ArrayList<Employee> staff = ...;
employeeDB.update(staff);
也可以将staff对象传递给update方法。- Warning:尽管编译器没有给出任何错误信息或警告,但是这样调用并不太安全。在update方法中,添加到数组列表中的元素可能不是Employee类型。在对这些元素进行检索时就会出现异常。听起来似乎很吓人,但思考一下就会发现。这与在Java中增加泛型之前就是一样的。虚拟级的完整性绝对没有受到威胁。在这种情形之下,既没有降低安全性,也没有受益于编译时的检查。
相反的,将一个原始ArrayList赋值给一个类型化ArrayList会得到一个警告。
ArrayList<Employee> result = employeeDB.find(query);//yields warning
使用类型转换并不能避免出现警告。
ArrayList<Employee> result = (ArrayList<Employee>)employeeDB.find(query);
//yields another warning
这样将会得到另一个警告信息,被告知类型转换有误。这就是Java中不尽如人意的参数类型的限制所带来的结果。这时候不要做什么,确定不会造成严重的后果就可以了。
5.4 对象包装器与自动装箱
有时,需要将int这样的基本类型对象转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapper)。这些对象包装器类拥有很鲜亮的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6个派生域公共的超类Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器还是final,因此它们不可以定义子类。
假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型。也就是说,不允许写成ArrayList<int>。这里就用到了Integer对象包装器类。
ArrayList<Integer> list = new ArrayList<>();Java SE5.0之后的另一个改进之处是更加便于添加或获得数组元素。这个调用:
list.add(3);
将自动得变成为list.add(Integer.valueOf(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方法。- Tips:自动装箱规范要求boolean、byte、char<=127,介于-128~127之间的short和int被包装到固定的对象中。例如,如果前面的例子中将a和b初始化为100,对它们进行比较的结果一定成立。
-
最后强调一下,拆箱和装箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。使用数值对象包装器还有另外一个好处。可以将某些基本方法放置在包装器中,例如,将一个数字字符串转换成数值。可以使用:
int x = Integer.parseInt(s);
,这与Integer对象没有任何关系,parseInt是一个静态方法。- 包装器类不可以实现修改参数值的方法,原理同第4章中的值传递。
5.5 参数数量可变的方法
Java SE 5.0以前的版本中,每个Java方法都有固定数量的参数。然而,现在的版本提供了可以用个可变的参数数量调用的方法(有时称为”变参“方法)。
printf方法的定义是这样的:
public class PrintStream{
public PrintStream print(String fmt, Object...args){return format(fmt,args);}
}
这里的省略号...是Java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外)。实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组,其中保存着所以参数(如果调用者提供的是整型数组或者是其他基本类型的值,自动装箱功能将把它们转换成对象)。现在将扫描fmt字符串,并将第i个格式说明符域arg[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.MIN_VALUE;
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方法。
5.6 枚举类
例:
public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};
,实际上,这个声明定义的类型是一个类,它刚好有4个实例,在此尽量不要构造新对象。因此,在比较两个枚举类型的值时,永远不需要调用equals,直接使用“==”就可以了。如果需要的话,可以在枚举类型中添加一些构造器、方法和域。当然,构造器只是在构造枚举常量的时候被调用。下面是一个示例:
public enum Size{
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private String addreviation;
private Size(String addreviation){this.addreviation = addreviation;}
public String getAddreviation(){return addreviation;}
}所有的枚举类都是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。