Skip to content

3. 类与接口

3.1. 通过实例讲解该对象

3.1.1. 概述

接下来,我们将通过实例来探索面向对象编程。对象是一个实体,它包含定义其状态的数据(称为属性)和函数(称为方法)。对象是基于一个称为类的模板创建的:

public class C1{
    type1 p1;            // property p1
    type2 p2;            // property p2
    
    type3 m3(){        // m3 method
        
    }
    type4 m4(){        // m4 method
        
    }
    
}

基于前面的类 C1,我们可以创建多个对象 O1、O2、…… 它们都将拥有属性 p1、p2、…… 以及方法 m3、m4、…… 它们的属性 pi 将具有不同的值,因此每个对象都有其自身的状态。

如果 O1 是类型 C1 的对象,那么 O1.p1 指代 O1 的属性 p1,而 O1.m1 指代 O1 的方法 m1

让我们考虑一个最初的对象模型:Person类。

3.1.2. Person类的定义

Person 类的定义如下:


import java.io.*;
 
public class personne{
  // attributes
  private String prenom;
  private String nom;
  private int age;
  
  // method
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }
 
  // method
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

这里是一个类的定义,类是一种数据类型。当我们创建该类型的变量时,称其为对象。因此,类是构建对象的模板。

类的成员字段可以是数据或方法(函数)。这些字段可以具有以下三种属性之一:

private私有字段仅可被类的内部方法访问

public:公共字段可被任何函数访问,无论该函数是否定义在该类内部

protected保护字段仅可被类的内部方法或派生对象访问(参见后文的继承概念)。

通常,类的数据被声明为 private,而其方法被声明为 public。这意味着对象的使用者(程序员)

a: 无法直接访问对象的私有数据

b: 能够调用对象的 public 方法,包括那些提供对其私有数据访问权限的方法。

声明对象的语法如下:


public class nomClasse{
    private  donnée ou méthode privée
    public  donnée ou méthode publique
    protected  donnée ou méthode protégée
}

注释

  • 私有、受保护和公有属性的声明顺序是任意的

3.1.3. initialize 方法

让我们回到我们声明的 Person 类:


import java.io.*;
 
public class personne{
  // attributes
  private String prenom;
  private String nom;
  private int age;
  
  // method
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }
 
  // method
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

initialize 方法的作用是什么?由于 lastName、firstNameage 是 Person 类的私有成员,因此以下语句

personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

是无效的。我们必须使用公共方法来初始化 Person 类型的对象。这就是 initialize 方法的作用。我们写:

personne p1;
p1.initialise("Jean","Dupont",30);

语法 p1.initialize 是有效的,因为 initialize 是 public 的。

3.1.4. new 运算符

指令序列

personne p1;
p1.initialise("Jean","Dupont",30);

是错误的。该陈述

    personne p1;

声明 p1person 类型对象的引用。该对象尚未存在,因此 p1 未被初始化。这相当于我们写了:

personne p1=null;

其中我们通过关键字 null 明确表示变量 p1 尚未引用任何对象。

随后当我们编写

p1.initialise("Jean","Dupont",30);

时,我们正在调用由 p1 引用的对象的 initialize 方法。然而,该对象尚未存在,编译器会报错。要让 p1 引用一个对象,我们必须写:

personne p1=new personne();

这会创建一个未初始化的 Person 对象:namefirst_name 属性(它们是 String 对象的引用)将取值为 null,而 age 将取值为 0。因此存在默认初始化。现在 p1 引用了一个对象,该对象的初始化语句

p1.initialise("Jean","Dupont",30);

是有效的。

3.1.5. 关键字 this

让我们看看 initialize 方法的代码:


public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

语句 this.firstName = P 表示将值 P 赋给当前对象(this)的 firstName 属性。关键字 this 指代当前对象:即正在执行该方法的对象。我们如何知道这一点?让我们看看在调用程序中,由 p1 引用的对象是如何初始化的:

p1.initialise("Jean","Dupont",30);

这里调用的是 p1 对象的 initialize 方法。当我们在该方法内部引用 this 对象时,实际上引用的是 p1 对象。initialize 方法也可以写成如下形式:


public void initialise(String P, String N, int age){
   prenom=P;
   nom=N;
   this.age=age;
  }

当对象的方法引用该对象的属性 A 时,默认使用 this.A 这种写法。当标识符发生冲突时,必须显式使用 this.A。以下语句即属于这种情况:


this.age=age;

在此,age 既指当前对象的属性,也指方法接收的 age 参数。此时必须通过将 age 属性写为 this.age 来消除歧义。

3.1.6. 一个测试程序

以下是一个测试程序:


public class test1{
  public static void main(String arg[]){
    personne p1=new personne();
    p1.initialise("Jean","Dupont",30);
    p1.identifie();
  }
}

Person 类在源文件 person.java 中定义,并已编译:

E:\data\serge\JAVA\BASES\OBJETS\2>javac personne.java

E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002  09:21                  473 personne.java
10/06/2002  09:22                  835 personne.class
10/06/2002  09:23                  165 test1.java

对于测试程序,我们也进行同样的操作:

E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java

E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002  09:21                  473 personne.java
10/06/2002  09:22                  835 personne.class
10/06/2002  09:23                  165 test1.java
10/06/2002  09:25                  418 test1.class

test1.java 程序没有使用以下语句导入 person 类,这可能令人感到惊讶:

import personne;

当编译器在源代码中遇到未在同一源文件中定义的类引用时,它会在多个位置搜索该类:

  • 通过 import 语句导入的包中
  • 在编译器启动所在的目录中

在我们的示例中,编译器是从包含 personne.class 文件的目录中启动的,这解释了为何它能找到 personne 类的定义。在此情况下,添加 import 语句会导致编译错误:

E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
test1.java:1: '.' expected
import personne;
               ^
1 error

为避免此错误并确保 Person 类被导入,今后我们将在程序开头写入以下内容:

// classes importées
// import personne;

现在我们可以运行 test1.class 文件:

E:\data\serge\JAVA\BASES\OBJETS\2>java test1
Jean,Dupont,30

可以将多个类合并到一个源文件中。让我们将 person*test1 这两个类合并到源文件 test2.java* 中。将 test1 类重命名为 test2,以反映源文件名称的变化:

// imported packages
import java.io.*;

class personne{
   // attributes
  private String prenom;    // first name
  private String nom;            // its name
  private int age;                // his age

   // method
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }//initialize

   // method
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }//identifies
}//class
public class test2{
  public static void main(String arg[]){
    personne p1=new personne();
    p1.initialise("Jean","Dupont",30);
    p1.identifie();
  }
}

请注意,Person 类不再具有 public 属性。实际上,在一个 Java 源文件中,只有一个类可以具有 public 属性。这个类必须包含 main 方法。此外,源文件的名称必须与该类同名。现在,让我们编译 test2.java 文件:

E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002  09:36                  633 test2.java

E:\data\serge\JAVA\BASES\OBJETS\3>javac test2.java

E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002  09:36                  633 test2.java
10/06/2002  09:41                  832 personne.class
10/06/2002  09:41                  418 test2.class

请注意,源文件中的每个类都生成了一个 .class 文件。现在让我们运行 test2.class 文件:

E:\data\serge\JAVA\BASES\OBJETS\2>java test2
Jean,Dupont,30

从现在起,我们将交替使用这两种方法:

  • 将类集中到单个源文件中
  • 每个源文件包含一个类

3.1.7. 另一种方法是初始化

让我们继续使用 Person 类,并向其中添加以下方法:


public void initialise(personne P){
    prenom=P.prenom;
    nom=P.nom;
    this.age=P.age;
  }

现在我们有两个名为 *initialize* 的方法:只要它们的参数不同,这是允许的。本例中正是如此。 参数现在是一个指向人(person)的引用 P。随后,人 P 的属性被赋值给当前对象(this)。请注意,尽管这些属性是私有的,但 initialize 方法仍可直接访问对象 P 的属性。这一点始终成立:类 C 的对象 O1 的方法总能访问同一类 C 中其他对象的私有属性。

以下是对新 Person 类的测试:


// import nobody;
import java.io.*;
 
public class test1{
  public static void main(String arg[]){
    personne p1=new personne();
    p1.initialise("Jean","Dupont",30);
    System.out.print("p1=");
    p1.identifie();
    personne p2=new personne();
    p2.initialise(p1);
    System.out.print("p2=");
    p2.identifie();
  }
}

及其结果:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.8. Person类的构造函数

构造函数是一种以类名命名的方法,在创建对象时被调用。它通常用于初始化对象。这是一种可以接受参数但不返回结果的方法。其原型或定义前不带任何类型(甚至不带 void)。

如果一个类有一个接受 n 个参数 args 的构造函数,则该类的对象声明和初始化可以如下进行:


        classe objet =new classe(arg1,arg2, ... argn);

或者


        classe objet;

        objet=new classe(arg1,arg2, ... argn);

当一个类拥有一个或多个构造函数时,必须使用其中一个构造函数来创建该类的对象。如果类 C 没有构造函数,则它拥有一个默认构造函数,即无参数的构造函数:public C()。此时,对象的属性将使用默认值进行初始化。这就是我们在前面的程序中编写以下代码时发生的情况:

    personne p1;
    p1=new personne();

现在,让我们为 Person 类创建两个构造函数:


public class personne{
  // attributes
  private String prenom;
  private String nom;
  private int age;
  
  // manufacturers
  public personne(String P, String N, int age){
    initialise(P,N,age);
  }
  public personne(personne P){
    initialise(P);
  }
 
  // method
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }
  public void initialise(personne P){
    this.prenom=P.prenom;
    this.nom=P.nom;
    this.age=P.age;
  }
 
  // method
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

我们的两个构造函数只是调用了相应的 initialize 方法。请注意,当我们在构造函数中看到 initialize(P) 这样的写法时,编译器会将其转换为 this.initialize(P)。因此,在构造函数中,initialize 方法会被调用以对由 this 引用的对象进行操作,即当前对象,也就是正在被构造的对象。

以下是一个测试程序:


// import nobody;
import java.io.*;
 
public class test1{
  public static void main(String arg[]){
    personne p1=new personne("Jean","Dupont",30);
    System.out.print("p1=");
    p1.identifie();
    personne p2=new personne(p1);
    System.out.print("p2=");
    p2.identifie();
  }
}

以及所得结果:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.9. 对象引用

我们仍在使用相同的 Person 类。测试程序如下:


// import nobody;
import java.io.*;
 
public class test1{
  public static void main(String arg[]){
    // p1
    personne p1=new personne("Jean","Dupont",30);
    System.out.print("p1=");  p1.identifie();
    // p2 references the same object as p1
    personne p2=p1;
    System.out.print("p2="); p2.identifie();
    // p3 references an object that will be a copy of the object referenced by p1
    personne p3=new personne(p1);
    System.out.print("p3=");  p3.identifie();
    // change the state of the object referenced by p1
    p1.initialise("Micheline","Benoît",67);
    System.out.print("p1=");  p1.identifie();
    // as p2=p1, the object referenced by p2 must have changed state
    System.out.print("p2=");  p2.identifie();
    // as p3 does not reference the same object as p1, the object referenced by p3 must not have changed
    System.out.print("p3=");  p3.identifie();
  }
}

结果如下:

p1=Jean,Dupont,30
p2=Jean,Dupont,30
p3=Jean,Dupont,30
p1=Micheline,Benoît,67
p2=Micheline,Benoît,67
p3=Jean,Dupont,30

在使用以下方式声明变量 p1

personne p1=new personne("Jean","Dupont",30);

p1 引用了对象 personne("Jean","Dupont",30),但它本身并不是该对象。在 C 语言中,我们会说它是一个指针,即所创建对象的地址。如果接着写:

    p1=null

被修改的并非对象 person("Jean","Dupont",30) 本身,而是引用 p1 改变了其值。如果对象 person("Jean","Dupont",30) 不再被任何其他变量引用,它将会“消失”。

当我们写:

personne p2=p1;

我们初始化了指针 p2:它“指向”(即引用)与指针 p1 相同的对象。因此,如果修改了 p1 “指向”(或引用的)对象,也就修改了 p2 引用的对象。

当我们写:

personne p3=new personne(p1);

会创建一个新对象,它是 p1 所引用的对象的副本。这个新对象将由 p3 引用。如果你修改了 p1 “指向”(或引用的)对象,则不会以任何方式修改 p3 所引用的对象。结果正是如此。

3.1.10. 临时对象

在表达式中,你可以显式调用对象的构造函数:对象会被创建,但你无法访问它(例如对其进行修改)。这个临时对象是为了求值而创建的,求值完成后即被丢弃。它占用的内存空间随后将由一个名为“垃圾回收器”的程序自动回收,该程序的作用是回收那些不再被程序数据引用的对象所占用的内存空间。

请看以下示例:


// import nobody;
 
public class test1{
  public static void main(String arg[]){
    new personne(new personne("Jean","Dupont",30)).identifie();
  }
}

现在我们修改 Person 类的构造函数,使其显示一条消息:


// manufacturers
  public personne(String P, String N, int age){
    System.out.println("Constructeur personne(String, String, int)");
    initialise(P,N,age);
  }
  public personne(personne P){
    System.out.println("Constructeur personne(personne)");
    initialise(P);
  }

我们得到以下结果:

Constructeur personne(String, String, int)
Constructeur personne(personne)
Jean,Dupont,30

展示了两个临时对象的连续构造过程。

3.1.11. 用于读写私有属性的方法

我们在 Person 类中添加了必要的方法,用于读取或修改对象属性的状态:


public class personne{
  private String prenom;
  private String nom;
  private int age;
  
  public personne(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }
 
  public personne(personne P){
    this.prenom=P.prenom;
    this.nom=P.nom;
    this.age=P.age;
  }
 
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
 
  // accessors
  public String getPrenom(){
    return prenom;
  }
  public String getNom(){
    return nom;
  }
  public int getAge(){
    return age;
  }
 
  //modifiers
  public void setPrenom(String P){
    this.prenom=P;
  }
  public void setNom(String N){
    this.nom=N;
  }
  public void setAge(int age){
    this.age=age;
  }
}

我们使用以下程序测试新类:


// import nobody;
 
public class test1{
  public static void main(String[] arg){
    personne P=new personne("Jean","Michelin",34);
    System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
    P.setAge(56);
    System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
  }
}

我们得到以下结果:

P=(Jean,Michelin,34)
P=(Jean,Michelin,56)

3.1.12. 类方法和属性

假设我们想统计应用程序中创建的 Person 对象的数量。我们可以自己管理一个计数器,但可能会遗漏那些零散创建的临时对象。在 Person 类的构造函数中加入一条增量计数器的指令似乎更稳妥。问题在于如何传递该计数器的引用以便构造函数能对其进行增量操作:我们需要向构造函数传递一个新的参数。 我们也可以将计数器包含在类定义中。由于它是类本身的属性,而非该类某个特定对象的属性,因此我们需要使用 static 关键字以不同的方式进行声明:

    private static long nbPersonnes;        // number of people created

要引用它,我们写成 person.nbPeople,以表明它是 Person 类本身的属性。这里,我们创建了一个私有属性,无法从类外部直接访问。 因此,我们创建了一个公共方法来提供对类属性 nbPersonnes 的访问。为了返回 nbPersonnes 的值,该方法不需要特定的对象:事实上,nbPersonnes 并非某个特定对象的属性,而是整个类的属性。因此,我们需要一个也被声明为 static 的类方法:

public static long getNbPersonnes(){
    return nbPersonnes;
}

该方法将通过 person.getNbPeople() 这种语法从外部调用。以下是一个示例。

Person 类变为如下形式:


public class personne{
  
  // class attribute
  private static long nbPersonnes=0;
 
  // object attributes

 
  // manufacturers
  public personne(String P, String N, int age){
    initialise(P,N,age);
    nbPersonnes++;
  }
  public personne(personne P){
    initialise(P);
    nbPersonnes++;
  }
 
  // method

 
  // class method
  public static long getNbPersonnes(){
    return nbPersonnes;
  }
 
}// class

使用以下程序:


// import nobody;
 
public class test1{
  public static void main(String arg[]){
    personne p1=new personne("Jean","Dupont",30);
    personne p2=new personne(p1);
    new personne(p1);
    System.out.println("Nombre de personnes créées : "+personne.getNbPersonnes());
  }// hand
}//test1

我们得到以下结果:

    Nombre de personnes créées : 3

3.1.13. 将对象传递给函数

我们已经提到,Java 通过值传递传递实际函数参数:实际参数的值会被复制到形式参数中。因此,函数无法修改实际参数。

对于对象而言,切勿被人们惯常的语言误用所误导——即系统地将“对象”与“对象引用”混为一谈。 对象只能通过对其的引用(指针)进行操作。因此,传递给函数的并非对象本身,而是对该对象的引用。于是,被复制到形式参数中的是引用的值——而非对象本身的值——因此不会创建新的对象。

如果将对象引用 R1 传递给函数,它将被复制到对应的形式参数 R2 中。因此,引用 R2 和 R1 指向同一个对象。如果函数修改了 R2 所指向的对象,显然也会修改 R1 所引用的对象,因为它们是同一个对象。

Image

以下示例说明了这一点:


// import nobody;
 
public class test1{
  public static void main(String arg[]){
    personne p1=new personne("Jean","Dupont",30);
    System.out.print("Paramètre effectif avant modification : ");
    p1.identifie();
    modifie(p1);
    System.out.print("Paramètre effectif après modification : ");
    p1.identifie();
  }// hand
 
  private static void modifie(personne P){
    System.out.print("Paramètre formel avant modification : ");
    P.identifie();
    P.initialise("Sylvie","Vartan",52);
    System.out.print("Paramètre formel après modification : ");
    P.identifie();
  }// modify
}// class

modify 方法被声明为静态方法,因为它是一个类方法:调用时无需在方法名前添加对象。所得结果如下:

Constructeur personne(String, String, int)
Paramètre effectif avant modification : Jean,Dupont,30
Paramètre formel avant modification : Jean,Dupont,30
Paramètre formel après modification : Sylvie,Vartan,52
Paramètre effectif après modification : Sylvie,Vartan,52

我们可以看到,仅构造了一个对象:即主函数中的人 p1,且该对象确实已被 modify 函数修改。

3.1.14. 将函数的输出参数封装在对象中

由于参数按值传递,因此无法编写具有 int 类型输出参数的 Java 函数,例如,因为我们无法传递非对象的 int 类型的引用。因此,我们可以创建一个封装 int 类型的类:


public class entieres{
  private int valeur;
 
  public entieres(int valeur){
    this.valeur=valeur;
  }
 
  public void setValue(int valeur){
    this.valeur=valeur;
  }
 
  public int getValue(){
    return valeur;
  }
}

上述类包含一个用于初始化整数的构造函数,以及两个用于读取和修改该整数值的方法。我们使用以下程序对该类进行测试:


// import integer;
 
public class test2{
  public static void main(String[] arg){
    entieres I=new entieres(12);
    System.out.println("I="+I.getValue());
    change(I);
    System.out.println("I="+I.getValue());
  }
  private static void change(entieres entier){
    entier.setValue(15);
  }
}

并得到以下结果:

I=12
I=15

3.1.15. 一群人

对象与其他数据一样,是一种数据,因此可以将多个对象组合到数组中:


// import nobody;
 
public class test1{
  public static void main(String arg[]){
    personne[] amis=new personne[3];
    System.out.println("----------------");
    amis[0]=new personne("Jean","Dupont",30);
    amis[1]=new personne("Sylvie","Vartan",52);
    amis[2]=new personne("Neil","Armstrong",66);
    int i;
    for(i=0;i<amis.length;i++)
      amis[i].identifie();
  }
}

语句 person[] friends = new person[3]; 创建了一个包含 3 个 person 类型元素的数组。这 3 个元素在此处被初始化为 null 值,这意味着它们不引用任何对象。同样,从技术意义上讲,我们所说的“对象数组”实际上只是一个对象引用的数组。 对象数组的创建——即数组本身也是一个对象(如 new 的使用所示)——并不会自动创建其元素类型对应的任何对象:这必须在后续操作中完成。

得到以下结果:

----------------
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Jean,Dupont,30
Sylvie,Vartan,52
Neil,Armstrong,66

3.2. 通过示例理解继承

3.2.1. 概述

这里我们将讨论继承的概念。继承的目的是“定制”现有的类,使其满足我们的需求。假设我们要创建一个 Teacher 类:教师是一种特定类型的人。他们拥有其他人所没有的属性,例如他们所教授的科目。但他们也拥有任何人都有的属性:名字、姓氏和年龄。 因此,教师是 Person 类的完全成员,但拥有额外的属性。与其从头编写 Teacher 类,我们更倾向于基于现有的 Person 类进行构建,并将其适应教师的特定特征。正是继承的概念使我们能够做到这一点。

为了表示 Teacher 类继承了 Person 类的属性,我们会这样编写:


    public class enseignant extends personne

Person 被称为父类(或基类),而 Teacher 是派生类(或子类)。一个 Teacher 对象具有 Person 对象的所有特性:它拥有相同的属性和方法。父类的这些属性和方法在子类的定义中不会重复;我们只需指定子类新增的属性和方法:


class enseignant extends personne{
// attributes
  private int section;
 
// manufacturer
  public enseignant(String P, String N, int age,int section){
    super(P,N,age);
    this.section=section;
  }
}

我们假设 Person 类定义如下:


public class personne{
  private String prenom;
  private String nom;
  private int age;
  
  public personne(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }
 
  public personne(personne P){
    this.prenom=P.prenom;
    this.nom=P.nom;
    this.age=P.age;
  }
 
  public String identite(){
    return "personne("+prenom+","+nom+","+age+")";
  }
 
  // accessors
  public String getPrenom(){
    return prenom;
  }
  public String getNom(){
    return nom;
  }
  public int getAge(){
    return age;
  }
 
  //modifiers
  public void setPrenom(String P){
    this.prenom=P;
  }
  public void setNom(String N){
    this.nom=N;
  }
  public void setAge(int age){
    this.age=age;
  }
}

identifiant 方法已稍作修改,用于返回一个标识该人的字符串,现更名为 identite在此,Teacher 类在 Person 类的方法和属性基础上进行了扩展:

  • 一个 `section` 属性,表示该教师在教师团队中所属的教学组(通常每门学科对应一个教学组)
  • 一个新的构造函数,用于初始化教师的所有属性

3.2.2. 创建 Teacher 对象

Teacher 类的构造函数如下:


// constructeur
  public enseignant(String P, String N, int age,int section){
    super(P,N,age);
    this.section=section;
  }

语句 super(P, N, age) 是对父类构造函数的调用,在此处即 Person 类。我们知道,该构造函数会初始化 Student 对象内部所包含的 Person 对象的 first_name、last_nameage 字段。这看起来相当复杂,我们可能更倾向于这样写:


// constructeur
  public enseignant(String P, String N, int age,int section){
    this.prenom=P;
        this.nom=N
        this.age=age
      this.section=section;
  }

这是不可能的。Person 类已将其三个字段——first_namelast_name 和 age——声明为 private。只有同类的对象才能直接访问这些字段。所有其他对象(包括本例中的子类对象)都必须使用 public 方法来访问它们。如果 Person 类将这三个字段声明为 protected,情况就会不同:那样的话,派生类就可以直接访问这三个字段了。 因此,在本例中,使用父类的构造函数才是正确的解决方案,这也是标准做法:在构造子类对象时,我们首先调用父类的构造函数,然后完成子类对象特有的初始化(在本例中即 section 字段)。

让我们尝试编写第一个程序:


// import nobody;
// import teacher;
 
public class test1{
  public static void main(String arg[]){
    System.out.println(new enseignant("Jean","Dupont",30,27).identite());
  }
}

该程序仅创建了一个 Teacher 对象(new)并获取其标识。Teacher 类本身没有 identity 方法,但其父类拥有该方法,且该方法为 public 类型:通过继承,它便成为了 Teacher 类的 public 方法。

将类的源文件放置在同一目录下,然后进行编译:

E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002  10:00                  765 personne.java
10/06/2002  10:00                  212 enseignant.java
10/06/2002  10:01                  192 test1.java

E:\data\serge\JAVA\BASES\OBJETS\4>javac *.java

E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002  10:00                  765 personne.java
10/06/2002  10:00                  212 enseignant.java
10/06/2002  10:01                  192 test1.java
10/06/2002  10:02                  316 enseignant.class
10/06/2002  10:02                1 146 personne.class
10/06/2002  10:02                  550 test1.class

执行 test1.class 文件:

E:\data\serge\JAVA\BASES\OBJETS\4>java test1
personne(Jean,Dupont,30)

3.2.3. 方法重载

在上一个示例中,我们已将人员的身份信息作为教师的一部分,但缺少一些特定于 Teacher 类的信息(如所属部门)。因此,我们需要编写一个方法来识别教师:


class enseignant extends personne{
  int section;
 
  public enseignant(String P, String N, int age,int section){
    super(P,N,age);
    this.section=section;
  }
 
  public String identite(){
    return "enseignant("+super.identite()+","+section+")";    
  }    
}

Teacher 类的 identity 方法依赖于其父类的 identity 方法(super.identity)来显示其“person”部分,然后添加了 Teacher 类特有的 section 字段。

现在 Teacher 类拥有两个 identity 方法:

  • 一个是从父类 Person 继承而来的
  • 以及它自己的

如果 E 是一个 Teacher 对象,则 E.identity 指代 Teacher 类的 identity 方法。我们说父类的 identity 方法被子类的 identity 方法“重写”了。一般而言,如果 O 是对象,M 是方法,要执行方法 O.M,系统将按以下顺序查找方法 M

  • 在对象 O 的类中
  • 在其父类中(如果存在父类)
  • 在其父类的父类中(如果存在)
  • 以此类推……

因此,继承允许子类重写父类中同名的方法。正是这一点使得子类能够根据自身需求进行调整。结合稍后将要讨论的多态性,方法重写是继承的主要优势。

让我们考虑与之前相同的示例:


// import nobody;
// import teacher;
 
public class test1{
  public static void main(String arg[]){
    System.out.println(new enseignant("Jean","Dupont",30,27).identite());
  }
}

此次获得的结果如下:

    enseignant(personne(Jean,Dupont,30),27)

3.2.4. 多态性

考虑一个类层次结构:C0 C1 C2 … Cn

其中 Ci Cj 表示类 Cj 继承自类 Ci。这意味着类 Cj 不仅具有类 Ci 的所有特征,还具有其他特征。设 Oi 是类型 Ci 的对象。以下写法是有效的:

    Oi=Oj avec j>i

事实上,根据继承关系,类 Cj 不仅具备类 Ci 的所有特征,还拥有额外的特征。因此,类型为 Cj 的对象 Oj 自身包含一个类型为 Ci 的对象。该运算

    Oi=Oj

意味着 Oi 是指向对象 Oj 内部所包含的 Ci 类型对象的引用。

Ci 的变量 Oi 实际上不仅可以指向类 Ci 的对象,还可以指向任何从类 Ci 派生的对象,这一特性被称为多态性:即变量能够指向不同类型的对象。

让我们通过一个示例来考虑以下函数,该函数与任何类都无关:

    public static void affiche(Object obj){
        .
    }

Object 类是所有 Java 类的“父类”。因此,当我们编写:

    public class personne

我们实际上是在隐式地编写:

    public class personne extends Object

因此,每个 Java 对象都包含一个 *Object* 组件。因此,我们可以这样写:

    enseignant e;
    affiche(e);

display 函数中类型为 Object 的形式参数将接收类型为 Teacher 的值。由于 Teacher 继承自 Object,因此这是有效的。

3.2.5. 重载与多态

让我们完善我们的 display 函数:

    public static void affiche(Object obj){
        System.out.println(obj.toString());
    }

obj.toString() 方法返回一个字符串,该字符串以 class_name@object_address 的形式标识 obj 对象。在之前的示例中会发生什么情况:

    enseignant e=new enseignant(...);
    affiche(e);

系统必须执行语句 System.out.println(e.toString()),其中 e 是一个 Teacher 对象。它会从 Teacher 类所在的类层次结构的末端开始,向上搜索 toString 方法:

  • Teacher 类中,未找到 toString() 方法
  • 在父类 Person 中,未找到 toString() 方法
  • 在父类 `Object` 中,它找到了 `toString()` 方法并执行了它

以下程序演示了这一点:


// import nobody;
// import teacher;
 
public class test1{
  public static void main(String arg[]){
    enseignant e=new enseignant("Lucile","Dumas",56,61);
    affiche(e);
    personne p=new personne("Jean","Dupont",30);
    affiche(p);
  }
  public static void affiche(Object obj){
    System.out.println(obj.toString());
  }
}

结果如下:

enseignant@1ee789
personne@1ee770

也就是说,class_name@object_address。由于这种写法不够清晰,我们可能会想为 PersonStudent 类定义一个 toString 方法,以覆盖父类 ObjecttoString 方法。与其编写与 PersonTeacher 类中已有的身份识别方法类似的方法,不如直接将这些身份识别方法重命名为 toString


public class personne{
    ...
 
  public String toString(){
    return "personne("+prenom+","+nom+","+age+")";
  }
 
    ...
}
 
class enseignant extends personne{
  int section;
 

 
  public String toString(){
    return "enseignant("+super.toString()+","+section+")";    
  }    
}

使用与之前相同的测试程序,结果如下:

enseignant(personne(Lucile,Dumas,56),61)
personne(Jean,Dupont,30)

3.3. 内部类

一个类可以包含另一个类的定义。请看以下示例:

// imported classes
import java.io.*;

public class test1{

    // internal class
    private class article{
         // we define the structure
        private String code;
        private String nom;
        private double prix;
        private int stockActuel;
        private int stockMinimum;

     // manufacturer
    public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
         // attribute initialization
      this.code=code;
      this.nom=nom;
      this.prix=prix;
      this.stockActuel=stockActuel;
      this.stockMinimum=stockMinimum;
    }//manufacturer

    //toString
    public String toString(){
        return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
    }//toString
  }//item class

   // local data
  private article art=null;

   // manufacturer
  public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
       // attribute definition
    art=new article(code, nom, prix, stockActuel,stockMinimum);
  }//test1

   // accessor
  public article getArticle(){
      return art;
  }//getArticle

    public static void main(String arg[]){
      // create a test1 instance
      test1 t1=new test1("a100","velo",1000,10,5);
    // display test1.art
    System.out.println("art="+t1.getArticle());
  }//hand

}// fin class

test1 类包含另一个类(article 类)的定义。我们说 article 是 test1 类的内部类。当内部类仅在包含它的类内部需要时,这种做法非常有用。编译上面的 test1.java 源代码时,我们会得到两个 .class 文件:

E:\data\serge\JAVA\classes\interne>dir
05/06/2002  17:26                1 362 test1.java
05/06/2002  17:26                  941 test1$article.class
05/06/2002  17:26                1 020 test1.class

为 article 类生成了一个 test1$article.class 文件,该类是 test1 类的内部类。如果运行上面的程序,我们会得到以下结果:

E:\data\serge\JAVA\classes\interne>java test1
art=article(a100,velo,1000.0,10,5)

3.4. 接口

接口是一组构成契约的方法或属性原型。决定实现某个接口的类,即承诺提供该接口中定义的所有方法的实现。编译器会验证该实现。

以下是 java.util.Enumeration 接口定义的一个示例

方法摘要
 
boolean
hasMoreElements()
          检查此枚举是否包含更多元素。
 
Object
nextElement()
          如果该枚举对象至少还有一个元素可提供,则返回该枚举的下一个元素。
 

任何实现此接口的类都将被声明为

public class C : Enumeration{
    ...
    boolean hasMoreElements(){....}
    Object nextElement(){...}
}

方法 hasMoreElements()nextElement() 必须在类 C 中定义。

请看以下代码,它定义了一个学生类,该类定义了学生的姓名及其在某门课程中的成绩:


     // a student class
public class élève{
     // public attributes
    public String nom;
    public double note;
     // manufacturer
    public élève(String NOM, double NOTE){
        nom=NOM;
        note=NOTE;
    }//manufacturer
}//student  

我们定义了一个 notes 类,用于收集某门课程中所有学生的成绩:


// imported classes
// student import
 
// class notes
public class notes{
 
     // attributes
    protected String matière;
    protected élève[] élèves;
 
     // manufacturer
    public notes (String MATIERE, élève[] ELEVES){
         // student & subject memorization
        matière=MATIERE;
        élèves=ELEVES;
    }//notes
 
    // toString
    public String toString(){
        String valeur="matière="+matière +", notes=(";
        int i;
         // concatenate all the notes
        for (i=0;i<élèves.length-1;i++){
            valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
        };
         //final note
        if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
        valeur+=")";
         // end
        return valeur;
    }//toString
}//class

subjectstudents 属性被声明为 protected,以便从派生类中访问它们。我们决定将 notes 类派生为 notesStats 类,该类将额外包含两个属性:成绩的平均值和标准差:


public class notesStats extends notes implements Istats {
     // attributes
    private double _moyenne;
    private double _écartType;

notesStats 类继承自 notes 类,并实现了以下 Istats 接口:


// an interface
public interface Istats{
    double moyenne();
    double écartType();
}//

这意味着 notesStats 类必须包含两个名为 averagestandardDeviation 的方法,且其签名需符合 Istats 接口中指定的要求。notesStats 类的定义如下:


// imported classes
// import notes;
// import Istats;
// student import;
 
public class notesStats extends notes implements Istats {
     // attributes
    private double _moyenne;
    private double _écartType;
 
     // manufacturer
    public notesStats (String MATIERE, élève[] ELEVES){
         // parent class construction
        super(MATIERE,ELEVES);
         // average score calculation
        double somme=0;
        for (int i=0;i<élèves.length;i++){
            somme+=élèves[i].note;
        }
        if(élèves.length!=0) _moyenne=somme/élèves.length;
        else _moyenne=-1;
         // standard deviation
        double carrés=0;
        for (int i=0;i<élèves.length;i++){
            carrés+=Math.pow((élèves[i].note-_moyenne),2);
        }//for
        if(élèves.length!=0) _écartType=Math.sqrt(carrés/élèves.length);
        else _écartType=-1;
    }//manufacturer
 
 
     // ToString
    public String toString(){
        return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
    }//ToString
 
     // istats interface methods
    public double moyenne(){
         // makes the average score
        return _moyenne;
    }//average
    public double écartType(){
         // makes the standard deviation
        return _écartType;
    }//écartType
}//class

均值 _mean 和标准差 _standardDev 在对象创建时即被计算出来。因此,meanstandardDev 方法仅返回 _mean_standardDev 属性的值。如果学生数组为空,这两个方法均返回 -1。

以下是测试类:


// imported classes
// student import;
// import Istats;
// import notes;
// import notesStats;
 
// test class
public class test{
    public static void main(String[] args){
        // some students & notes
        élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
         // recorded in a notes object
        notes anglais=new notes("anglais",ELEVES);
         // and display
        System.out.println(""+anglais);
         // idem with mean and standard deviation
        anglais=new notesStats("anglais",ELEVES);
        System.out.println(""+anglais);
    }//hand
}//class
 

返回结果:


matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0])
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0]),moyenne=16.0,écart-type=1.632993161855452

本示例中的不同类均包含在各自的源文件中:

E:\data\serge\JAVA\interfaces\notes>dir
06/06/2002  14:06                  707 notes.java
06/06/2002  14:06                  878 notes.class
06/06/2002  14:07                1 160 notesStats.java
06/06/2002  14:02                  101 Istats.java
06/06/2002  14:02                  138 Istats.class
06/06/2002  14:05                  247 élève.java
06/06/2002  14:05                  309 élève.class
06/06/2002  14:07                1 103 notesStats.class
06/06/2002  14:10                  597 test.java
06/06/2002  14:10                  931 test.class

NotesStats 类完全可以自行实现 averagestandardDev 方法,而无需声明它实现了 Istats 接口。那么,接口的意义何在?答案在于:一个函数可以将类型为 I 的值作为参数。任何实现接口 I 的 C 类对象,都可以作为该函数的参数。请看以下接口:


// an Iexample interface
public interface Iexemple{
    int ajouter(int i,int j);
    int soustraire(int i,int j);
}//interface

Iexemple 接口定义了两个方法:addsubtract。以下类 class1class2 实现了该接口。


// imported classes
// import Iexample;
 
public class classe1 implements Iexemple{
public int ajouter(int a, int b){
        return a+b+10;
    }
    public int soustraire(int a, int b){
        return a-b+20;
    }
}//class

// imported classes
// import Iexample;
 
public class classe2 implements Iexemple{
    public int ajouter(int a, int b){
                    return a+b+100;
                }
    public int soustraire(int a, int b){
        return a-b+200;
    }
}//class

为了简化示例,这些类除了实现 Iexample 接口外,不做其他任何事情。现在考虑以下示例:


// imported classes
// import class1;
// import class2;
 
// test class
public class test{
    // a static function
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calculate
 
     // the main function
    public static void main(String[] arg){
        // creation of two objects class1 and class2
        classe1 c1=new classe1();
        classe2 c2=new classe2();
        // static function calls calculate
        calculer(4,3,c1);
        calculer(14,13,c2);
    }//hand
}//test class

静态函数 calculate 接受一个类型为 Iexample 的元素作为参数。因此,该参数可以接收类型为 class1class2 的对象。主函数中就是这样做的,结果如下:

17
21
127
201

因此,我们可以看到,这一特性与类中的多态性相似。因此,如果一组互不相关的类 Ci(因此无法使用基于继承的多态性)具有一组签名相同的方法,那么将这些方法归入一个接口 I 中,并让所有相关类都从该接口继承,可能会很有用。 这样,这些类 Ci 的实例就可以作为接受类型为 I 的参数的函数的参数,即这些函数仅使用接口 I 中定义的 Ci 对象的方法,而不使用各个 Ci 类的特定属性和方法。

在上一个示例中,每个类或接口都是一个单独源文件的主体:

E:\data\serge\JAVA\interfaces\opérations>dir
06/06/2002  14:33                  128 Iexemple.java
06/06/2002  14:34                  218 classe1.java
06/06/2002  14:32                  220 classe2.java
06/06/2002  14:33                  144 Iexemple.class
06/06/2002  14:34                  325 classe1.class
06/06/2002  14:34                  326 classe2.class
06/06/2002  14:36                  583 test.java
06/06/2002  14:36                  628 test.class

最后,请注意接口继承可以是多重的,即可以编写

public class classeDérivée extends classeDeBase implements i1,i2,..,in{
...
}

其中 ij 均为接口。

3.5. 匿名类

在前面的示例中,class1class2 这两个类本可以省略。请看以下程序,它与前面的程序功能基本相同,但没有显式定义 class1class2 这两个类:


// imported classes
// import Iexample;
 
// test class
public class test2{
 
  // an internal class
  private static class classe3 implements Iexemple{
        public int ajouter(int a, int b){
            return a+b+1000;
        }
        public int soustraire(int a, int b){
            return a-b+2000;
        }
    };//definition class3
 
     // a static function
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calculate
 
     // the main function
    public static void main(String[] arg){
        // creation of two objects implementing the Iexemple interface
        Iexemple i1=new Iexemple(){
        public int ajouter(int a, int b){
            return a+b+10;
        }
        public int soustraire(int a, int b){
            return a-b+20;
        }
    };//definition i1
 
        Iexemple i2=new Iexemple(){
        public int ajouter(int a, int b){
            return a+b+100;
        }
        public int soustraire(int a, int b){
            return a-b+200;
        }
    };//definition i2
        // another object Iexample
    Iexemple i3=new classe3();
 
        // static function calls calculate
        calculer(4,3,i1);
        calculer(14,13,i2);
    calculer(24,23,i3);
    }//hand
}//test class
 

关键特征在于代码中:


         // creation of two objects implementing the Iexemple interface
        Iexemple i1=new Iexemple(){
        public int ajouter(int a, int b){
            return a+b+10;
        }
        public int soustraire(int a, int b){
            return a-b+20;
        }
    };//definition i1

我们创建了一个名为 i1 的对象,其唯一目的是实现 Iexample 接口。该对象的类型为 Iexample。因此,我们可以创建接口类型的对象。许多 Java 类的方法返回接口类型的对象,即那些唯一目的是实现接口方法的对象。要创建对象 i1,人们可能会想写:


        Iexemple i1=new Iexemple()

然而,接口无法被实例化。只有实现该接口的类才能被实例化。在此,我们直接在对象 i1 的定义体内“即时”定义了这样一个类:


        Iexemple i1=new Iexemple(){
        public int ajouter(int a, int b){
            // définition de ajouter
        }
        public int soustraire(int a, int b){
            // définition de soustraire
        }
    };//définition i1

此类语句的含义类似于以下序列:

public class test2{
................
// an internal class
private static class classe1 implements Iexemple{
        public int ajouter(int a, int b){
            // definition of add
        }
        public int soustraire(int a, int b){
            // definition of subtract
        }
};//definition class1
.................
    public static void main(String[] arg){
...........
        Iexemple i1=new classe1();
}//hand
}//class

在上例中,我们确实是在实例化一个类,而不是一个接口。这种“动态定义”的类被称为匿名类。这是一种常用的方法,用于实例化那些唯一目的是实现某个接口的对象。

执行上述程序将得到以下结果:

17
21
127
201
1047
2001

前面的示例使用匿名类实现了接口。这些匿名类还可以用于派生没有带参数构造函数的类。请看以下示例:

// imported classes
// import Iexample;

class classe3 implements Iexemple{
    public int ajouter(int a, int b){
        return a+b+1000;
    }
    public int soustraire(int a, int b){
        return a-b+2000;
    }
};//definition class3

public class test4{

     // a static function
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calculate

   // hand method
    public static void main(String args[]){
       // definition of an anonymized class deriving from class3
     // to redefine subtract
    classe3 i1=new classe3(){
        public int ajouter(int a, int b){
          return a+b+10000;
      }//subtract
    };//i1
          // static function calls calculate
        calculer(4,3,i1);
  }//hand
}//class 

这里有一个类 classe3,它实现了 Iexemple 接口。在 main 函数中,我们定义了一个类型为 classe3 派生类的变量 i1。这个派生类是在匿名类中“动态”定义的,并重写了 classe3 类的 ajouter 方法。 其语法与实现接口的匿名类完全相同。不过,在此处编译器会检测到 classe3 并非接口而是类。因此,对编译器而言,这属于类派生。匿名类主体中定义的所有方法都将覆盖基类中同名的方法。

运行上述程序将得到以下结果:

E:\data\serge\JAVA\classes\anonyme>java test4
10007
2001

3.6. P ackages

3.6.1. 在包中创建类

要在屏幕上打印一行,我们使用以下语句

System.out.println(...)

如果我们查看 System 类的定义,会发现它实际上被称为 java.lang.System

Image

让我们通过一个示例来验证这一点:


public class test1{
    public static void main(String[] args){
        java.lang.System.out.println("Coucou");
    }//hand
}//class

让我们编译并运行这个程序:

E:\data\serge\JAVA\classes\paquetages>javac test1.java

E:\data\serge\JAVA\classes\paquetages>dir
06/06/2002  15:40                  127 test1.java
06/06/2002  15:40                  410 test1.class

E:\data\serge\JAVA\classes\paquetages>java test1
Coucou

那么,为什么我们可以写


        System.out.println("Coucou");

而不是


        java.lang.System.out.println("Coucou");

因为默认情况下,每个 Java 程序都会自动导入 java.lang 包。因此,这相当于我们在每个程序的开头都添加了以下语句:

import java.lang.*;

这条语句意味着什么?它提供了对 java.lang 包中所有类的访问权限。编译器会在该位置找到 System.class 文件,该文件定义了 System 类。我们目前还不知道编译器会在何处找到 java.lang 包,也不知道包的具体结构。我们稍后会再回到这个话题。要在包中创建一个类,我们写:

package paquetage;
// class definition
...

在此示例中,让我们将之前学习的 Person 类置于一个包内。我们将选择 istia.st 作为包名。Person 类将变为:


// name of the package in which the person class will be created
package istia.st;
 
// class person
public class personne{
    // last name, first name, age
    private String prenom;
    private String nom;
    private int age;
  
    // builder 1
    public personne(String P, String N, int age){
        this.prenom=P;
        this.nom=N;
        this.age=age;
    }
 
    // toString
    public String toString(){
        return "personne("+prenom+","+nom+","+age+")";
    }
 
}//class
 

该类编译后将放置在当前目录的 istia\st 目录中。为什么是 istia\st?因为该包名为 istia.st

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:04       <DIR>          istia

E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002  16:04       <DIR>          st

E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002  16:28                  675 personne.class

现在,让我们在第一个测试类中使用 person 类:


    public class test{
        public static void main(String[] args){
            istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
            System.out.println("p1="+p1);
        }//hand
    }//test class

请注意,Person 类现在前面加上了其包名 istia.st。编译器将在何处查找 istia.st.Person 类? 编译器会在预定义的目录列表以及从当前目录开始的目录树中搜索所需的类。在此情况下,它会在名为 istia\st\personne.class 的文件中查找 istia.st.personne 类。这就是为什么我们将 personne.class 文件放在 istia\st 目录中的原因。现在让我们编译并运行该测试程序:

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:06                  246 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class

E:\data\serge\JAVA\classes\paquetages\personne>java test
p1=personne(Jean,Dupont,20)

为避免重复编写


            istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);

,你可以使用导入语句导入 istia.st.personne 类:


import istia.st.personne;

然后我们可以编写


            personne p1=new personne("Jean","Dupont",20);

编译器会将其转换为


            istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);

测试程序随后变为如下形式:


// imported namespaces
import istia.st.personne;
 
public class test2{
    public static void main(String[] args){
        personne p1=new personne("Jean","Dupont",20);
        System.out.println("p1="+p1);
    }//hand
}//test2 class

让我们编译并运行这个新程序:

E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:06                  246 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  16:50                  740 test2.class

E:\data\serge\JAVA\classes\paquetages\personne>java test2
p1=personne(Jean,Dupont,20)

我们已将 istia.st 包放置在当前目录中。这并非强制要求。让我们将其放入一个名为 mesClasses 的文件夹中该文件夹仍位于当前目录下。请注意,istia.st 包中的类位于名为 istia\st 的文件夹中。当前目录的目录结构如下:

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  16:06                  246 test.java
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  16:50                  740 test2.class
06/06/2002  16:21       <DIR>          mesClasses

E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses
06/06/2002  16:22       <DIR>          istia

E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia
06/06/2002  16:22       <DIR>          st

E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia\st
06/06/2002  16:01                1 153 personne.class

现在让我们再次编译 test2.java 程序:

E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
test2.java:2: package istia.st does not exist
import istia.st.personne;

由于我们已将 istia.st 包移动,编译器无法再找到它。请注意,编译器之所以进行查找,是因为存在 import 语句。默认情况下,它会在当前目录下的 istia\st 文件夹中查找该包而该文件夹已不存在。让我们来查看一下编译器选项:

E:\data\serge\JAVA\classes\paquetages\personne>javac
Usage: javac <options> <source files>
where possible options include:
  -g                        Generate all debugging info
  -g:none                   Generate no debugging info
  -g:{lines,vars,source}    Generate only some debugging info
  -O                        Optimize; may hinder debugging or enlarge class file
  -nowarn                   Generate no warnings
  -verbose                  Output messages about what the compiler is doing
  -deprecation              Output source locations where deprecated APIs are used
  -classpath <path>         Specify where to find user class files
  -sourcepath <path>        Specify where to find input source files
  -bootclasspath <path>     Override location of bootstrap class files
  -extdirs <dirs>           Override location of installed extensions
  -d <directory>            Specify where to place generated class files
  -encoding <encoding>      Specify character encoding used by source files
  -source <release>         Provide source compatibility with specified release
  -target <release>         Generate class files for specific VM version
  -help                     Print a synopsis of standard options

在此,-classpath 选项会非常有用。它允许您告诉编译器在何处查找类和包。让我们试一试。通过告知编译器 istia.st 包现在位于 mesClasses 文件夹中,进行编译:

E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath mesClasses test2.java

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:47                  236 test2.java
06/06/2002  17:03                  740 test2.class
06/06/2002  16:21       <DIR>          mesClasses

这次,编译顺利完成。让我们运行 test2.class 程序:

E:\data\serge\JAVA\classes\paquetages\personne>java test2
Exception in thread "main" java.lang.NoClassDefFoundError: istia/st/personne
        at test2.main(test2.java:6)

现在轮到 Java 虚拟机无法找到 istia/st/personne 类了。它正在当前目录中查找该类,而该类现在位于 mesClasses 目录中。让我们看看 Java 虚拟机的选项:

E:\data\serge\JAVA\classes\paquetages\personne>java
Usage: java [-options] class [args...]
           (to execute a class)
   or  java -jar [-options] jarfile [args...]
           (to execute a jar file)

where options include:
    -client       to select the "client" VM
    -server       to select the "server" VM
    -hotspot      is a synonym for the "client" VM  [deprecated]
                  The default VM is client.

    -cp -classpath <directories and zip/jar files separated by ;>
                  set search path for application classes and resources
    -D<name>=<value>
                  set a system property
    -verbose[:class|gc|jni]
                  enable verbose output
    -version      print product version and exit
    -showversion  print product version and continue
    -? -help      print this help message
    -X            print help on non-standard options
    -ea[:<packagename>...|:<classname>]
    -enableassertions[:<packagename>...|:<classname>]
                  enable assertions
    -da[:<packagename>...|:<classname>]
    -disableassertions[:<packagename>...|:<classname>]
                  disable assertions
    -esa | -enablesystemassertions
                  enable system assertions
    -dsa | -disablesystemassertions
                  disable system assertions

我们可以看到,JVM 也有一个类路径选项,就像编译器一样。让我们使用它来告诉 JVM istia.st 包的位置:

E:\data\serge\JAVA\classes\paquetages\personne>java.bat -classpath mesClasses test2
Exception in thread "main" java.lang.NoClassDefFoundError: test2

进展不大。现在连 test2 类本身都找不到。原因如下:当未指定 classpath 关键字时,系统会系统地搜索当前目录中的类;但一旦指定了 classpath,则不会搜索当前目录。因此,位于当前目录中的 test2.class 文件无法被找到。解决方案?将当前目录添加到 classpath 中。当前目录由符号 . 表示。

E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)

为何如此复杂?包(package)的目的是避免类之间的命名冲突。假设两家公司 E1 和 E2 分别发布了位于 com.e1com.e2 包中的类。 假设客户 C 购买了这两套类,其中两家公司都定义了一个名为 Person 的类。客户 C 将把来自公司 E1 的 Person 类引用为 com.e1.Person,而来自公司 E2 的则引用为 com.e2.Person,从而避免了命名冲突。

3.6.2. 搜索包

当我们在程序中编写代码时

import java.util.*;

以访问 java.util 包中的所有类时,该包位于何处?我们曾提到,默认情况下,系统会在当前目录或编译器或 JVM 的 classpath 选项中指定的目录列表中搜索包(如果存在该选项)。此外,系统还会在 JDK 安装目录下的 lib 目录中进行搜索。请考虑以下目录:

Image

在此示例中,系统将搜索 jdk14\libjdk14\jre\lib 目录,查找 .class 文件或 .jar.zip 文件(即类归档文件)。例如,让我们搜索位于上述 jdk14 目录中的 .jar 文件:

Image

其中有几十个。可以使用 WinZip 工具打开 .jar 文件。让我们打开上面的 rt.jar 文件(rt = RunTime)。它包含数百个 .class 文件,其中包括属于 java.util 包的文件:

Image

管理包的一个简单方法是将其放置在 <jdk>\jre\lib 目录下,其中 <jdk> 代表 JDK 的安装目录。通常,一个包包含多个类,将它们打包成一个 .jar 文件(JAR = Java ARchive 文件)会比较方便。jar.exe 可执行文件位于 <jdk>\bin 文件夹中:

E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\bin\jar.exe"
07/02/2002  12:52               28 752 jar.exe

若需查看 jar 程序的使用帮助,可不带任何参数直接调用该程序:

E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar.exe"
Syntaxe : jar {ctxu}[vfm0M] [fichier-jar] [fichier-manifest] [rÚp -C] fichiers ...
Options :
    -c  crÚer un nouveau fichier d'archives
    -t  gÚnÚrer la table des matiÞres du fichier d'archives
    -x  extraire les fichiers nommÚs (ou tous les fichiers) du fichier d'archives
    -u  mettre Ó jour le fichier d'existing archives
    -v  gÚnÚrer des informations verbeuses sur la sortie standard
    -f  spÚcifier le nom du fichier d'archives
    -m  inclure les informations manifest provenant du fichier manifest spÚcifiÚ
    -0  stocker seulement ; ne pas utiliser la compression ZIP
    -M  ne pas crÚer de fichier manifest pour les entrÚes
    -i  gÚnÚrer l'index for jar files spÚcifiÚs
    -C  passer au rÚpertoire spÚcifiÚ et inclure le fichier suivant
Si un rÚpertoire est spÚcifiÚ, il est traitÚ rÚcursivement.
Les noms des fichiers manifest et d'archives must be spÚcifiÚs
dans l'order of ''m'' and ''f'' indicators.

Exemple 1 : pour archiver deux fichiers de classe dans le fichier d'archives classes.jar :
       jar cvf classes.jar Foo.class Bar.class
Exemple 2 : utilisez le fichier manifest existant 'mymanifest'' to archive all files in the

           rÚpertoire foo/ dans 'classes.jar'':
       jar cvfm classes.jar monmanifest -C foo/ .

让我们回到之前在 *istia.st 包中创建的 *person.class 类:

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  17:36                  195 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  18:15                  740 test2.class

E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002  16:04       <DIR>          st

E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002  16:28                  675 personne.class

让我们创建一个名为 istia.st.jar 的文件,将 istia.st 包中的所有类打包进去,即打包上述 istia\st 目录树中的所有类:

E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar" cvf istia.st.jar istia\st\*

E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002  16:28                  467 personne.java
06/06/2002  17:36                  195 test.java
06/06/2002  16:04       <DIR>          istia
06/06/2002  16:06                  738 test.class
06/06/2002  16:47                  236 test2.java
06/06/2002  18:15                  740 test2.class
06/06/2002  18:08                  874 istia.st.jar

让我们使用 WinZip 查看 istia.st.jar 文件的内容:

Image

我们将 istia.st.jar 文件放置在 <jdk>\jre\lib\perso 目录中:

E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\jre\lib\perso"
06/06/2002  18:08                  874 istia.st.jar

现在让我们编译 test2.java 程序并运行它:

E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath istia.st.jar test2.java

E:\data\serge\JAVA\classes\paquetages\personne>java -classpath istia.st.jar;. test2
p1=personne(Jean,Dupont,20)

请注意,我们只需指定要搜索的归档文件名,而无需显式指定其位置。系统会搜索<jdk>\jre\lib树下的所有目录以查找所需的.jar文件。

3.7. 税费计算示例

我们将重新审视上一章已介绍过的税费计算问题,并使用一个类来处理它。让我们回顾一下该问题:

我们考虑一个简化的案例,即纳税人只需申报工资收入:

  • 我们计算该雇员的税率档次数量 nbParts:若未婚,则为 nbEnfants/2 + 1;若已婚,则为 nbEnfants/2 + 2,其中 nbEnfants 表示子女数量。
  • 如果其子女数量至少为三名,则可额外获得半份免税额
  • 我们计算其应税收入 R = 0.72 * S,其中 S 为其年薪
  • 计算其家庭系数 QF = R / nbParts
  • 我们计算他的税款。I. 请看下表:
12620.0
0
0
13,190
0.05
631
15,640
0.1
1,290.5
24,740
0.15
2,072.5
31,810
0.2
3,309.5
39,970
0.25
4,900
48,360
0.3
6,898.5
55,790
0.35
9,316.5
92,970
0.4
12,106
127,860
0.45
16,754.5
151,250
0.50
23,147.5
172,040
0.55
30,710
195,000
0.60
39,312
0
0.65
49,062

每行有 3 个字段。要计算税款 I,请查找满足 QF <= 字段1 的第一行。例如,如果 QF = 23,000,则找到的行将是

    24740        0.15        2072.5

此时,Tax I 等于 0.15*R - 2072.5*nbParts。如果 QF 使得条件 QF<=field1 永远不成立,则使用最后一行中的系数。这里:

    0                0.65        49062

由此得出的税额 I = 0.65*R - 49062*nbParts。

impots 类将定义如下:


// creation of an impots class
 
public class impots{
 
    // data required for tax calculation
     // come from an external source
 
    private double[] limites, coeffR, coeffN;
 
     // manufacturer
    public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
         // check that the 3 arrays have the same size
        boolean OK=LIMITES.length==COEFFR.length && LIMITES.length==COEFFN.length;
        if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la même taille("+
                                LIMITES.length+","+COEFFR.length+","+COEFFN.length+")");
        // it's good
        this.limites=LIMITES;
        this.coeffR=COEFFR;
        this.coeffN=COEFFN;
    }//manufacturer
 
     // tAX CALCULATION
    public long calculer(boolean marié, int nbEnfants, int salaire){
         // calculating the number of shares
        double nbParts;
        if (marié) nbParts=(double)nbEnfants/2+2;
        else nbParts=(double)nbEnfants/2+1;
        if (nbEnfants>=3) nbParts+=0.5;
         // calculation of taxable income & family quota
        double revenu=0.72*salaire;
        double QF=revenu/nbParts;
         // tAX CALCULATION
        limites[limites.length-1]=QF+1;
        int i=0;
        while(QF>limites[i]) i++;
        // return result
        return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
    }//calculate
}//class

创建一个税务对象,其中包含计算纳税人应纳税额所需的数据。这是该对象的稳定部分。一旦创建了该对象,就可以反复调用其 calculate 方法,根据纳税人的婚姻状况(已婚或未婚)、子女数量和年薪来计算其应纳税额。

一个测试程序可能如下所示:


//imported classes
// import impots;
import java.io.*;
 
    public class test
    {
        public static void main(String[] arg) throws IOException
        {
             // interactive tax calculator
             // the user enters three data points on the keyboard: married nbEnfants salary
             // the program then displays the tax payable
 
            final String syntaxe="syntaxe : marié nbEnfants salaire\n"
                            +"marié : o pour marié, n pour non marié\n"
                            +"nbEnfants : nombre d'enfants\n"
                            +"salaire : salaire annuel en F";
 
             // data tables required for tax calculation
            double[] limites=new double[] {12620,13190,15640,24740,31810,39970,48360,55790,92970,127860,151250,172040,195000,0};
            double[] coeffR=new double[] {0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65};
            double[] coeffN=new double[] {0,631,1290.5,2072.5,3309.5,4900,6898.5,9316.5,12106,16754.5,23147.5,30710,39312,49062};
 
       // create a reading flow
      BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
            // tax object creation
            impots objImpôt=null;
            try{
                objImpôt=new impots(limites,coeffR,coeffN);
            }catch (Exception ex){
                System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
                System.exit(1);
            }//try-catch
 
            // infinite loop
            while(true){
                 // tax calculation parameters are requested
                System.out.print("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :");
                String paramètres=IN.readLine().trim();
                // anything to do?
                if(paramètres==null || paramètres.equals("")) break;
                // check the number of arguments in the input line
                String[] args=paramètres.split("\\s+");
                int nbParamètres=args.length;
                if (nbParamètres!=3){
                    System.err.println(syntaxe);
                    continue;
                }//if
                 // checking the validity of parameters
                // married
                String marié=args[0].toLowerCase();
                if (! marié.equals("o") && ! marié.equals("n")){
                    System.err.println(syntaxe+"\nArgument marié incorrect : tapez o ou n");
                    continue;
                }//if
                 // nbEnfants
                int nbEnfants=0;
                try{
                    nbEnfants=Integer.parseInt(args[1]);
                    if(nbEnfants<0) throw new Exception();
                }catch (Exception ex){
                    System.err.println(syntaxe+"\nArgument nbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }//if
                 // salary
                int salaire=0;
                try{
                    salaire=Integer.parseInt(args[2]);
                    if(salaire<0) throw new Exception();
                }catch (Exception ex){
                    System.err.println(syntaxe+"\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }//if
                 // parameters are correct - tax is calculated
                System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
                 // next taxpayer
            }//while
        }//hand
    }//class

以下是上述程序的运行示例:

E:\data\serge\MSNET\c#\impots\3>java test

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument marié incorrect : tapez o ou n

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument nbEnfants incorrect : tapez un entier positif ou nul

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument salaire incorrect : tapez un entier positif ou nul

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d f
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F

Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :