Java程序设计与应用开发(第3版)
上QQ阅读APP看书,第一时间看更新

3.1 类和对象

万物皆是对象,人、汽车、日期时间、银行账号、音乐、图形图像、颜色、字体、文件、记录以及网络地址等,在面向对象的程序设计中都视为对象。把这些现实世界的对象和概念用计算机语言抽象表示出来就是类。

在Java中,类是一种重要的复合数据类型,是组成Java程序的基本要素,也是面向对象程序设计的基本单位。类定义了某类对象的共有变量和方法。变量是现实对象的属性或状态的数字化表示,方法是对现实对象进行的某种操作或其对外表现的某种行为。对象是由一组相关的变量和方法共同组成的一个具体的软件体。

类实例化就成为对象。对象和类之间的关系就如同房子和其设计图纸的关系。类的作用就像一个模板,所有对象实例依照它来创建。

3.1.1 类声明

类由传统的复合数据类型(如C语言中的结构)发展而来,几乎所有的面向对象的程序设计语言都采用class来声明类,在Java中类声明的格式如下:

     [public][abstract | final] class className
     [extends  superClassName]  [implements  interfaceNameList ...]
     {...class body...}

(1)public为类的访问控制符。Java类具有两种访问控制符:public和package。public允许一个类具有完全开放的可见性,所有其他类都可以访问它;省略public则为package可见性,即只有位于同一个包中的类可以访问该类。

(2)abstract指明该类为一个抽象类,暗示该类需要被继承,才能实例化。final阻止一个类被继承。

(3)className指定该类类名。

(4)extends为类继承,superClassName为父类。如果在类定义时没有指定类继承关系,Java将自动从Object类派生该类。

(5)implements实现接口,interfaceNameList为被实现的一个或多个接口名,接口实现是可选的。虽然Java类只可继承一个父类,却可同时实现多个接口。

(6){...}由一对花括号括起来的部分构成类的主体。这是一个类的具体实现部分,在其中我们可以定义类的变量和方法。

3.1.2 类成员

类体是一个类的功能部分,由变量和方法两部分组成,两者通称为类成员。

类体的格式如下:

     class className{
         //成员变量
         [public | protected | private ] [static]
         [final] [transient] [volatile] type variableName;
         //成员方法
         [public | protected | private ] [static]
         [final | abstract] [native] [synchronized]
         returnType  methodName ([paramList]) [throws exceptionList]
         {statements}
     }

(1)Java定义了4种访问级别:public、protected、package和private。用来控制其他类对当前类的成员的访问,这种控制对于类变量和方法来说是一致的。当省略了访问控制符时,则为package。

(2)具有static声明的成员属于类本身的成员,而不是类被实例化后的具体对象实例的成员。

(3)final变量用来定义不变的常量,final方法在类继承时不允许子类覆盖。

(4)transient表明类成员变量不应该被序列化,序列化是指把对象按字节流的形式进行存储。

(5)volatile变量阻止编译器对其进行优化处理。

(6)native方法是指Java本地方法,Java本地方法是指利用C或C++等语言实现的方法。

注意:Java语言不支持C++友元方法或友类。

1. 变量

成员变量表示类的静态属性和状态,可以是任何的数据类型,既可以是基本数据类型也可以是复合数据类型。这就是说,一个类的变量可以是其他类的对象。

在Java中,所有的类成员变量都具有初始值。我们可以在定义类的成员变量时,对其进行赋值,如果一个类成员变量在定义时没有指定初始值,则系统会给它赋予默认值(见表3.1)。

表3.1 变量的默认初始化表

类成员变量定义在所有的方法(包括成员方法和构建器)之外,而定义在方法中的变量称为局部变量,另外方法参数也是一种局部变量。所有的局部变量,须经程序赋值才能使用,否则在编译Java程序时将发生变量未初始化的错误。例如:

     public class MyClass {
       //x 为类成员变量
       int x;
       public void print() {
         //y和z 为局部变量
         int y = 0;
         int z;
         //x 值为0
         System.out.println("x = " + x);
         //y 值为0
         System.out.println("y = " + y);
         //非法使用尚未初始化的局部变量z,编译时无法通过
         System.out.println("z = " + z);
       }
     }

2. 方法

Java中所有用户定义的操作均用方法(method)来实现,方法由一组完成某种特定功能可执行的代码组成。在Java类中可以定义方法,在Java的接口中可以声明方法,当一个类要实现某个接口时,就需要实现该接口中声明的方法。

方法包括两种:构建器(constructor)和普通方法。和C++一样,构建器在新建对象时被调用,它没有返回类型。普通方法都有返回类型,如果一个普通方法不需要返回任何值,那么它必须标上返回类型void。

方法的参数表由成对的类型及参数名组成,相互间用逗号分隔。如果方法无参数,则参数表为空。

方法内部定义的局部变量不能与方法的参数同名,否则会产生编译错误,例如:

     public class TestSketch{
         void  test(int i,int j){
             for (int i=0; i<=100; i++){
                 System.out.println(""+i);
             }
         }
     }

编译报错如下:

     TestSketch.java:3: i is already defined in test(int,int)
     for (int i=0; i<=100; i++){
             ^
     1 error

注意:方法体中声明的局部变量的作用域在该方法内部。若局部变量与类的成员变量同名,则类的成员变量被隐藏。

例3.1 VariableDemo.java

     //本例说明局部变量和同名类成员变量的作用域不同
     class Variable{
       //类成员变量
       int a=0;
       int b=0;
       int c=0;
       void test(int a,int b){
         this.a=a;
         this.b=b;
         int c=5; //局部变量
         System.out.println("--print data in method test--");
         System.out.println("a="+a+" b="+b+" c="+c);
       }
     }
     public class VariableDemo{
       public static void main(String args[]){
         Variable v=new Variable();
         System.out.println("--print original data--");
         System.out.println("a="+v.a+" b="+ v.b+" c="+v.c);
         v.test(3,4);
         System.out.println("--print data after method test--");
         System.out.println("a="+v.a+ " b="+ v.b+" c="+v.c);
       }
     }

程序运行结果:

     --print original data--
     a=0 b=0 c=0
     --print data in method test--
     a=3 b=4 c=5
     --print data after method test--
     a=3 b=4 c=0

在C程序中main()作为一个程序的入口方法,在Java中也同样利用这个方法来启动一个Java程序。main()使用一个字符串数组作为参数,它表示启动Java这个程序时的命令行参数,在下面的例子中展现了如何使用main的这个参数。

例3.2 TestMain.java

     public class TestMain {
       public static void main(String[] args) {
         for(int i=0; i < args.length; i++) {
           System.out.println("参数[" + i + "]:" + args[i]);
         }
       }
     }

程序运行结果:

     C:\>java TestMain Hello World
     参数[0]:Hello
     参数[1]:World

命令行参数并不是必需的,但大多数应用都热衷于使用这种方式向程序输入一组参数。需要指出的是,在上例中Hello对应的args索引为0,World对应的args索引为1,如此类推,熟悉C语言的读者会发现其中的不同。

main方法应该声明为static,否则虽然程序编译可以通过,但运行时Java虚拟机将指出main方法不存在。错误信息如下:

     Exception in thread "main" java.lang.NoSuchMethodError: main

方法执行结束,可以向调用者返回一个值,返回值的类型必须匹配方法声明中的返回类型,返回值类型可以是基础数据类型,也可以是一个对象类型。返回值跟随在方法体内的return语句之后,在一个void类型的方法中也可以包含return语句,不过此时return语句后不能跟随变量,它只是表示方法已执行完毕。在一个方法体内可能同时包含多个return语句,在程序运行中不管遇到哪个return语句,都表示方法执行结束,并返回方法的调用者。

下面是另一个可供参考的例子:

     public class MessageQueue{
       private Vector queue;
       //构建器不能有返回值
       public MessageQueue(){
         ...
       }
       //无返回值
       public void add(Message m){
         queue.add(m);
       }
       //返回boolean类型
       //与C语言不同,对于返回boolean类型的方法,不能返回一个整数
       public boolean isEmpty(){
         if(queue.size()==0){
           return true;
         }
         return false;
       }
       //返回一个对象
       //对于对象类型的返回值,可以返回一个null
       public Message get(){
         if(isEmpty()){
           return null;
         }
         Message first =(Message)queue.elementAt(0);
         queue.remove(0);
         return first;
       }
     }

3. 构建器

当一个类实例化时,调用的第一个方法就是构建器(也有的资料将constructor翻译为构造方法或构造函数,它们的含义都是相同的)。构建器是方法的一种,但它和一般方法有着不同的使命。构建器(constructor)是提供对象初始化的专用方法。它和类的名字相同,但没有任何返回类型,甚至不能为void类型。

构建器在对象创建时被自动调用,它不能被显式地调用。

Java中的每个类都有构建器,用来初始化该类的对象。如果在定义Java类时没有创建任何构建器,Java编译器自动添加一个默认的构建器。例如:

     class Point{
         double x,y;
     }

等同于:

     class Point{
         double x,y;
         Point(){ //默认构建器
         }
     }

在类中,可以通过方法的重载来提供多个构建器。

例3.3 Point.java

     class Point{
       double x,y;
       //构建器的重载
       Point(){
         x=0;
         y=0;
       }
       Point(double x){
         this.x=x;
         y=0;
       }
       Point(double x, double y){
         this.x=x;
         this.y=y;
       }
       Point(double x, double y, double z){
         this.x=x;
         this.y=y+z;
       }
     }

我们还可以在构建器中利用this关键字调用类中的其他构建器,需要注意的是,利用this来调用类中的其他构建器时,必须放在代码第一行。例如上例中的不带任何参数的构建器Point()可以改写为:

     Point(){
         this(0, 0);
     }

在调用类的构建器创建一个对象时,当前类的构建器会自动调用父类的默认构建器,也可以在构建器中利用super关键字指定调用父类的特定构建器。一般方法可以被子类继承,而构建器却不可被继承。与this关键字一样,super调用也必须放在代码第一行。例如:

     class StylePoint extends Point{
       int style;
       StylePoint(double x, double y, int style) {
         super(x, y);
         this.style = style;
       }
     }

注意:构建器只能由new运算符调用。

new运算符除了分配存储空间之外,还初始化实例变量,调用实例的构建器。下面给出一个分配空间并初始化Point的实例。

     Point a;             //声明
     a = new Point( );    //初始化并分配存储空间

下面的构建器是带有参数的例子:

     Point b;                     //声明
     b = new Point(3.4, 2.8);     //初始化实际变量

对象的声明并不为对象分配内存空间,而只是分配一个引用空间;对象的引用类似于指针,是32位的地址空间,它的值指向一个中间的数据结构,它存储有关数据类型的信息以及当前对象所在堆的地址,而对于对象所在的实际的内存地址是不可操作的,这就保证了安全性。

注意:类是用来定义对象状态和行为的模板,对象是类的实例。类的所有实例都分配在可作为无用单元回收的堆中。声明一个对象引用并不会为该对象分配存储空间,程序员必须显式地为对象分配存储空间,但不必显式地删除存储空间,因为无用单元回收器会自动回收无用的内存。

3.1.3 关键字this

前面已经多次用到了关键字this,在一个方法内部如果局部变量与类变量的名字相同,则局部变量隐藏类变量,在这种情况下,如果要访问类变量,我们必须使用关键字this。

在类的构建器和非静态方法内,this代表当前对象的引用。利用关键字this,可以在构建器和非静态方法内,引用当前对象的任何成员。

注意:this用在方法中,表示引用当前对象。

其实在Java程序设计中,一个方法引用它自己的实例变量及其他实例方法时,在每个引用的前面都隐含着this。例如:

      class  Test{
        int a,b,c;
        …
        void myPrint( ) {
          print(a+ "\n");  //等价于 print(this.a+ "\n");
        }
        …
     }

一个对象要把自己作为参数传给另一个对象时,就需要用到this。例如:

     class MyClass {
        void method(OtherClass obj){
          …
          obj.method(this)
          …
        }
     }

构建器用来创建和初始化一个对象,通过构建器可以传入多个参数用来初始化类的成员变量,如果对应的构建器参数和类的成员变量具有相同的名字,则不失为一种良好的编程风格,可以使得程序一目了然,此时就需要使用this来引用类成员变量。例如:

     class Moose {
        String hairDresser;
        Moose(String hairDresser) {
          this.hairDresser = hairDresser;
        }
     }

在此例中,类Moose的构建器的参数hairDresser和类成员变量具有同样的名字。具有this前缀的变量hairDresser指向类成员,而不具有this前缀的变量hairDresser指向构建器的参数。

另外,在构建器中,我们还可以利用0个或多个参数的this()方法,调用该类的其他构建器,这种方法称为显式构建器调用。

除了this之外,super关键字可用于访问超类中被隐藏的变量和被改写的方法。关于super的细节将在3.2.2小节讨论。

3.1.4 方法重载

方法重载(overload)是指多个方法具有相同的名字,但是这些方法的参数必须不同(或者是参数的个数不同,或者是参数类型不同)。

方法在同一个类的内部重载,类中方法声明的顺序并不重要。

注意:返回类型不能用来区分重载的方法。方法重载时,参数类型的区分度一定要足够,例如不能是同一类型的参数。重载的认定是指要决定调用的是哪一个方法,在认定重载的方法时,不考虑返回类型。

例3.4 OverloadDemo.java

     public class OverloadDemo{
       //方法1
       public void print(int a){
         System.out.println("一个参数:a="+a);
       }
       //方法2
       public void print(int a, int b) {
         System.out.println("两个参数:a="+a+" b="+b);
       }
       public static void main(String args[]) {
         OverloadDemo oe=new OverloadDemo();
         oe.print(100);
         oe.print(100,200);
       }
     }

程序运行结果:

     一个参数:a=100
     两个参数:a=100 b=200

注意:方法重载时,编译器会根据参数的个数和类型来决定当前所使用的方法。

通过参数个数来区分方法重载,还是比较容易分辨的;而通过参数类型来区分方法重载,就略显复杂了,有时要格外小心,避免出现“二义性”。例如我们看看如下代码:

     public class OverloadDemo2 {
        //方法1
        public void print(int a, long b, long c) {
            ...
        }
        //方法2
        public void print(long a, long b, long c) {
            ...
        }
     }

当我们调用print(1, 2L, 3L)时,方法1将被调用;当我们调用print(1L, 2L, 3L)时,方法2将被调用。当我们调用print(1, 2, 3)时,哪一个方法将被调用?答案是方法1。因为方法1的第一个参数为int型,与print(1, 2, 3)的第1个参数正好匹配,虽然第2和第3个参数类型并不相同,但相对于方法2来说,方法1与print(1, 2, 3)调用更为接近,所以方法1将被调用。

现在我们对OverloadDemo2的方法2略做修改,如下所示:

     public class OverloadDemo3 {
        //方法1
        public void print(int a, long b, long c) {
            ...
        }
        //方法2
        public void print(long a, int b, long c) {
            ...
        }
     }

当我们再次调用print(1, 2, 3)时,将会怎样?方法1和方法2与调用print(1, 2, 3)不存在哪个更接近,这就产生了一种模棱两可的情形。那么参数顺序是否会产生影响呢?不会!如果利用参数顺序来解决这个问题,必将给程序编写带来极大的潜在危险。

对于这种二义性的情形,程序编译将不能通过,Java编译器将会指出具有二义性的方法调用。

3.1.5 类继承

在构造一个新的类时,首先找到一个已有的类,新类在这个已有类的基础上构造,这种特性我们称为继承,也可以称作派生(derived)。继承使用关键字extends声明。继承出的类称为原来类的子类,而原来类被称为父类或者超类。

注意:类的继承具有传递性:如果B是A的子类,C是B的子类,则C是A的子类。

下面是一个点类继承的例子:

     //在平面直角坐标系中的点类
     public class Point{
         float x,y;
         ...
     }
     //可打印的点类
     class PintablePoint extends Point implements Printable{
         ...
         public void Print( ) {
         }
     }

关键字extends只能引出一个超类superClassName,即Java语言仅支持单继承(single inheritance)。

Java程序运行时建立的每个对象都具有Object类定义的数据和功能。例如,每个对象可调用Object类定义的实例方法equals和toString。方法equals用于比较两个对象是否相等,方法toString用于将对象转换成字符串的描述形式。

所有的类均从一个根类Object中派生出来。除Object之外的任何类都有一个直接超类。如果一个类在声明时未指明其直接超类,那么默认即为Object。

例如:

     class Point {
         float x,y;
     }

与下面写法等价:

     class Point extends Object {
         float  x,y;
     }

继承关系使得Java中所有的类构成一棵树,Object类就是这棵树的根。

注意:Java语言之所以没有采用C++的多继承机制,是为了避免多继承带来的诸多不便,比如:二义性的产生、编译器更加复杂、程序难以优化等问题。Java语言虽然仅支持单继承,但是可以通过接口机制来支持某些在其他语言中用多继承实现的机制(详见第4章)。

3.1.6 类的初始化过程

当创建一个对象时,对象的各个变量根据其类型被设置为相应的默认初始值(详细设置见3.1.2小节),然后调用构建器。每个构建器有三个执行阶段。

(1)调用超类的构建器。

(2)由初始化语句对各变量进行初始化。

(3)执行构建器的体。

下面是一个类的初始化过程的例子:

     class Mask{
       int rightMask = 0x00ff;
       int fullMask;
       public Mask(){
         fullMask = rightMask;
       }
       public int init(int orig){
         return (orig&fullMask);
       }
     }
     class EnhancedMask extends Mask{
       protected int leftMask = 0xff00;
       public EnhancedMask(){
         fullMask |= leftMask;
       }
     }

若创建一个类型为EnhancedMask的对象,逐步完成构造,表3.2所示是每一步结束后各个变量的值。

表3.2 EnhancedMask对象的初始化过程表

在构建器中调用其他方法时,应密切注意程序代码的执行次序和当前成员变量的赋值变化,因为此时对象尚未创建完成,有些变量还没有初始化。上面第5步中,如果在Mask构建器最后调用了方法init(),fullMask的值此时为0x00ff,而不是0xffff;只有当EnhancedMask对象构造完成之后,fullMask的值才为0xffff。

另外,我们还必须注意有关多态性的问题,参见3.2.3小节。假如我们在类EnhancedMask中重写了init()方法,如果我们在Mask构建器中调用了init()方法,那么当我们创建EnhancedMask的对象时,它实际调用的是EnhancedMask的init()方法,而不是Mask的init()方法。

注意:若在构造对象阶段需要调用方法,那么在这些方法设计时,必须考虑以上因素。而且,对于构建器所调用的每个非终结(non-final)方法,因为能被改写,所以对它们应该仔细编制文档,以告示他人:他们可能想要改写具有潜在限制的构建器。

3.1.7 源文件

源文件是我们开发程序的基本单位,Java源文件是扩展名为.java的纯文本文件。Java编译器处理编译Java源文件,输出为Java字节码文件,即扩展名为.class的文件。

在一个Java源文件中只允许定义0个或1个public类或接口,但可以同时有不受限制的多个default类和接口。如果源文件包含了public类或接口,则文件名必须和public类或接口一样;如果源文件中不包含public类或接口,文件名可以是任意合法的文件名。

一个Java源文件的内容通常由如下3个功能部分构成。

(1)package包声明:命名当前包。

(2)import包引入:引入其他程序包。

(3)类和接口定义:定义新的类和接口。

下面的例子是一个典型的Java源文件的组成格式:

     //包声明
     package  com.mycompany.myproject;
     //引入其他程序包或类
     import java.io.*;
     import java.util.Vector;
     //类定义
     public class MyClass{
       ...
     }
     interface MyInterface{
       ...
     }

不同的程序员在组织源程序时有自己的爱好:有的喜欢在一个源文件中只存放一个类或是接口;而有的喜欢在同一个源文件中存放多个类或是接口。在例3.1中,源文件VariableDemo.java中同时存放了两个类Variable和VariableDemo。也可以将这两个类分别存放到不同的文件中。例如,将类Variable存放在文件Variable.java中,将类VariableDemo存放在文件VariableDemo.java中。使用这种方式时,可以有两种方法进行编译。一种方法是使用通配符:

     javac Variable*.java

这样,所有以Variable开头的并以.java结尾的源程序都得到编译。

另一种方法是直接输入:

     javac VariableDemo.java

虽然我们并没有显式地指定对Variable.java进行编译,但是编译器在对VariableDemo.java进行编译时,发现其中使用了类Variable,因此会主动寻找Variable.class,如果没有能够找到,则继续寻找Variable.java并编译之。此外,即使Variable.class已经存在,如果Variable.java的时间戳比Variable.class的时间戳新,仍旧会对Variable.java进行编译。