第5章Java高级特征 在第4章Java面向对象特征的基础上,本章将进一步介绍Java的高级面向对象特征,其中某些特征如接口是Java独有的语言机制。本章介绍的具体内容包括通过static关键字定义的类变量、类方法和初始化程序块,final关键字,抽象类,接口(interface),package,泛型与集合类,枚举类型,包装类与自动装箱和拆箱等。 5.1static关键字 static关键字可以用来修饰类的成员变量、成员方法和内部类,使得这些类成员的创建和使用,与类相关而与类的具体实例不相关,因此以static修饰的变量或方法又称为类变量和类方法。 5.1.1类变量/静态变量 在成员变量声明时使用static,则该变量就称为类变量或静态变量。静态变量只在系统加载其所在类时分配空间并初始化,并且在创建该类的实例时将不再分配空间,所有的实例将共享类的静态变量。因此静态变量可用来在实例之间进行通信或跟踪该类实例的数目。 例51定义了一个类Count。Count中定义了一个静态变量counter。创建Count对象时,将递增counter,并把counter的值赋予该对象的serialNumber变量。这样,如果把serialNumber看作对象的序列号,则通过静态变量Counter将使Count类的每个对象都被赋予了唯一的序列号,这些序列号从1开始递增。 例51为Count类的对象赋予递增的序列号。 class Count{ private int serialNumber; public static int counter = 0; public Count(){ counter++; serialNumber = counter; } public int getSerialNumber(){ return serialNumber; } } public class TestStaticVar{ public static void main(String[] args){ Count[] cc = new Count[10]; for(int i=0;icc.length;i++){ cc[i]=new Count(); System.out.println("cc["+i+"].serialNumber = "+cc[i].getSerialNumber()); } } } 例51的运行结果如下: cc[0].serialNumber = 1 cc[1].serialNumber = 2 cc[2].serialNumber = 3 cc[3].serialNumber = 4 cc[4].serialNumber = 5 cc[5].serialNumber = 6 cc[6].serialNumber = 7 cc[7].serialNumber = 8 cc[8].serialNumber = 9 cc[9].serialNumber = 10 Java中没有全局变量,但静态变量是在一个类的所有实例对象中都可以访问的变量,有点类似于其他语言中的全局变量。 静态变量只依附于类,而与类的实例对象无关,所以对于不是private类型的静态变量,可以在该类外直接用类名调用,而不像实例变量那样需要通过实例对象才能访问。例如,可以在下面的类中直接对例51中的Count类的静态变量Counter进行访问。 public class OtherClass{ public void incrementNumber(){ Count.Counter++; } } 5.1.2类方法/静态方法 在类的成员方法声明中带有static关键字,则该方法就称为类方法或静态方法。静态方法要通过类名而不是通过实例对象访问。在例52中,类GeneralFunction定义了一个实现两个整数加法的静态方法,在另一个类UseGeneral中可以通过GeneralFunction类直接访问。 例52对GeneralFunction类静态方法的访问。 class GeneralFunction{ public static int add(int x, int y){ return x+y ; } } public class UseGeneral{ public static void main(String[] args){ int c = GeneralFunction.add(9,10); System.out.println("9 + 10 = "+c); } } 例52的运行结果如下: 9 + 10 = 19 在静态方法的编写与使用时应该注意下列问题。 (1) 因为静态方法的调用不是通过实例对象进行的,所以在静态方法中没有this指针,不能访问所属类的非静态变量和方法,只能访问方法体内定义的局部变量、自己的参数和静态变量。 (2) 子类不能重写父类的静态方法,但在子类中可以声明与父类静态方法相同的方法,从而将父类的静态方法隐藏。另外子类不能把父类的非静态方法重写为静态的。例如,下列代码将出现编译错误。 class ClassA { public void methodOne(int i) { } public void methodTwo(int i) { } public static void methodThree(int i) { } public static void methodFour(int i) { } } class ClassB extends ClassA { public static void methodOne(int i) { }//错误!将ClassA中的methodOne()变成静态的 public void methodTwo(int i) { } public void methodThree(int i) { }//错误!不能重写ClassA中的静态方法methodThree() public static void methodFour(int i) { }//正确!将把ClassA中的methodFour()方法隐藏 } (3) main()方法是一个静态方法。因为它是程序的入口点,这可以使JVM不创建实例对象就可以运行该方法。因此,如果要在main()方法中访问所在类的成员变量或方法,就必须首先创建相应的实例对象。例如,下面的代码将出现编译时错误。 public class wrong{ int x; public static voidx(){ x=9;//错误!访问类的非静态变量 } } 5.1.3静态初始化程序 在一个类中,不属于任何方法体并且以static关键字修饰的语句块,称为静态语句块。因为静态语句块常用来进行类变量的初始化,所以也称为静态初始化程序块。其定义格式如下: static{ … } 静态语句块在加载该类时执行且只执行一次。如果一个类中定义了多个静态语句块,则这些语句块将按在类中出现的次序运行。例53是一个使用静态语句块的例子。 例53静态语句块与静态变量的访问。 class StaticInitDemo{ static int i; static { i = 5; System.out.println("Static code: i="+i++); } } public class TestStaticInit { public static void main(String args[]){ System.out.println(" Main code: i="+ StaticInitDemo.i); } } 例53的运行结果如下: Static code: i=5 Main code: i=6 在例53中,类StaticInitDemo定义了类变量i,并在静态语句块中对i赋予了初值5。由于静态语句块是在类加载时运行,因此系统将首先打印输出“Static code: i=5”,并把i的值增加为6,然后再运行TestStaticInit类中的main()方法,打印输出“Main code: i=6”。 5.2final关键字 1. 在类的声明中使用final Java允许在类的声明中使用final关键字。被定义成final的类不能再派生子类。例如java.lang.String类就是一个final类。这保证对String对象方法的调用确实运行的是String类的方法,而不是经其子类重写后的方法。 2. 在成员方法声明中使用final 对于类中的成员方法也可以定义为final。被定义成final的方法不能被重写。当方法的实现不能被改变,或者方法对于保证对象状态的一致性很关键时,应该把该方法定义为final。 定义为final的方法可以使运行时的效率优化。正如在第4章中提到的,对于final方法,编译器可以产生直接调用方法的代码,从而阻止运行时刻对方法调用的动态联编。实际上,如果方法被定义为static或private,编译器也将对它们进行上述优化。 3. 在成员变量的声明中使用final 如果类的成员变量被定义成final,则变量一经赋值就不能改变,所以可以通过声明final变量并同时赋初值来定义常量,并且变量名一般大写。例如: final int NUMBER =100; 如果在程序中要改变final变量的值,则将产生编译时错误。如果类的final变量在声明时没有赋初值,则在所属类的每个构造方法中都必须对该变量赋值。如果未赋初值的final变量是局部变量,则可以在所属方法体的任何位置对其赋值,但只能赋一次值。例54是对final变量声明与赋值的例子。 例54声明类的final变量并在构造方法中赋值。 class Customer{ private final long customerID; private static long counter=200901; public Customer(){ customerID = counter++; } public long getID(){ return customerID; } public static void main(String[] args){ Customer[] cc = new Customer[5]; for (int i=0; icc.length; i++){ cc[i]=new Customer(); System.out.println("The customerID is "+cc[i].getID()); } } } 例54的运行结果如下: The customerID is 200901 The customerID is 200902 The customerID is 200903 The customerID is 200904 The customerID is 200905 5.3抽象类 5.3.1什么是抽象类 Java允许在类中只声明方法而不提供方法的实现。这种只有声明而没有方法体的方法称为抽象方法,而包含一个或多个抽象方法的类称为抽象类。抽象类必须在声明中加abstract 关键字,而抽象方法在声明中也要加上abstract。抽象类也可有构造方法、普通的成员变量或方法,也可以派生抽象类的子类。 抽象类在使用上有特殊的限制,即不能创建抽象类的实例。正是为了阻止程序员创建抽象类的实例对象,使编译器在编译时刻对此进行检查,Java中要将抽象类和抽象方法带上abstract标记。如果抽象类的子类实现了抽象方法,则可以创建该子类的实例对象,否则该子类也是抽象类,也不能创建实例。一般将抽象类构造方法的访问权限声明为protected而不是public,从而保证构造方法能够由子类调用而不被其他无关的类调用。例如: abstract class Employee { abstract void raiseSalary (int i); } class Manager extends Employee { void raiseSalary (int i){…} } … Employee e = new Manager();//创建Employee子类Manager的对象 Employee e = new Employee();//错误!Employee为抽象类 … 5.3.2抽象类的作用 类是现实世界同类对象的抽象,是Java程序中创建对象的模板。抽象类不能实例化对象,那么抽象类的意义是什么呢?程序中定义抽象类的目的是为一类对象建立抽象的模型,在同类对象所对应的类体系中,抽象类往往在顶层。这一方面使类的设计变得清晰,另一方面抽象类也为类的体系提供通用的接口。这些通用的接口反映了一类对象的共同特征。定义了这样的抽象类后,就可以利用Java的多态机制,通过抽象类中的通用接口处理类体系中的所有类。 在第4章介绍运行时多态的概念时,曾在图49中给出了一个关于几何形状Shape及其子类的例子。在这个类的层次结构中,Shape类是顶层类。实际上Shape类的对象是没有实际意义的。定义Shape类的目的并不是为了在程序中创建 图51抽象类Shape及其类体系 并操作它的对象,而是为了定义几何形状类体系的通用接口,如draw()和erase(),这些接口在Shape类中不需要给出具体实现,而由它的各个子类提供自己的实现。因此Shape类可以定义为抽象类,而draw()和erase()方法可以定义为抽象方法,如图51所示。 实际上,即使不包括任何抽象方法,也可将一个类声明为抽象类。这样的类往往是没有必要定义任何抽象方法,而设计者又想禁止创建该类的实例对象,此时只需在类的声明中加上abstract关键字。 定义抽象类和抽象方法可以向用户和编译器明确表明该类的作用和用法,使类体系设计更加清晰,并能够支持多态,因此是Java的一种很有用的面向对象机制。 5.4接口 5.4.1什么是接口 Java中的接口(interface)使抽象类的概念更深入一层。接口中声明了方法,但不定义方法体,因此接口只是定义了一组对外的公共接口。与类相比,接口只规定了一个类的基本形式,不涉及任何实现细节。实现一个接口的类将具有接口规定的行为。 在OOP中,一个类的“公共接口”可以被认为是使用类的“客户”代码与提供服务类之间的契约或协议。因此可以认为一个接口的整体就是一个行为的协议。实现一个接口的类将具有接口规定的行为,并且外界可以通过这些接口与它通信。有些OOP中采用protocol关键字,而Java中使用interface关键字。 下面给出接口的具体定义。 5.4.2接口的定义 接口的定义包括接口声明和接口体两部分。格式如下: interfaceDeclaration{ interface Body } 1. 接口声明 接口声明的格式如下: [public] interface InterfaceName [extends listofSuperInterface]{ … } 其中public指明任意类均可以使用这个接口。默认情况下,只与该接口定义在同一个包中的类才可以访问这个接口。extends子句与类声明中的extends子句基本相同,不同的是一个接口可以有多个父接口,用逗号隔开,而一个类只能有一个父类。子接口继承父接口中所有的常量和方法。 2. 接口体 接口体中包含常量定义和方法定义两部分。 在接口中定义的常量默认具有public、final、static的属性。常量定义的具体格式为: type NAME = value; 其中type可以是任意类型,NAME是常量名,通常用大写,value是常量值。在接口中定义的常量可以被实现该接口的多个类共享。 在接口中声明的方法默认具有public和abstract属性。方法定义的格式为: returnTypemethodName([paramlist]); 接口中只进行方法的声明,而不提供方法的实现。所以,方法定义没有方法体,且以分号“; ”结尾。另外,如果在子接口中定义了和父接口同名的常量和相同的方法,则父接口中的常量被隐藏,方法被重写。 注意: 接口中的成员不能使用某些修饰符,例如transient、volatile、synchronized、private、protected。 5.4.3接口的实现与使用 类的声明中用implements子句来表示一个类实现了某个接口,在类体中可以使用接口中定义的常量,而且必须实现接口中定义的所有方法。一个类可以实现多个接口,在implements子句中用逗号分隔。 在类中实现接口所定义的方法时,方法的声明必须与接口中所定义的完全一致。 下面举一个接口及接口实现的例子。现实世界中有很多实体具有飞行的功能。我们可以构造一个公共的接口Flyer来抽象描述飞行行为。该接口规定了3个方法: 起飞、着陆和飞行。接口Flyer的定义如下: public interface Flyer{ public void takeoff(); public void land(); public void fly(); } 飞机是我们很熟悉的一种具有飞行能力的工具。我们可以定义一个类Airplane,该类通过实现Flyer接口从而对外表征出Flyer接口所规定的飞行行为。Airplane的定义如下: public class Airplane implements Flyer{ public void takeoff(){ //加速直至飞起,收起着陆装置等操作 } public void land(){ //下落着陆装置、减速并降低机翼直到接触地面等操作 } public void fly(){ //保持所有发动机正常运行等操作 } } 在程序中,接口可以像类一样作为数据类型来使用,并且可以支持多态。此时任何实现该接口的类都可认为是该接口的“子类”,因此声明为某接口类型的变量,可以指向该接口“子类”的实例,通过这些变量可以访问接口中规定的方法。例55是通过接口实现多态的例子。 图52Shape接口及实现它的各个类 例55将第4章中例418中的类进行了重定义,将Shape定义为描述几何图形的接口,几种图形如Circle、Square和Triangle都实现Shape接口,如图52所示。 例55的程序是对例418的改写。在例55中将Shape定义为接口,Circle、Square和Triangle分别实现了该接口,main()方法实现与例418相同的操作。 例55通过接口实现多态示例。 import java.util.*; //将Shape定义为interface interface Shape { void draw(); void erase(); } //定义Circle类实现Shape class Circle implements Shape { public void draw() { System.out.println("Calling Circle.draw()"); } public void erase() { System.out.println("Calling Circle.erase()"); } } //定义Square类实现Shape class Square implements Shape { public void draw() { System.out.println("Calling Square.draw()"); } public void erase() { System.out.println("Calling Square.erase()"); } } //定义Triangle类实现Shape class Triangle implements Shape { public void draw() { System.out.println("Calling Triangle.draw()"); } public void erase() { System.out.println("Calling Triangle.erase()"); } } //包含main()的测试类 public class NewShapes{ static void drawOneShape(Shape s){ s.draw(); } static void drawShapes(Shape[] ss){ for(int i = 0; i ss.length; i++){ ss[i].draw(); } } public static void main(String[] args) { Random rand = new Random(); Shape[] s = new Shape[9]; for(int i = 0; i s.length; i++){ switch(rand.nextInt(3)) { case 0: s[i] = new Circle();break; case 1: s[i] = new Square();break; case 2: s[i] = new Triangle();break; } } drawShapes(s); } } 例55的某次运行结果如下: Calling Circle.draw() Calling Triangle.draw() Calling Square.draw() Calling Circle.draw() Calling Triangle.draw() Calling Triangle.draw() Calling Triangle.draw() Calling Square.draw() Calling Square.draw() 由于Circle,Square和Triangle类的实例是随机生成的,所以例55各次运行的结果可能不同。注意在例55中,由于接口Shape中声明的方法其访问权限默认是public,所以在实现Shape接口的各类如Circle、Square和Triangle中,在对Shape中定义的两个方法draw()和erase()实现时,要在声明中增加public,否则这些类对接口方法的实现将缩小访问权限,会出编译时错误。 5.4.4多重继承 在C++中,多重继承要将多个父类合并到一个类中。因为每个父类都有自己的一套实现细节,导致合并操作复杂,并可能存在同一个方法的两种不同实现,由此产生代码冲突,增加代码的不可靠性。Java中规定一个类只能继承一个父类,但可以实现多个接口,Java是利用接口实现多重继承的。由于接口根本没有实现细节,所以在进行父类与多个接口的合并时,只可能有一个类具有实现细节,如图53所示。由此C++多重继承实现中存在的问题,在Java中都不存在了,保证了Java的简单性与代码的安全可靠。 图53Java中的多重继承 下面举一个多重继承的例子。 对于5.4.3节中给出的描述飞行行为的接口Flyer,由于飞机、鸟,甚至科幻中的超人都可以飞,所以Airplane类、Bird类和Superman类都可以实现Flyer接口。而同时,飞机是一种交通工具,因此Airplane类又是Vehicle类的子类; 鸟是一种动物,因此Bird又是Animal类的子类,如图54所示。所以一个类可以从一个父类继承,并且可以同时继承其他接口。 图54类体系中的类同时实现接口的示例 Bird类可以进行如下定义: public class Bird extends Animal implements Flyer{ public void takeoff(){…} public void land(){…} public void fly(){…} public void buildNest(){…} public void layEggs(){…} public void eat(){…} } 注意: 在子类的声明中,extends子句必须放在implements子句前面。 一个类也可以实现多个接口。水上飞机(Seaplane)不仅能飞还能够在海上航行。Seaplane类继承了Airplane类,所以继承了Airplane类中对Flyer接口的实现,从而具有了飞行的行为; 而同时Seaplane类也可以实现Sailer接口,使该类具有航行的接口与行为,如图55所示。 图55类体系中的类同时实现多个接口 5.4.5通过继承扩展接口 接口定义后,可能在某些情况下需要对接口进行扩展,如增加新的方法声明。例如,对于例55中的接口Shape,如果需要计算一个几何图形的面积,可以向Shape中加入一个方法: interface Shape { void draw(); void erase(); double area(); } 上述直接向Shape中增加方法的方式扩展接口可能带来问题: 所有实现原来Shape接口的类都将因为Shape接口的改变而不能正常工作。为了既能扩展接口,又保证不影响实现该接口的类,一种可行的方法是通过创建接口的子接口来增加新的方法,例如: interface ShapeArea extends Shape{ double area(); } 这样使用Shape接口的用户可以选择采用新的接口ShapeArea,也可以保持原来对Shape接口的实现。 在接口的定义中使用继承,可以方便地为一个接口添加新的方法; 也可以通过接口继承将几个接口合并为一个接口,即在子接口声明中的extends关键字后引用多个基础接口,这些接口间通过“,”分隔。 5.4.6接口中的缺省方法与静态方法 在5.4.5节中介绍了通过继承的方式在接口中增加新方法,扩展接口的功能。在Java SE 8 (JDK)中,为了解决接口的扩展问题,引入了一种有效的机制——缺省方法(default method)。缺省方法使人们可以向已经发布并存在实现(implement)类的接口中增加新的方法而不需要这些已存在的实现类做任何修改。 在接口中定义缺省方法,是通过在方法的声明前面使用default关键字实现的。在接口中声明的所有方法,包括缺省方法隐含的都是public,所以可以在缺省方法前省去public。缺省方法不是抽象方法,是具有方法体的普通方法。所有实现这个接口的类,都将继承缺省方法并可以直接使用。如果接口中的缺省方法不能满足某个实现类需要,也可以进行重写。下面给出一个例子。 (1) 下列代码定义了一个接口I_A,以及实现该接口的两个类ClassI_A和ClassI_B: interface I_A { public void abstractMethod(); } class ClassI_A implements I_A{ public void abstractMethod(){ System.out.println("The abstractMethod() in ClassI_A is called."); } } class ClassI_B implements I_A{ public void abstractMethod(){ } } 运行如下测试类: public class InterfaceDefaultTest_1{ public static void main (String args[]){ I_A a =new ClassI_A(); a.abstractMethod(); } } 得到的运行结果如下: The abstractMethod() in ClassI_A is called. (2) 向接口I_A中增加一个缺省方法,而(1)中的两个类ClassI_A和ClassI_B都可以不变: interface I_A { public void abstractMethod(); //增加一个新方法 default void defaultInfo() { System.out.println("A default method in I_A is called."); } } 运行如下测试类: public class InterfaceDefaultTest_2{ public static void main (String args[]){ I_A a =new ClassI_A(); a.abstractMethod(); a.defaultInfo(); I_A b = new ClassI_B(); b.defaultInfo(); } } 得到的结果如下: The abstractMethod() in I_A is called. A default method in I_A is called. A default method in I_A is called. (3) 接口I_A的实现类ClassI_B中,重写该接口中的缺省方法。 class ClassI_B implements I_A{ public void abstractMethod(){ } //重写接口I_A 中的缺省方法 public void defaultInfo(){ System.out.println("A overridden default method of I_A is called."); } } 运行上述(2)中的测试类,得到的结果为: The abstractMethod() in I_A is called. A default method in I_A is called. A overridden default method of I_A is called. 需要注意的是,如果一个类实现了两个接口,并且两个接口中含有相同声明的缺省方法时,就需要在该实现类中重写这个缺省方法,并且通过这个类对象访问缺省方法时,调用的是这个重写后的方法。 除了缺省方法,在JDK 8以后的版本中,还可以在接口中定义静态方法(static method)。接口中的静态方法,可以用来定义接口的辅助性通用方法,它不能被实现接口的类或子接口继承,该种方法的定义与用法与类中的static方法一样,通过接口名进行访问。另外,缺省是public的,所以public修饰符可以省略。 下列示例中,接口I_A中定义了静态方法staticInfo(),ClassI_A实现了接口I_A: interface I_A { //定义了一个静态方法 static void staticInfo() { System.out.println("A static method in I_A is called."); } public void abstractMeth(); default void defaultInfo() { System.out.println("A default method in I_A is called."); } } class ClassI_A implements I_A{ public void abstractMeth(){ } } 运行如下测试类: public class InterfaceStaticTest{ public static void main (String args[]){ I_A.staticInfo(); I_A a =new ClassI_A(); a.defaultInfo(); } } 得到的结果为: A static method in I_A is called. A default method in I_A is called. 5.4.7接口与抽象类 通过上述对抽象类和接口的介绍,可以发现接口与抽象类有一定的相似性,但实际上这二者之间有很大的区别,如下所述。  接口中的所有方法都是抽象的,而抽象类可以定义带有方法体的不同方法。  一个类可以实现多个接口,但只能继承一个抽象父类。  接口与实现它的类不构成类的继承体系,即接口不是类体系的一部分。因此,不相关的类也可以实现相同的接口。而抽象类是属于一个类的继承体系,并且一般位于类体系的顶层。 使用接口的主要优势在于: 一个优势是类通过实现多个接口可以实现多重继承,这是接口最重要的作用,也是使用接口的最重要的原因——能够使子类对象上溯造型为多个基础类(接口)类型; 另一个优势是能够抽象出不相关类之间的相似性,而没有强行形成类的继承关系。使用接口,可以同时获得抽象类以及接口的优势。所以如果要创建的类体系的基础类不需要定义任何成员变量,并且不需要给出任何方法的完整定义,则应该将基础类定义为接口。只有在必须使用方法定义或成员变量时,才应该考虑采用抽象类。 5.5包 5.5.1什么是Java中的包 在Java中,为了使类易于查找和使用,为了避免命名冲突和限定类的访问权限,可以将一组相关类与接口“包裹”在一起形成包(package)。包被认为是Java的重要特色之一,它体现了OOP的封装思想,为Java中管理大量的类和接口提供了方便。另外,由于Java编译器要为每个类生成一个字节码文件,且文件名与类名相同,因此有可能由于同名类的存在而导致命名冲突。包的引入为Java提供了以包为单位的独立命名空间,位于不同包中的类即使同名也不会冲突,从而有效地解决了命名冲突的问题。同时,包具有特定的访问控制权限,同一个包中的类之间拥有特定的访问权限。因此,Java中包是相关类与接口的一个集合,它提供了类的命名空间的管理和访问保护。 Java平台中的类与接口都是根据功能以包组织的。Java的JDK提供的包主要有 java.applet、java.awt、java.awt.datatransfer、java.awt.event、java.awt.image、java.beans、java.io、java.lang、java.lang.reflect、java.math、java.net、java.rmi、java.security、java.sql、java.util等。 每个包中都定义了许多功能相关的类和接口。我们也可以定义自己的包来实现自己的应用程序。 Java编译器把包对应于文件系统的目录和文件管理,还可以使用ZIP或JAR压缩文件的形式保存。例如,以Windows平台为例,名为java.applet的包中,所有类文件都存储在目录classPath\java\applet下。其中包根目录——classPath由环境变量CLASSPATH来设定。 包机制的好处主要体现在如下几点。  程序员容易确定包中的类是相关的,并且容易根据所需的功能找到相应的类。  每个包都创建一个新的命名空间,因此不同包中的类名不会冲突。  同一个包中的类之间有比较宽松的访问控制。 下面将介绍如何定义与使用包。 5.5.2包的定义与使用 1. 包的定义 使用package语句指定一个源文件中的类属于一个特定的包。package语句的格式如下: package pkg1[.pkg2[.pkg3…]]; 例如: package graphics; public class Circle extends Graphic implements Draggable { … } Circle类成为graphics 包中的一个public成员,并存放在classPath\graphics目录中。如果源文件中没有package语句,则指定为无名包。无名包没有路径,一般情况下,会把源文件中的类存储在当前目录(即存放Java源文件的目录)下。前面许多例子都属于这种情况。 说明:  package语句在每个Java 源程序中只能有一条,一个类只能属于一个包。  package语句必须在程序的第一行,该行前可有空格及注释行。  包名以“.”为分隔符。 2. 包成员的使用 包中的成员是指包中的类和接口。只有public类型的成员才能被包外的类访问。要从包外访问public类型的成员,要通过以下方法。  引入包成员或整个包,然后使用短名(short name,类名或接口名)引用包成员。  使用长名(long name,由包名与类/接口名组成)引用包成员。 1) 引入包成员 可以先引入包中的指定类或整个包,再使用该类。这时可以直接使用类名或接口名。在Java中引入包(如JDK中的包或用户自定义的包)中的类是通过import语句实现的。import语句的格式如下: import pkg1[.pkg2[.pkg3…]].(classname|*); 其中pkg1[.pkg2[.pkg3…]]表明包的层次,与package语句相同,它对应于文件目录,classname则指明所要引入的类。如果要从一个包中引入多个类,则可以用通配符(*)来代替。例如下列代码引入graphics包中的指定类Circle: import graphics.Circle;//引入graphics包中的Circle类 … Circle myCircle = new Circle(); … 下列代码引入graphics包中的所有类,程序中便可以直接引用该包中的任意类,如Circle 和Rectangle: import graphics.*;//引入graphics包中的所有类 … Circle myCircle = new Circle(); Rectangle myRectangle = new Rectangle();… 注意: import 语句必须在源程序所有类声明之前,在package 语句之后。因此Java程序的一般结构如下: [ package 语句]//默认是package. ; (属于当前目录) [ import 语句]//默认是importjava.lang.*; [类声明] 2) 使用长名引用包成员 要在程序中使用其他包中的类,而该包并没有引入,则必须使用长名引用该类。长名的格式是: 包名.类名 例如,如果当前程序要访问graphics包中的Circle类,但该类并未通过import语句引入,则要使用graphics.Circle来引用Circle类: … graphics.CirclemyCircle = new graphics.Circle(); … 这种方式过于烦琐,一般只有当两个包中含有同名的类时,为了对两个同名类加以区分才使用长名。如果没有这种需要,更简单常用的方法是使用import语句来引入所需要的类,然后在随后的程序中直接使用类名对类操作。 3. 包定义与使用示例 例56定义二维几何图形的包并使用。 (1) 文件Rectangle.java,定义了Rectangle类放入graphics.twoD包中。 package graphics.twoD; public class Rectangle { public int width = 0; public int height = 0; public Point origin; public Rectangle(Point p, int w, int h) { origin = p; width = w; height = h; } //移动矩形的方法 public void move(int x, int y) { origin.x = x; origin.y = y; } //计算矩形面积的方法 public int area() { return width * height; } } (2) 文件Point.java,定义了Point类放入graphics.twoD包中。 package graphics.twoD; public class Point { public int x = 0; public int y = 0; public Point(int x, int y) { this.x = x; this.y = y; } } (3) 文件TestPackage.java,包含main()方法的测试程序,定义了一个点及一个矩形,计算并输出矩形的面积。 import graphics.twoD.*; public class TestPackage{ public static void main(String args[]){ Point p=new Point(2,3); Rectangle r=new Rectangle(p,10,10); System.out.println("The area of the rectangle is "+r.area()); } } 假如例56中Point.java与Rectangle.java在C:\work目录下,TestPackage.java在C:\work\test目录下,而graphics.twoD在C:\mypkg目录下,则例56的编译与运行可按下列步骤进行。 (1) 将C:\mypkg添加到classpath系统变量中,使该路径作为一个包根路径。既可以通过set命令添加,即set classpath = %classpath%; C:\mypkg,也可以在Windows中通过系统变量的设置窗口进行。 (2) 将C:\work作为当前目录,输入: javac -d c:\mypkg Point.java Rectangle.java 则在C:\mypkg\graphics\twoD目录下将产生Point.class和Rectangle.class两个类文件。Javac命令中的d选项是指定编译所产生类文件的根路径,如不指定,则编译生成的类文件如Point.class和Rectangle.class将存放在当前路径下。 (3) 进入TestPackage.java所在的目录C:\work\test,先后输入下列命令编译和运行: javac TestPackage.java java TestPackage TestPackage.java的运行结果如下: The area of the rectangle is 100 5.5.3引入其他类的静态成员 如果程序中需要频繁使用其他类中定义为static final的常量或static方法,则每次引用这些常量或方法都要出现它们所属的类名,会使得程序显得比较混乱。Java中提供了static import语句,使得程序中可以引入所需的常量和静态方法,这样程序中对它们的使用就不用再带有类名了。 例如,java.lang.Math类定义了一个常量PI和很多静态方法,包括计算正弦、余弦、正切、余切等的方法: public static final double PI 3.141592653589793 public static double cos(double a) 在JDK 1.5之前的版本中,如果在其他程序中要使用它们,则需要在这些常量和方法前带有所属类名: double r = Math.cos(Math.PI * theta); 现在,程序可以使用static import语句,将java.lang.Math类的static成员引入,则在程序中使用Math的静态成员将不再需要带有类名。静态成员可以单个引入,也可以通过通配符“*”成组引入,例如: import static java.lang.Math.PI; 或 import static java.lang.Math.*; 一个类的静态成员一旦被引入后,就可以直接使用成员的名称,而不用带类名。例如上面对Math类的cos()方法的调用,可以用下列代码替代: double r = cos(PI * theta); 对于JDK类库之外的用户自定义类的静态成员,也可以使用这种静态引入语句。 上述静态引入语句如果使用得当,会使程序变得简明易读,但如果过多使用则会适得其反。除了程序员,其他人很难了解哪些类定义了哪些静态成员,所以,程序中过多使用静态引入会使程序变得难以理解和维护。 5.5.4包名与包成员的存储位置 从例56可以看到,Java中包名实际上是包的存储路径的一部分,包名中的.分隔符相当于目录分隔符。包存储的路径实际上由包根路径加上包名指明的路径组成,而包的根路径由CLASSPATH环境变量指出。 假如CLASSPATH环境变量按下面的值进行设置: CLASSPATH = C:\jdk1.4.2\lib; .; C:\mypkg 则Java在编译TestPackage.java和解释执行TestPackage.class时,将会在下列路径下查找Point类和Rectangle类。下面以Point.class为例。 (1) C:\jdk1.4.2\lib\graphics\twoD\Point.class (2) .\graphics\twoD\Point.class (3) C:\mypkg\graphics\twoD\Point.class 如果在上述路径下都没有找到Point类,则将产生编译或运行时错误。 5.5.5Java源文件与类文件的管理 利用Java的包名与类文件存储位置之间的关系,可以对Java应用程序的源文件与类文件进行很好的管理。下面几点是进行一个应用系统开发时可以参考的。 (1) 在应用系统目录下分别创建源文件目录与类文件目录,并把类文件目录加入到classpath环境变量中。 例如,要开发几何图形操作相关的应用,可以创建下列目录: D:\graphicApp\source——作为存放源文件的顶层(根)路径。 图56graphicApp的源文件 与类文件的管理 D:\graphicApp\classes——作为存放类和接口的文件的包根路径。 (2) 每个源文件都存放在source目录中以包名为相对路径的子目录下; 编译后产生的类文件以所属包名为相对路径,存储在classes目录下。例如对于例56的程序,其源文件与类文件可以保存在如图56所示的层次目录下。 由于Java程序在编译时,每个类都要生成一个文件,所以一个应用程序包含的文件可能是很多的。按照上述方法对Java应用系统的文件进行存放,可以实现对文件的有效管理,并且能够保证引用这些类的程序在编译和运行时能够简便有效地定位到相应的类。 5.6泛型与集合类 5.6.1泛型概述 泛型即泛化技术(generics),是在JDK 1.5中引入的重要语言特征。泛型技术可以通过一种类型或方法操纵各种类型的对象,而同时又提供了编译时的类型安全保证。在JDK 1.5 以后的版本中,Java对JDK中的集合类(collections)应用了泛型。 在软件开发中,程序中的错误几乎是难以避免的。这些错误可以分为编译时错误与运行时错误。编译时错误发现早,可以根据编译器提示的错误信息比较容易进行修改,而运行时错误却难以定位和修改。泛型使得很多程序中的错误能够在编译时刻被发现,从而增加了代码的正确性和稳定性。 泛型技术的基本思想是类和方法的泛化,是通过参数化实现的,因此泛型又被称为参数化类型,即通过定义含有一个或多个类型参数的类或接口,使得程序员可以对具有类似特征与行为的类进行抽象。下面通过一个例子说明泛型的概念。 在JDK 1.4及以前版本中,因为集合类可以保存Object及其子类的对象,所以可以创建一个集合类的实例,如LinkedList,存放各种类型的对象。程序员对LinkedList中的对象的具体类型是清楚的,并且要通过强制类型转换才能使用某些特定于对象的操作。有时,程序员也可以通过注释或其他说明性手段说明LinkedList中保存的对象类型。但是,编译程序是无法通过这些程序或注释了解到集合中对象类型的任何信息,也就无法进行类型检查,因此容易发生运行时错误。如例57所示。 例57不使用泛型的集合类示例。 1 import java.util.*; 2 public class ListTest { 3 public static void main(String[] args) { 4 5//注意: 列表中只存放Integer类型的变量 6List listofInteger = new LinkedList(); 7listofInteger.add(new Integer(2000)); 8listofInteger.add("8"); 9 10Integer x = (Integer) listofInteger.get(0); 11System.out.println(x); 12x = (Integer) listofInteger.get(1); 13System.out.println(x); 14 } 15} 例57的第8行中,程序员误将数字8以字符串的形式放到了listofInteger中,而在第12行,将“8”取出后,执行Integer的强制类型转换,这是错误的。例57会通过编译,但在运行时将会出现错误信息,如图57所示。 图57例57的运行结果 如果使用泛型编写上述程序,将程序员对LinkedList中对象类型的意图传递给编译器,则例57中程序员的疏忽会在编译时就被检查出。例58是采用泛型编写的程序。 例58使用泛型的集合类使用测试。 1import java.util.*; 2public class ListTestWithGenerics { 3public static void main(String[] args) { 4 5ListInteger listofInteger = new LinkedListInteger(); 6listofInteger.add(new Integer(2000)); 7listofInteger.add("8"); 8 9Integer x = listofInteger.get(0); 10System.out.println(x); 11x = listofInteger.get(1); 12System.out.println(x); 13} 14} 例58中,第5行变量listofInteger的声明List,表示它不是一个存放Object类型对象的列表,而是存放Integer对象的列表。此处,List就是一个带有类型参数(Integer)的泛化接口,并且在创建这个列表对象时,也要指定类型参数。另外,例58中的第9行和第11行原有的强制类型转换也去掉了。 通过例58第5行中的声明,编译器了解了程序员对变量listofInteger类型限定的意图,并且将在编译时对程序进行相应的类型检查,保证所有对listofInteger变量操作满足其类型的要求。而强制类型转换只是表明程序员对某行代码的操作认为是正确的,但是无法实现编译时的检查。在例58的第7行中向listofInteger增加非Integer类型的对象,则在编译时会出现下列错误: ListTestWithGenerics.java:7: 找不到符号 符号: 方法 add(java.lang.String) 位置: 接口 java.util.List”,并且把MyBox类体中出现的所有Object都用T进行替换,从而将MyBox 定义为能够存放各种确定类型对象容器的抽象类型。例如,当我们在代码中通过MyBox创建一个MyBox对象,则该对象就只能存放Integer类型的对象。从上述例子中也可以看出,泛型的定义并不复杂。可以将T看作一类特殊的变量,该变量的值在使用时指定,可以是除了基本数据类型之外的任意类型,包括类、接口,甚至可以是一个类型变量。T可以被称为是一种类型形参,或类型参数。 泛型在使用时,必须像方法调用一样执行“泛型调用”, 将泛型中的类型变量T替换为具体的类、接口等,如例59所示。 例59泛型类的定义及其使用示例。 class MyBoxT { private T t; public void add(T t) { this.t = t; } public T get() { return t; } } public class MyBoxTest{ public static void main(String args[]){ MyBoxInteger aBox; aBox = new MyBoxInteger(); aBox.add(new Integer(1000)); Integer i = aBox.get(); System.out.println("The Integer is : "+i); } } 例59的运行结果如下: The Integer is : 1000 例59中出现对泛型MyBox的一个调用: MyBoxInteger aBox; MyBox读作“MyBox of Integer”。泛型调用与普通的方法调用相类似,所不同的是泛型调用时传递的实参是一个具体的类型而不是普通意义上的实参值。泛型的一个调用使泛型被固定为参数T所指定的类型,所以一般被称为参数化类型(parameterized type)。参数化类型的实例化还是使用new关键字,只是要在类名与“()”之间插入带有尖括号的参数类型。例如: aBox = new MyBoxInteger(); 需要注意的是,泛型中的类型变量自身并不是实际存在的类型,即根本不存在T.java或T.class,并且T也不是泛型类名的一部分。另外,一个泛型可以有多个类型参数,但是每个参数在该泛型中应是唯一的。例如不能出现MyBox,但可以出现MyBox。 2. 类型参数的命名习惯 习惯上,类型参数的名称用单个大写字母表示。这使得类型参数能够与其他变量名或类名、接口名有明显的区别。最常用的类型参数名包括如下几种。  E——Element,表示元素,一般在JDK的集合类中使用。  K——Key,表示键值。  N——Number,表示数字。  T——Type,表示类型。  V——Value,表示值。  S,U,V等——可被用作一个泛化类型的第二个,第三个,第四个类型参数。 3. 泛型中的子类 在Java中,父类的变量可以指向子类的对象,因为子类被认为是与父类兼容的类型。因此,下列的代码是合法的: Object someObject = new Object(); Integer someInteger = new Integer(10); someObject = someInteger; 在泛型中,这一点仍然是成立的。可以使用一个父类作为类型参数调用泛型,而在后续对参数化类的访问中,使用该父类的子类对象。例如: MyBoxNumber box = new MyBoxNumber(); box.add(new Integer(10)); box.add(new Double(10.1)); 因为Integer和Double是Number的子类,所以上述代码是合法的。但在泛型中, MyBox与MyBox和MyBox之间没有父 子类关系,即MyBox和MyBox不是MyBox的子类。 首先从语义上,虽然Integer和Double是Number的子类,但容纳Integer和Double对象的MyBox却不一定是容纳Number对象MyBox的子类。再如,假设Animal是Lion,Butterfly的父类,而Cage、Cage、Cage分别是关养所有类型动物、狮子和蝴蝶的笼子。为了使各种类型的动物都不能逃跑,Cage需要考虑各种情况,既要能够关住狮子、老虎,也要关住蝴蝶、蚯蚓,而Cage和Cage却只要能够关住狮子和蝴蝶就可以了,它们并没有Cage的所有特征并且要远比Cage简单。因此,Cage 不是 Cage的子类,而Cage 也不是 Cage的子类。 另外,从Java泛型的语言机制方面,再进一步理解这个问题。假设Java中允许List是List的子类,则程序中可以出现下列代码: 1ListStringls = new ArrayListString(); 2ListObject lo = ls; 3Lo.add(new Object()); 4String s = ls.get(0); 但实际上上述代码是有问题的。第4行中,ls.get(0)返回的对象类型是Object,而String类型的变量s是不能指向Object对象的,这是因为父类弱,子类强,父类中往往不包含子类的很多信息,所以不能按照子类的变量访问父类对象。根据第1行的定义,ls指向一个字符串列表,如果没有第2、3行,第4行的代码是合法的。由于假设List是List的子类,第2行代码合法并使得字符串列表有了另一个入口lo。通过lo可以将Object类型的对象放入列表,从而出现通过String类型变量ls访问到Object对象的情况,所以List是List子类的假设是不成立的。 因此,即使调用泛型的实参类型之间有父子类关系,调用后得到的参数化类型之间也不会具有同样的父子类关系。 5.6.3通配符 Java允许在泛型的类型形参中使用通配符(wildcards),以提高程序的灵活性。下面通过例子说明泛型中通配符的作用。 如果我们要编写一个方法实现给笼子里的动物喂食的操作,可以编写一个feedAnimals()方法: void feedAnimals(CageAnimal someCage) { for (Animal a : someCage) a.feedMe(); } 但上述feedAnimals()方法,实际上只能对Cage中的动物喂食,给狮子和蝴蝶的喂食却不能调用该方法,因为Cage 和Cage并不是Cage的子类。在这里需要定义所有动物笼子的父类。 Java泛型中,提供了通配符实现这种类的定义: 以通配符“?”替代泛型尖括号中的具体类型,表明该泛型的类型是一种未知的类。例如Cage<?>,表示一种未知类型物体的笼子,可以指任何存放物体的笼子,可能是一种Animal的笼子,也可能是Lion的笼子,还可能是放某种水果Fruit的笼子。Cage<?>可以认为是Cage、Cage、Cage的父类。 在上述feedAnimals()中,需要定义所有动物笼子的父类。这可以通过使用受限通配符(Bounded wildcard)来实现: Cage? extends Animal “? extends Animal”的含义是Animal或其某种未知的子类,也可以理解为“某种动物”。Cage泛指Animal及其子类的笼子,是Cage和Cage的父类。在“? extends Animal”中,Animal被认为是泛型变量的上限。也可以用super关键字代替extends定义泛型变量的下限: ,其含义是Animal或其未知的某个父类。在上文的Cage中,称为无限制通配符(unbounded wildcards),实际上是与等价的。例510中给出了一个完整示例。 例510泛型中的通配符示例。 import java.util.*; class CageE extends LinkedListE{}; class Animal{ public void feedMe(){ }; } class Lion extends Animal{ public void feedMe(){ System.out.println("Feeding lions"); } } class ButterFly extends Animal{ public void feedMe(){ System.out.println("Feeding butterflies"); } } public class WildcardsTest{ public static void main(String args[]){ WildcardsTest t = new WildcardsTest(); CageLion lionCage = new CageLion(); CageButterfly butterflyCage = new CageButterfly(); lionCage.add(new Lion()); butterflyCage.add(new Butterfly()); t.feedAnimals(lionCage); t.feedAnimals(butterflyCage); } void feedAnimals(Cage? extends Animal someCage) { for (Animal a:someCage) a.feedMe(); } } 例510的运行结果如下: Feeding lions Feeding butterflies 5.6.4泛化方法 Java泛型中,类型参数还可以出现在方法声明中,以定义泛化方法(generic methods)。泛化方法与泛型的声明类似,但泛化方法中类型参数的作用域只限于声明它的方法。下面例511中给出了一个泛化方法的例子。 例511泛化方法示例。 class MyBoxT { private T t; public void add(T t) { this.t = t; } public T get() { return t; } publicU void inspect(U u){ System.out.println("T: " + t.getClass().getName()); System.out.println("U: " + u.getClass().getName()); System.out.println(); } } public class BoxTest{ public static void main(String[] args) { MyBoxInteger integerBox = new MyBoxInteger(); integerBox.add(new Integer(10)); System.out.println("The first inspection:"); integerBox.inspect("some text"); System.out.println("The second inspection:"); integerBox.inspect(new Double(100.0)); } } 例511的运行结果如下: The first inspection: T: java.lang.Integer U: java.lang.String The second inspection: T: java.lang.Integer U: java.lang.Double 例511中,inspect()定义了一个类型参数U,是一个泛化方法。该方法把一个对象的类型打印到标准输出上。为了进行对比,该方法也输出了将类型变量T所指向对象的类型。 泛化方法的定义是在一般方法声明中增加了类型参数的声明。具体是在方法声明的各种修饰符,如public、final、static、abstract、synchronized等,与方法返回类型之间,增加一个带尖括号的类型参数列表。类型参数表的定义与泛型中的定义一样,也可以使用受限类型参数。 Java中,不仅可以对实例方法进行泛化,也可以对静态方法、构造方法进行泛化,即所有方法都可以定义为泛化方法。 注意: 在例511的main()中,在对泛化方法inspect()的调用时,并没有显式传递实参的类型,只是像普通的方法调用一样用实参去调用该方法。这主要是Java编译器具有类型推理的能力,它根据调用方法时实参的类型,推理得出被调用方法中类型变量的具体类型,并据此检查方法调用中类型的正确性。 泛化方法实现的功能,有时也可以用带有通配符的泛型实现。例如在下列代码中,containsAll()与 addAll()两个方法都定义为泛化方法: interface CollectionE { publicT boolean containsAll(CollectionT c); publicT extends E boolean addAll(CollectionT c); } 但这两个方法也可以利用通配符定义: interface CollectionE { public boolean containsAll(Collection? c); public boolean addAll(Collection? extends E c); } 那么泛化方法与通配符分别适合怎样的应用呢?从前面对通配符的介绍中可以看到,引入通配符的主要目的是支持泛型中的子类,从而实现多态。如果方法泛化的目的只是为了能够适用于多种不同类型,或支持多态,则应该使用通配符。泛化方法中类型参数的优势是可以表达多个参数或返回值之间的类型依赖关系。如果方法中并不存在类型之间的依赖关系,则可以不使用泛化方法,而使用通配符。在上述Collection的泛化方法定义中,类型参数T只使用了一次,并且containsAll()与 addAll()只有一个参数,返回值也不依赖于类型参数,因此适宜采用通配符方式。 当然,泛化方法与通配符也可以一起使用,如下面的代码: class Collections { public staticT void copy(ListT dest, List? extends T src){…} } 一般地,因为通配符更清晰、简明,因此建议尽量采用。另外,通配符还可以在方法声明之外使用,例如类变量、局部变量与数组的类型声明等。 5.6.5类型擦除 Java虚拟机中,并没有泛型类型的对象。泛型是通过编译器执行一个被称为类型擦除(type erases)的前端转换来实现的。类型擦除可以理解成一种源程序到源程序的转换,即把带有泛型的程序转换为不包含泛型的版本。 一般地,擦除进行下列处理。 (1) 用泛型的原生类型替代泛型。 原生类型(raw type)是泛型中去掉尖括号及其中的类型参数的类或接口。泛型中所有对类型变量的引用都替换为类型变量的最近上限类型,如对于Cage,T的引用将用Animal替换,而对于Cage,T的引用将用Object替换。 例如,对于例59中定义的泛型MyBox,类型擦除后得到了相应的原生类型MyBox: public class MyBox { private Object t; public void add(Object t) { this.t = t; } public Object get() { return t; } } MyBox是一个普通类,与泛型引入之前的类一样。 (2) 对于含泛型的表达式,用原生类型替换泛型。 例如,List的原生类型是List。类型擦除中,List被转换成List。对于泛型方法的调用,如果擦除后返回值的类型与泛型声明的类型不一致,则会插入相应的强制类型转换。 (3) 对于泛化方法的擦除,是将方法声明中的类型参数声明去掉,并进行类型变量的替换。 例如,对于方法: public staticT extendsComparable Tmin(T[] a) 类型擦除后,转换为: public staticComparablemin(Comparable [] a) 下面给出一个泛型的类型擦除示例。对于下列loophole()方法: public String loophole(Integer x) { ListString ys = new LinkedListString(); ys.add(x.toString()); return ys.iterator().next(); } 经编译器进行泛型类型擦除后,实际运行的代码如下: public String loophole(Integer x) { List ys = new LinkedList(); ys.add(x); return (String) ys.iterator().next(); } Java虚拟机对于泛型采用擦除机制的主要目的,是为了与JDK 1.5之前的已有代码兼容。在JDK 1.5及以后版本中,对于JDK中定义的泛型如集合类,应尽量使用泛型机制,而不要使用原生类。如果将泛型与原生类混合使用,编译器会给出一些类型未检查的警告。对于这样的警告应给予重视并进行程序检查,否则就有可能出现运行时错误。 5.6.6集合类 1. 集合类概述 一个集合对象或一个容器表示了一组对象,集合中的对象称为元素。在这个对象中,存放指向其他对象的引用。Java的Collections API 包括了下列核心集合接口,如图58所示。 图58Java Collections API的核心接口 图58中的Java Collections API的核心接口支持泛型,并且形成了两个独立的树状结构。Map是一种特殊的集合,与一般的集合不同。 1) Collection Collection接口是集合接口树的根,它定义了集合操作的通用API。对Collection接口的某些实现类允许有重复元素,而另一些不允许有重复元素; 某些是有序的而另一些是无序的。JDK中没有提供这个接口的实现,而是提供了它的子接口如Set和List的实现。 2) Set Set中不能包含重复的元素。它是数学中“集合”概念的抽象,可以用来表示类似于学生选修的课程集合或机器中运行的进程集合等。 3) List List是一个有序的集合,称为列表或序列。List中可以包含重复的元素。可以通过元素在List中的索引序号访问相应的元素。Vector就是一种常用的List。 4) Map Map实现键值到值的映射。Map中不能包含重复的键值,每个键值最多只能映射到一个值。Hashtable就是一种常用的Map。 5) Queue Queue是存放等待处理的数据的集合,称为队列。Queue中的元素一般采用FIFO(先进先出)的顺序,也有以元素的值进行排序的优先队列。无论队列采用什么样的顺序,remove()和poll()方法都是对队列的最前面元素进行操作。在FIFO队列中,新添加的元素都是放到队列的尾部,其他队列可能采用不同的排放策略。 6) SortedSet和SortedMap SortedSet和SortedMap分别是具有排序性能的Set和Map。 2. 几种常用集合 1) Set Set继承了Collection接口,Set的方法都是从Collection继承的,它没有声明其他方法。Set接口中所包含的方法如下,实现Set的类也实现了这些接口,所以我们可以对具体的Set对象调用这些方法: public interface SetE extends CollectionE { //基本操作 int size(); boolean isEmpty(); boolean contains(Object element); boolean add(E element); boolean remove(Object element); IteratorE iterator();//返回当前集合元素的反复器iterator //集合元素批操作 boolean containsAll(Collection? c); boolean addAll(Collection? extends E c); //将集合c的元素都加到本集合中,成功返回 //true,否则为false boolean removeAll(Collection? c); boolean retainAll(Collection? c); //在当前集合中只保留属于c的元素,如果当 //前集合发生变化则返回true //否则返回false void clear();//清除集合中的所有元素 //数组操作 Object[] toArray(); //返回包含当前集合所有元素的数组。该数组是新创建的,与当前集 //合独立。因此调用者可以随意修改返回的数组 T T[] toArray(T[] a);//返回包含当前集合所有元素的数组。所返回数组的运行时类型是数 // 组a的类型。如果数组a能够容下集合的所有元素,则将集合元素 //写入a并返回,否则创建类型与a相同、长度等于集合长度的数组 } JDK中提供了实现Set接口的3个实用的类: HashSet类、TreeSet类和LinkedHashSet类。 HashSet类是采用Hash表实现了Set接口。一个HashSet对象中的元素存储在一个Hash表中,并且这些元素没有固定的顺序。由于采用Hash表,所以当集合中的元素数量较大时,其访问效率要比线性列表快。 TreeSet类实现了SortedSet接口,是采用一种有序树的结构存储集合中的元素。TreeSet对象中元素按照升序排序。 LinkedHashSet类实现了Set接口,采用Hash表和链表相结合的结构存储集合中的元素。LinkedHashSet对象的元素具有固定的顺序,它集中了HashSet与TreeSet的优点,既能保证集合中元素的顺序又能够具有较高的存取效率。 下面给出一个Set的使用实例。 例512Set的使用示例。 import java.util.*; public class FindDups { public static void main(String args[]) { //创建一个HashSet对象,默认的初始容量是16 SetString s = new HashSetString(); //将命令行中的每个字符串加入到集合s中,其中重复的字符串将不能加入,并被打印输出 for (String a : args){ if (!s.add(a)) System.out.println("Duplicate detected: "+a); } //输出集合s的元素个数以及集合中的所有元素 System.out.println(s.size()+" distinct words detected: "+s); } } 如果在Windows的命令窗口中输入下列命令: java FindDups I come I see I go 则例512的运行结果如下: Duplicate detected: I Duplicate detected: I 4 distinct words detected: [see, come, I, go] 2) List List是一种有序的集合,它继承了Collection接口。除了继承了Collection中声明的方法,List接口中还增加了如下操作。  按位置存取元素: 按照元素在List中的序号对其进行操作。  查找: 在List中搜寻指定的对象并返回该对象的序号。  遍历: 使用了ListIterator实现对一个List的遍历。  子List的截取,即建立List的视图(view): 能够返回当前List中的任意连续的一部分,形成子List。 List接口的定义如下: public interface ListE extends CollectionE { //按位置存取元素 E get(int index); E set(int index, E element); Boolean add(E element); void add(int index, E element); E remove(int index); boolean addAll(int index, Collection? extends E c); //查找 int indexOf(Object o); int lastIndexOf(Object o); //遍历 ListIteratorE listIterator(); ListIteratorE listIterator(int index); //子List的截取 ListE subList(int from, int to); } JDK中提供了实现List接口的3个实用类: ArrayList类、LinkedList类和Vector类,这些类都在java.util包中。 ArrayList类采用可变大小的数组实现了List接口。除了实现List接口,该类还提供了访问数组大小的方法。ArrayList对象会随着元素的增加其容积自动扩大。这个类是非同步的(unsynchronized),即如果有多个线程对一个ArrayList对象并发访问,为了保证ArrayList数据的一致性,必须在访问该ArrayList的程序中通过synchronized关键字进行同步控制。ArrayList是3种List中效率最高也是最常用的。它还可以使用System.Arraycopy()进行多个元素的一次复制。除了非同步特性之外,ArrayList几乎与Vector类是等同的,可以把ArrayList看作没有同步开销的Vector。 LinkedList类采用链表结构实现List接口。除了实现List接口中的方法,该类还提供了在List的开头和结尾进行get,remove和insert等操作。这些操作使得LinkedList可以用来实现堆栈、队列或双端队列。LinkedList类也是非同步的。 Vector类采用可变体积的数组实现List接口。该类像数组一样,可以通过索引序号对所包含的元素进行访问。它的操作方法几乎与ArrayList相同,只是它是同步的。 例513是一个使用List的例子。这是一个关于扑克牌的例子。该例中用ArrayList保存了52张扑克牌,并通过Collections类的static方法shuffle()实现“洗牌”操作。最后利用dealHand()方法为参加游戏的人每人生成一手牌,每手牌的牌数是指定的。该程序有两个命令行参数: 参加纸牌游戏的人数以及每手牌的牌数。 例513List的使用示例。 import java.util.*; class Deal { public static void main(String args[]) { int numHands = Integer.parseInt(args[0]); int cardsPerHand = Integer.parseInt(args[1]); //生成一副牌(含52张牌) String[] suit = new String[] {"spades", "hearts", "diamonds", "clubs"}; String[] rank = new String[] {"ace","2","3","4","5","6","7","8","9","10","jack","queen","king"}; ListString deck = new ArrayListString(); for (String ss : suit) for (String sr : rank) deck.add(sr + " of " + ss); Collections.shuffle(deck);//随机改变deck中元素的排列次序,即洗牌 for (int i=0; inumHands; i++) System.out.println(dealHand(deck, cardsPerHand));//生成一手牌并将其输出 } public static List dealHand(ListString deck, int n) { int deckSize = deck.size(); //从deck中截取一个子链表 ListString handView = deck.subList(deckSize-n, deckSize); ListString hand = new ArrayListString(handView);//利用该子链表创建一个链表 handView.clear();//将子链表清空 return hand; } } 如果在Windows命令窗口中输入下列命令: java Deal 2 5 则例513的某次运行结果如下: [4 of clubs, 4 of hearts, 8 of clubs, queen of clubs, 2 of hearts] [7 of spades, 6 of hearts, 10 of diamonds, 6 of spades, 10 of spades] 例513每次运行的结果都可能是不一样的。 注意: List.subList()方法返回的子List称为当前List的视图(view),这意味着对子List的改变将反映到原来的List中。所以例513的dealHand()方法中,执行handView.clear()方法将handView清空,同时也将deck中对应于handView的元素删除了。因此每次调用dealHand()方法都将返回包含deck中后面指定数目的元素并把它们从deck中清除掉。 例513中用到的java.util.Collections类是一个集合操作的实用类。该类提供了集合操作的很多方法,如同步、排序、逆序等,而且所有方法都是static,因此可以不通过实例化直接调用。例如,常用集合Set、List和Map的put()、get()、remove()等方法是不同步的,如果有多个线程同时对一个集合对象进行操作,就可能导致集合对象数据的错误。所以必须对共享集合的操作实现同步控制,使得一段时间内只能有一个线程对集合进行操作,保证数据的一致性。Collection类提供了集合对象同步控制,它提供了一系列方法使集合对象具有同步控制能力,例如调用synchronizedList(List list)方法,将得到一个基于指定list的具有同步控制的list。 3) Queue Queue除了基本的Collection接口中定义的操作,还提供了其他插入、删除和元素检查等操作。队列可以限定其元素的个数,这样的队列称为有界队列。在java.util.concurrent包中的某些队列的实现是有界的,而在java.util包中队列的实现类是没有元素个数限制的。Queue接口的定义如下: public interface QueueE extends CollectionE { E element(); boolean offer(E e); E peek(); E poll(); E remove(); } Queue提供的插入、删除和元素检查等方法都有两种形式,每种形式执行的操作是一样的,只是在操作不能正常进行时的处理不一样: 一种方式是抛出异常,另一种方式是返回null或false等特定值。关于这两种方式的具体说明,如表51所示。 表51队列操作的两种方式比较 队列操作功能说明异常情况抛出异常的方法返回特定值的方法 插入向队列中加入元素有界队列满add(e)offer(e),返回false 移除从队首移走一个元素队列空remove()poll(),返回null 元素检查返回队首元素,但不删除该元素队列空element()peek(),返回null 例514是一个使用队列保存待处理数据的例子。程序实现了一个倒计数的计数器。具体处理流程是: 先把从时间time到0的所有整数,按从大到小的顺序存储在队列queue中,然后每隔1秒从队列中移出一个数打印输出。 例514Queue使用示例。 import java.util.*; public class Counter { public static void main(String[] args){ int time = 5;//设定计时开始时间 QueueInteger queue = new LinkedListInteger();//创建队列 for (int i = time; i= 0; i--) queue.add(i);//把整数秒数存储在队列中 while (!queue.isEmpty()) { System.out.println(""+queue.remove()); try{ Thread.sleep(1000); }catch(InterruptedException e){ }//把队列中的整数输出 } } } 例514的运行结果如下: 5 4 3 2 1 0 4) Map Map包含了一系列“键(key)值(value)”之间的映射关系。一个Map对象可以看成是一个“键值”对的集合,可以在该集合中通过一个键找到其对应的值。“键”和“值”可以是任意类型的对象。 如图58所示,Map接口是独立于Collection接口体系的,Map体系中的所有类和接口的方法都源自Map接口。Map接口的定义如下所示: public interface MapK,V { //基本操作 V put(K key, V value); V get(Object key); V remove(Object key); boolean containsKey(Object key); boolean containsValue(Object value); int size(); boolean isEmpty(); //整体批操作 void putAll(Map? extends K, ? extends V m); void clear(); //集合视图 public SetK keySet(); public CollectionV values(); public SetMap.EntryK,V entrySet(); //为entrySet 元素定义的接口 public interface Entry { K getKey(); V getValue(); V setValue(V value); } } Map接口的方法主要实现下列3类操作。  基本操作: 包括向Map中添加值对,通过键获取对应的值或删除该“键值”对,测试Map中是否含有某个键或某个值,以及返回Map包含元素个数等。  批操作: 包括向当前Map中添加另一个Map和清空当前Map的操作。  集合视图: 包括获取当前Map中键的集合、值的集合以及所包含的“键值”对等。其中“键值”对集合的元素类型由Map中的内部接口Entry定义。 JDK中提供了实现Map接口的实用类,包括HashMap类、HashTable类、TreeMap类、WeekHashMap类和IdentityHashMap类等。 HashMap类和HashTable类都采用Hash表实现Map接口。HashMap是无序的,它与HashTable几乎是等价的,区别在于HashMap是非同步的并且允许有空的键与值。由于采用Hash函数,对于Map的普通操作性能是稳定的,但如果使用iterator访问Map,为了获得高的运行效率最好在创建HashMap时不要将它的容量设得太大。 TreeMap类与TreeSet类相似,是采用一种有序树的结构实现了Map的子接口SortedMap。该类将按键的升序的次序排列元素。 WeekHashMap类与HashMap相类似,只是WeekHashMap中的 “键值”对在其键不再被使用时将自动被删除,由垃圾搜集器回收。 IdentityHashMap类与其他Map类相比,其特殊之处是在比较两个键是否相同时,比较的是键的引用而不是键对象自身。 上述Map类中,HashMap(无序的Map)和TreeMap(有序的Map)是常用的。 下面给出一个Map的使用实例。例515中,利用TreeMap进行单词词频的统计。将单词与该单词的词频作为“键值”的映射对。 例515利用Map进行单词词频的统计。 import java.util.*; public class Freq { public static void main(String args[]) { String[] words={"if","it","is", "to", "be", "it", "is", "up", "to", "me", "to", "delegate"}; Integer freq; MapString, Integer m = new TreeMapString, Integer(); //构造字符串数组words的单词频率表 。以单词为key,以词频为value for (String a : words) { freq = m.get(a);//获取指定单词的词频 //词频递增 if (freq==null){ freq = new Integer(1); }else{ freq = new Integer(freq.intValue() + 1); } m.put(a, freq);//在Map中更改词频 } System.out.println(m.size() + " distinct words detected:"); System.out.println(m); } } 例515的运行结果如下: 8 distinct words detected: {be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1} 3. 集合元素的遍历 Java Collections API 为集合对象提供了iterator(重复器),用来遍历集合中的元素。Iterator接口中的方法使我们可以向前遍历所有类型的集合。在对一个Set对象的遍历中,元素的遍历次序是不确定的。List对象的遍历次序是从前向后,并且List对象还支持Iterator的子接口ListIterator,该接口支持List的从后向前的反向遍历。 Iterator层次体系中包含两个接口: Iterator以及ListIterator。它们的定义如下: public interface IteratorE { boolean hasNext(); E next(); void remove(); } public interface ListIteratorE extends IteratorE { boolean hasNext(); E next(); boolean hasPrevious(); E previous(); int nextIndex(); int previousIndex(); void remove(); void set(E e); void add(E e); } Iterator中的remove()方法将删除当前遍历到的元素,即删除由最近一次next()或previous()调用返回的元素。 ListIterator中的set()方法可以改变当前遍历到的元素。add()方法将在下一个将要取得的元素之前插入新的元素。如果实际操作的集合不支持remove()、set()或add()方法, 图59Iterator层次结构图 则将抛出UnsupportedOperationException。 图59表示了Iterator和ListIterator的继承关系,以及它们与Collection和List的关系。 例516是利用ListIterator操作一个ArrayList的例子。 例516ListIterator的使用示例。 import java.util.*; public class ListIteratorDemo { public static void main(String[] args) { ListInteger list = new ArrayListInteger(); //向list中添加元素 for(int i=1; i5; i++){ list.add(new Integer(i)); } System.out.println("The original list : "+list); //创建list的iterator ListIteratorInteger listIter = list.listIterator(); listIter.add(new Integer(0));//在序号为0的元素前添加一个元素 System.out.println("After add at beginning:"+list); if (listIter.hasNext()) { int i = listIter.nextIndex();//i的值将为1 listIter.next();//返回序号为1的元素 listIter.set(new Integer(9));//修改list中的序号为1的元素 System.out.println("After set at "+i+":"+list); } if (listIter.hasNext()) { int i = listIter.nextIndex();//i的值将为2 listIter.next(); listIter.remove();//删除序号为2的元素 System.out.println("After remove at "+i+" : "+list); } } } 例516的运行结果如下: The original list : [1, 2, 3, 4] After add at beginning:[0, 1, 2, 3, 4] After set at 1:[0, 9, 2, 3, 4] After remove at 2 : [0, 9, 3, 4] 5.7枚 举 类 型 5.7.1枚举概述 枚举类型(enum type)是在JDK 1.5 以后引入的一种新的语法机制,一般用于表示一组常量。因此枚举定义中的域(field)是由固定的一组常量组成的,这些域的名称一般都使用大写字母。在Java中,应该尽量使用枚举类型表达固定不变的一组常量,例如方位(值为NORTH、SOUTH、EAST 和WEST),一年中的季节(WINTER、SPRING、SUMMER、FALL)等,如例517所示。 例517枚举类型定义示例。 public enum Week { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } 虽然最简单的Java枚举定义看起来与C、C++和C#中的枚举定义很相像,但Java枚举类型的功能却比这些语言中的枚举强大得多。一个枚举的声明实际上是定义了一个类。这个类的声明中可以包含方法和其他属性,以支持对枚举值的相关操作,还可以实现任意的接口。枚举类型提供了所有Object类的方法,并且实现了Comparable和 Serializable接口。 5.7.2枚举类型的定义 枚举类型的定义格式与类的声明类似,一般格式如下: [public] enum 枚举类型名 [implements 接口名表] { 枚举常量定义 [枚举体定义] } 1. 枚举声明 被声明为public的枚举类型,可被其他包中的类访问,否则只能在定义它的包中使用。 关键字enum指明当前定义的枚举类型,而不是类或接口。枚举类型与类一样,也可以实现接口。所有的枚举类型都隐含地继承了java.lang.Enum类,由于Java不支持多继承,所以枚举类型的声明中不能再继承任何类。 2. 枚举常量定义 枚举类型实际上是具有固定实例的特殊类。这些固定的实例就是通过枚举常量定义的。枚举常量是枚举类型的static、final的实例,是枚举类型的“值”,因此枚举常量可以在其他程序中通过枚举类型名进行引用,如对例517中枚举Week的常量引用Week.SUNDAY。 最简单的枚举类型只包含一组枚举常量,如例517所示。枚举常量定义格式为: 常量1 [,常量2 [,…常量n]] [ ; ] 枚举常量之间用“,”分隔,最后用“;”表示结束。如果没有枚举体定义部分,则“;”可省略。每个枚举常量都将与一个整数值相对应,第一个枚举常量值为0,第二个为1,以此类推。 枚举类型是特殊的类。枚举类型的一个常量,实际上就是枚举类型的一个实例。在虚拟机加载枚举类型时,会调用枚举类型的构造方法,创建各个枚举实例。与类的构造方法一样,如果在枚举类型中没有显式地定义构造方法,编译器将自动为其提供一个默认构造方法。如果枚举类型在枚举体内定义了自己的构造方法,则在定义枚举常量时,可采用: 常量(参数1,参数2,…) 的形式,则在创建该枚举实例时将按照参数列表调用相应的构造方法。如果枚举常量使用默认的或枚举体内定义的不带参数的构造方法,则“()”可以省略,如例517所示。 3. 枚举体的定义 枚举体的定义与类的定义一样,可以包含变量、构造方法与成员方法,且定义形式也和类一样。但枚举类型的构造方法只能定义为private,默认也为private,这保证除了系统创建的枚举常量外,不会有任何其他程序调用枚举类型构造方法创建新的实例。 5.7.3枚举类型的方法 Java中,所有的枚举类型都默认继承于java.lang.Enum类。由于java.lang.Enum 直接继承java.lang.Object且实现了java.lang.Comparable接口,所以每个枚举类型都具有Object类和Comparable接口中可被继承的方法,常用的方法包括如下几种。  final Boolean equals(Object other): 如果other指向的对象等于此枚举常量时,返回true。  final String name(): 返回此枚举常量的名称。  final int ordinal(): 返回此枚举常量在枚举类型定义中的位置序数,第一个常量的序数值为0。  String name(): 返回枚举常量的名称,该名称与枚举常量在枚举类型声明中的名称完全一样。  String toString(): 返回枚举常量包含在枚举类型声明中的名称。该方法可以被重写,以返回枚举常量的其他名称。  Static>T valueOf(classenumType,String name): 返回指定枚举类型中指定名称的枚举常量。 另外,编译器在创建一个枚举时也将自动加入一些特殊的方法。例如,编译器将加入一个静态方法values(),该方法返回一个包含该枚举类型所有常量的数组,并且数组中常量的顺序与枚举类型中声明的顺序相同。values()方法经常和for循环一起使用,实现对一个枚举类型所有值(常量)的遍历。例518中,利用values()方法打印输出枚举Week中的所有常量。 例518枚举类型的values()方法使用示例。 enum Week { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } public class EnumValuesTest{ public static void main(String args[]){ for (Week w:Week.values()){ System.out.print(w.name()+", "); } System.out.println(); } } 例518的运行结果如下: SUNDAY. MONDAY. TUESDAY. WEDNESDAY. THURSDAY. FRIDAY. SATURDAY. 5.7.4枚举的使用 枚举类型可以像其他类型一样使用,可以定义数组,可以作为参数类型 。枚举类型的变量也属于一种引用型变量,还可以通过枚举常量引用枚举类型中定义的成员。 枚举类型还可以和switch语句结合使用,如例519所示。例519中,定义了一个枚举类型Coin和一个最简单的枚举类型CoinColor。Coin中定义了PENNY、NICKEL等5个表示5种硬币枚举常量; 定义了一个变量value表示硬币的面值,构造方法Coin(int value),以及一个普通方法value()。这5个枚举常量在创建时,将调用所定义的构造方法,对私有变量value赋值。在CoinTest类的main()方法中,调用Coin.values()方法获得枚举Coin中的所有常量,并通过for循环和switch语句遍历这些常量,输出每个常量的名称、面值和颜色。 例519枚举类型使用示例。 enum Coin { PENNY(1), NICKEL(5), DIME(10), QUARTER(25); private final int value; Coin(int value) { this.value = value; } public int value() { return value; } } enum CoinColor { COPPER, NICKEL, SILVER } public class CoinTest { public static void main(String[] args) { for (Coin c : Coin.values()){ System.out.print(c + ": "+ c.value() +", "); switch(c) { case PENNY: System.out.println(CoinColor.COPPER); break; case NICKEL: System.out.println(CoinColor.NICKEL); break; case DIME: case QUARTER: System.out.println(CoinColor.SILVER); break; } } } } 例519的运行结果如下: PENNY: 1, COPPER NICKEL: 5, NICKEL DIME: 10, SILVER QUARTER: 25, SILVER 5.8包装类与自动装箱和拆箱 5.8.1基本数据类型的包装类 Java中,从执行效率的角度考虑,基本数据类型如int、double等不作为类来对待。同时Java提供了Wrapper类即包装类用来把基本数据类型表示成类。每个Java基本数据类型在java.lang包中都有一个对应的Wrapper类,如表52所示。每个Wrapper类对象都封装了基本类型的一个值。数值型类型的包装类包括Byte、Short、Integer、Long、Float和Double,都是抽象类java.lang.Number的子类。 表52Java基本数据类的包装类 基本数据类型Wrapper类基本数据类型Wrapper类 booleanBooleanintInteger byteBytelongLong charCharacterfloatFloat shortShortdoubleDouble 把基本数据类型的一个值传递给包装类的相应构造函数,可以构造Wrapper类的对象。例如: int pint = 500; Integer wInt=new Integer(pInt); int p2 = wInt.intValue(); Wrapper类中包含了很多有用的方法和常量。如数值型Wrapper类中的MIN_VALUE和MAX_VALUE常量,定义了该类型的 最小值与最大值。ByteValue()、shortValue()方法进行数值转换,valueOf()和 toString()实现字符串与数值之间的转换。例如: int x = Integer.valueOf(str).intValue(); int y = Integer.parseInt(str); 5.8.2自动装箱和拆箱 从JDK 1.5开始,Java对基本类型的数据提供了自动装箱(autoboxing)和自动拆箱(autounboxing)功能。当编译器发现程序在应该使用对象的地方使用了基本数据类型的数据,编译器将把该数据包装为该基本类型对应的包装类的对象,这称为自动装箱。类似地,当编译器发现在应该使用基本类型数据的地方使用了包装类的对象,则会把该对象拆箱,从中取出所包含的基本类型数据,这称为自动拆箱。例520给出了一个整型数值自动装箱与拆箱的示例。 例520自动装箱与拆箱示例。 public class AutoBoxingTest{ public static void main(String args[]){ Integer x, y; int c; //自动装箱,将x,y构造为两个Integer对象 x = 22; y = 15; if ( (c = x.compareTo(y)) == 0) System.out.println("x is equal to y"); else if ( c 0 ) System.out.println("x is less than y"); else System.out.println("x is greater than y"); System.out.println("The sum of x and y is "+(x+y) ); } } 例520的运行结果如下: x is greater than y The sum of x and y is 37 在例520中,变量x和y赋予了整型值。但由于它们是Integer类型的,编译器将自动把它们封装为两个Integer对象,这使得下面的x.compareTo(y)方法能正常运行。在程序最后的println()方法中,要进行x和y所包含整型值的加法运算,所以x和y将被自动拆箱,这样它们才能进行整数加法。 5.9注解Annotation 5.9.1注解的作用与使用方法 注解(Annotation)是从JDK 1.5开始引入的对程序代码的一种注释或标注机制,对所注释代码的功能没有任何影响。Java中,注解的作用主要包括:  向编译器提供信息: 编译器利用这些信息发现错误或抑制告警信息的输出。  指示编译或部署时的处理: 编译器或一些软件工具可以根据注解信息生成代码或XML文件等。  指示运行时的处理: 有些注解可以在运行时由JVM获得和进行检查处理。 在代码中使用注解的格式是: @Entity Declaration 符号“@”通知编译器,接下来的Entity是一个注解类型。注解可以用于标注程序中各种声明(Declaration),包括类、成员变量和方法等的声明。一般一个注解单独占一行,注解接下来的一行是被标注的声明。 例如下面代码中的注解Override,表示重写了父类的方法mySuperMethod(): @Override void mySuperMethod() { … } 另外,可以在注解类型名后带有括号,给出有名称或无名称的若干个元素,这些元素带有具体值,给出了注解更具体的内容。例如: @Author( name = "Benjamin Franklin", date = "3/27/2003" ) classMyClass() { … } 如果注解只有一个元素,则该元素的名称可以省略,例如: @SuppressWarnings("unchecked") void myMethod() { … } 注解也可以没有元素,此时括号可以省略,例如上面的@Override。一个声明代码也可以有多个注解,而如果这些注解是相同类型(即相同的注解类型)则被称为重复注解。例如: @Author(name = "Jane Doe") @Author(name = "John Smith") classMyClass { … } 注解类型可以是Java中定义的,也可以是自定义的。 5.9.2自定义注解类型 Java中注解类型的定义由三部分构成: 声明继承java.lang.annotation.Annotation的接口; 指定该注解类型可以标注的声明类型; 指定该注解类型的保存方式或称为作用域。 支撑注解类型上述定义内容的是Java中3个重要的类型:  java.lang.annotation包中的接口Annotation, 该接口声明了equals()、hashCode()等方法。  枚举类型ElementType,定义了注解可以标注的声明类型,包含了8个元素,每个元素表示一种声明。其中,TYPE表示类、接口(包括注释、枚举类型)的声明; FIELD表示成员变量声明; METHOD表示方法声明; PARAMETER表示参数声明; CONSTRUCTOR表示构造方法声明; LOCAL_VARIABLE表示局部变量声明; ANNOTATION_TYPE表示注解类型声明; PACKAGE表示包声明。  枚举类型RetentionPolicy,定义了注解信息的保存和作用方式,包含了SOURCE、CLASS和RUNTIME 3个元素。其中,SOURCE表示注解信息存储于源代码中,编译器处理完之后就没有该注释信息了; CLASS表示注解信息存储于类对应的.class文件中; RUNTIME表示注解信息存储于class文件中,并且可由JVM读取和处理。 注解类型定义中,一般要使用@interface、@Target和 @Retention给出新注解类型定义的三个部分,@Target和 @Retention是Java定义的注解类型。其中:  @interface表示其后面出现的接口继承了 java.lang.annotation.Annotation 接口,即该接口就是一个Annotation(注解)。“@interface”在注解定义中是必须有的,接下来可以具体定义该注解类型,主要是给出注解类型元素声明。这些声明形式上与方法声明很像,每一个方法实际上是声明了一个注解元素或配置参数。元素的名称就是方法的名称,元素的类型就是方法返回值类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。  @Target利用上述ElementType指定所定义注解可以标注的声明类型。注解类型定义中可以没有@Target,则表示该注解可以用于标注任意的声明类型。  @Retention利用上述RetentionPolicy指定所定义注解的保存和作用方式。在注解类型定义中@Retention可以不出现,默认的保存方式是RetentionPolicy.CLASS。 例如下列注解类型定义: @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface SomeAnnotation { … } 上述代码定义了注解类型SomeAnnotation,该注解能够用来标注的声明类型是ElementType.TYPE,保存方式是RetentionPolicy.RUNTIME。有了上述定义后,就可以利用SomeAnnotation进行相应声明代码的注释。 一种常见的自定义注解类型是用来代替代码中的一些注释,并可以使这些注释信息出现在Javadoc生成的文档中。例如关于代码作者、时间等信息说明的注释: // Author: John Doe // Date: 3/17/2002 // Current revision: 6 // Last modified: 4/12/2004 // By: Jane Doe // Reviewers: Alice, Bill, Cindy public class Generation3List extends Generation2List { … } 要利用注解产生上面的注释信息,首先需要定义一个注解类型: @interface ClassPreamble { String author(); String date(); int currentRevision() default 1; String lastModified() default "N/A"; String lastModifiedBy() default "N/A"; String[] reviewers(); } 在注解类型定义之后,就可以在使用该注解类型时给注解元素赋值: @ClassPreamble ( author = "John Doe", date = "3/17/2002", currentRevision = 6, lastModified = "4/12/2004", lastModifiedBy = "Jane Doe", reviewers = {"Alice", "Bob", "Cindy"} ) public class Generation3List extends Generation2List { … } 为了使注解@ClassPreamble中的信息在Javadoc产生的文档中出现,还必须在注解@ClassPreamble的定义中使用Java定义的注解@Documented: @Documented @interface ClassPreamble { // 注解元素定义 } 5.9.3Java中定义的注解类型 如上文所提到的,Java中定义了一组继承自Annotation接口的注解类型,如@Target、 @Retention、@Deprecated、 @Override、@Documented等。这些注解中,部分是用于注解Java代码,目的是向Java编译器提供信息,另外一些是作用于其他注解类型,即用来注释其他注解类型。 用于Java代码的注解,包括@Deprecated、@Override、@SuppressWarnings、@SafeVarargs和@FunctionalInterface,是在java.lang包中定义的。@SafeVarargs作用于一般方法和构造方法,说明被注释方法在其变长参数上没有执行不安全的操作,编译器可以抑制与变长参数相关的警告。@FunctionalInterface表明被注释的声明是一个功能性接口。其他三个注释是比较常用的,其具体含义和用法如下:  @Override: @Override 只能标注方法,表示该方法重写父类中的方法。如果编译器没有在方法所在类的父类中找到该方法,则会报错。例如: // 所标记的方法overriddenMethod()是一个被重写的父类方法 @Override int overriddenMethod() { }  @Deprecated: @Deprecated 所标注内容,不再被建议使用。当程序使用了 @Deprecated 注解标注的方法、类或成员变量,编译器都将产生一个警告。例如: //所标记的方法deprecatedMethod()是不再被建议使用的方法 @Deprecated static void deprecatedMethod() { }  @SuppressWarnings: @SuppressWarnings 通知编译器,对于所标注声明产生的警告,编译器要对这些警告保持静默,即不报告。例如: // 被标注方法使用了deprecated方法,注解通知编译器不要产生警告 @SuppressWarnings("deprecation") void useDeprecatedMethod() { // 下列方法是deprecated objectOne.deprecatedMethod(); } 编译器警告都属于某个特定类,Java语言中有两类警告: deprecation 和 unchecked。unchecked警告是在遇到泛型发布之前的代码时产生的。为了抑制多种类型的警告信息,可以使用下列形式: @SuppressWarnings({"unchecked", "deprecation"}) 除了上述用于Java代码的注解,Java中定义了用于标注其他注解的注解类型,被称为元注解(metaannotation)。java.lang.annotation中定义了几种元注解:  @Retention,声明了所标注注解类型的存储方式。存储方式如5.9.2节中所述。  @Documented,声明了被标注注解类型的元素可以使用Javadoc工具输出在所产生的文档中,如5.9.2节中的示例。  @Target,声明了被标注注解类型能够注释的声明类型,这些类型如5.9.2节中所述。  @Inherited,声明了所标注的注解类型具有继承性,即指子类可以继承父类的注解中被@Inherited修饰的注解类型。  @Repeatable ,声明了对于同一个声明代码,所标注的注解可以使用多次。如5.9.1节中的注解示例@Author。 5.10var局部变量类型推断 在JDK 10以后的版本,可以使用var标识符声明带有非空初始化的局部变量,这样可以使代码更简洁易读。例如,下面的代码使用了显式类型的变量声明: URL url = new URL("http://www.oracle.com/"); URLConnection conn = url.openConnection(); Reader reader = new BufferedReader( new InputStreamReader(conn.getInputStream())); 使用var进行这些局部变量声明,则代码可以写为: var url = new URL("http://www.oracle.com/"); var conn = url.openConnection(); var reader = new BufferedReader( new InputStreamReader(conn.getInputStream())); 通过var声明的局部变量,其类型可以从上下文推断得到。 var可以用于带有初始化的局部变量,增强for循环中用于指向数组或集合各个元素的索引变量,以及传统for循环中声明的局部变量; 不能用于方法与构造方法的形参、方法的返回类型、类成员变量、catch的形参以及其他类型的变量声明。具体用法如下: (1) 带有非空初始化的局部变量声明: 例如: var list = new ArrayList(); // 推断类型是ArrayList var stream = list.stream(); // 推断类型是 Stream var path = Paths.get(fileName); // 推断类型是 Path var bytes = Files.readAllBytes(path); // 推断类型是 bytes[] (2) 增强for循环(可参见3.4.4节)的索引变量: 例如: List myList = Arrays.asList("a", "b", "c"); for (var element : myList) {...} // 推断类型是String (3) 传统for循环(可参见3.3.2节)中声明的索引变量: 例如: for (var counter = 0; counter < 10; counter++) {...} // 推断类型是 int (4) trywithresources(可参见6.2.1节)的变量: 例如: try (var input = new FileInputStream("validation.txt")) {...} //推断类型是FileInputStream (5) 在JDK 11以后,对于使用隐含类型Lambda表达式(可参见4.3.5节)的形参声明可以使用var: 例如: (x, y) ->x.process(y) 等价于: (var x, var y) ->x.process(y) 需要注意的是,隐含类型Lambda表达式对于使用var,必须所有的形参都使用,或都不使用,并且var只可以在隐含类型Lambda表达式中使用,即显式类型Lambda表达式不能使用。例如,下列Lambda表达式形参的声明是非法的: 隐含类型Lambda表达式: (var x, y) ->x.process(y) // var 和非var混用 显式类型Lambda表达式: (var x, int y) ->x.process(y) // var 和显式类型声明混用 5.11小结 本章介绍了Java的高级特征,包括static变量、static方法与static语句块、抽象类与接口、包、泛型与集合类、枚举类型、包装类与自动装箱和拆箱。其中抽象类、接口与package是Java面向对象的重要高级特征,也是本章的重点。抽象类的主要作用是建立一类对象的抽象模型,定义通用的接口,支持多态。接口可以认为是一种极度抽象的抽象类,利用接口实现多重继承,避免了程序的复杂性与不安全性,使代码简单、可靠。package机制体现了Java的封装特性,为Java类的管理、访问控制、命名管理等提供了有效的方法。泛型提供了编译时的类型安全保证,增加了程序的可读性和强壮性。集合类是Java中一组很实用的类,应该掌握Java集合类的框架与各种集合类的特点与用法,在使用集合类时要尽量使用泛型。枚举是Java新增加的表示一组常量的一种类型,一个枚举类型相当于一个类,具有很强大的功能。 习题5 1. 举例说明类方法与实例方法以及类变量与实例变量之间的区别。 2. 什么是接口?接口的意义是什么? 3. 什么是包?如何定义包? 4. 什么是抽象类?抽象类与接口有何区别? 5. 下列接口的定义中,哪个是正确的? (1) interface Printable{ void print(){ }; } (2) abstract interface Printable{ void print(); } (3) abstract interface Printable extends Interface1,Interface2{ void print(){ }; } (4) interface Printable{ void print(); } 6. 在一个图书管理程序中,类Book,Newspaper和Video都是类Media的子类。编写一个类,该类能够实现对一组书、报纸等的存储,并提供一定的检索功能。 7. 利用枚举类型重新编写例513。