3. 类与接口
3.1. 通过实例了解对象
3.1.1. 概述
现在,我们将通过实例来探讨面向对象编程。对象是一个包含定义其状态的数据(称为属性)和函数(称为方法)的实体。对象是根据称为类的模板创建的:
public class C1{
type1 p1; // 属性 p1
type2 p2; // 属性 p2
…
type3 m3(…){ // 方法 m3
…
}
type4 m4(…){ // 方法 m4
…
}
…
}
基于前面的类 C1,可以创建许多对象 O1、O2、…… 所有对象都将具有属性 p1、p2……以及方法 m3、m4……它们的属性值各不相同,因此每个 pi 对象都具有其特有的状态。
如果 O1 是 C1 类型的对象, 则 O1.p1 表示 O1 的属性 p1,而 O1.m1 表示 O1 的方法。
让我们考虑一个初始对象模型:类 personne。
3.1.2. Person类的定义
类 personne 的定义如下:
import java.io.*;
public class personne{
// 属性
private String prenom;
private String nom;
private int age;
// 方法
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// 方法
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
这里定义了一个类,也就是一种数据类型。当我们创建该类型的变量时,会将其称为对象。因此,类是一个用于构建对象的模板。
类的成员或字段可以是数据,也可以是方法(函数)。这些字段可以具有以下三种属性之一:
privé:私有(private)字段仅限类内部方法访问
public:公共字段(public)可被类内定义或未定义的任何函数访问
protégé:受保护(protected)字段仅可由类或派生对象(稍后将介绍继承概念)的内部方法访问。
通常,类的数据被声明为私有,而其方法被声明为公有。这意味着对象的使用者(程序员)
a:无法直接访问对象的私有数据
b:可以调用对象的公共方法,特别是那些能够访问其私有数据的方法。
对象的声明语法如下:
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
}
Remarques
- private、protected 和 public 属性的声明顺序不限。
3.1.3. 初始化方法
让我们回到声明为:
import java.io.*;
public class personne{
// 属性
private String prenom;
private String nom;
private int age;
// 方法
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// 方法
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
方法 initialise 的作用是什么?由于姓名和年龄是“Person”类的私有数据,因此以下语句
这些语句是非法的。我们需要通过一个公共方法来初始化一个 personne 类型的对象。这就是方法 initialise 的作用。我们将编写:
p1.initialise 的写法是合法的,因为 initialise 是公开访问的。
3.1.4. new 运算符
指令序列
是错误的。指令
将 p1 声明为对类型为 personne 的对象的引用。该对象尚未存在,因此 p1 未被初始化。这相当于写成:
其中通过关键字 null 明确表示变量 p1 尚未引用任何对象。
随后编写
时,实际上是在调用由 p1 引用的对象的 initialise 方法。但该对象尚未存在,编译器将报错。要使 p1 引用一个对象,应写为:
这将创建一个尚未初始化的 personne 类型对象: 属性 nom 和 prenom(它们是 String 类型对象的引用)将分别取值 null 而 age 的值将变为 0。因此存在默认初始化。现在 p1 引用了一个对象,该对象的初始化语句
是有效的。
3.1.5. 关键字 this
让我们看看方法 initialise 的代码:
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
语句 this.prenom=P 表示当前对象(this)的属性 prenom 被赋值为 P。 关键字 this 指代当前对象:即包含被执行方法的对象。我们如何得知这一点?让我们看看在调用程序中,由 p1 引用的对象是如何初始化的:
被调用的其实是对象 p1 的方法 initialise。当在这个方法中引用对象 this 时,实际上引用的是对象 p1。 方法 initialise 也可以写成如下形式:
public void initialise(String P, String N, int age){
prenom=P;
nom=N;
this.age=age;
}
当对象的一个方法引用该对象的属性 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();
}
}
类 personne 在源文件 personne.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 并未通过以下语句导入类 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 类,今后在程序开头应写为:
现在我们可以运行文件 test1.class:
可以将多个类合并到同一个源文件中。因此,我们将类 personne 和 test1 合并到源文件 test2.java 中。 类 test1 已重命名为 test2,以适应源文件名称的变更:
// 导入的包
import java.io.*;
class personne{
// 属性
private String prenom; // 该人的名字
private String nom; // 他的姓
private int age; // 年龄
// 方法
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}//初始化
// 方法
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}//识别
}//类
public class test2{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
需要注意的是,类 personne 不再具有属性 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:
此后,我们将交替使用这两种方法:
- 将所有类合并到一个源文件中
- 每个源文件对应一个类
3.1.7. 另一种初始化方法
我们继续以类 personne 为例,并为其添加以下方法:
public void initialise(personne P){
prenom=P.prenom;
nom=P.nom;
this.age=P.age;
}
现在有两个名为 initialise 的方法:只要它们接受不同的参数,这种做法就是合法的。本例中正是如此。 该参数现在是一个指向某人的引用 P。随后,该人(P)的属性被赋予给当前对象(this)。 需要注意的是,尽管 P 对象的属性类型为 private,但方法 initialise 仍可直接访问其属性。 这一点始终成立:C 类中 O1 对象的方法始终可以访问同一 C 类中其他对象的私有属性。
以下是对新类 personne 的测试:
// 导入 Person;
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();
}
}
及其结果:
3.1.8. Person 类的构造函数
构造函数是一种以类名命名的方法,在创建对象时被调用。它通常用于初始化对象。该方法可以接受参数,但不返回任何结果。其原型或定义前不带任何类型(甚至不带 void)。
如果一个类有一个接受 n 个参数的构造函数 argi,则该类对象的声明和初始化可以采用以下形式:
classe objet =new classe(arg1,arg2, ... argn);
或
classe objet;
…
objet=new classe(arg1,arg2, ... argn);
当一个类有一个或多个构造函数时,必须使用其中一个构造函数来创建该类的对象。 如果类 C 没有构造函数,则它有一个默认构造函数,即无参构造函数:public C()。此时,对象的属性将使用默认值进行初始化。这就是在之前的程序中编写以下代码时发生的情况:
现在为我们的 personne 类创建两个构造函数:
public class personne{
// 属性
private String prenom;
private String nom;
private int age;
// 构造函数
public personne(String P, String N, int age){
initialise(P,N,age);
}
public personne(personne P){
initialise(P);
}
// 方法
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;
}
// 方法
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
这两个构造函数仅调用相应的 initialise 方法。需要提醒的是,当在构造函数中出现例如 initialise(P) 这样的表示法时,编译器会将其转换为 this.initialise(P)。 因此,在构造函数中,会调用方法 initialise 来处理由 this 引用的对象,即当前对象,也就是正在构建的对象。
以下是一个测试程序:
// import Person;
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();
}
}
以及所得结果:
3.1.9. 对象引用
我们始终使用同一个类 personne。测试程序如下:
// import Person;
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 引用与 p1 相同的对象
personne p2=p1;
System.out.print("p2="); p2.identifie();
// p3 引用一个对象,该对象将是 p1 所引对象的副本
personne p3=new personne(p1);
System.out.print("p3="); p3.identifie();
// 修改 p1 所引用的对象的状态
p1.initialise("Micheline","Benoît",67);
System.out.print("p1="); p1.identifie();
// 由于 p2=p1,p2 引用的对象的状态必然发生了变化
System.out.print("p2="); p2.identifie();
// 由于 p3 引用的对象与 p1 不同,因此 p3 引用的对象的状态应该没有改变
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 时
p1 引用了对象 personne("Jean","Dupont",30),但并非该对象本身。在 C 语言中,这相当于一个指针 c.a.d,即所创建对象的地址。如果随后编写:
被修改的并非对象 personne("Jean","Dupont",30),而是引用 p1 的值发生了变化。如果对象 person("Jean","Dupont",30) 没有被其他变量引用,它将会“丢失”。
当我们编写:
则初始化了指针 p2:它“指向”与指针 p1 相同的对象(即指向同一个对象)。 因此,如果修改了由 p1 “指向”(或引用的)对象,则会修改由 p2 引用的对象。
当我们编写:
将创建一个新对象,该对象是 p1 所引用的对象的副本。这个新对象将由 p3 引用。 如果修改由 p1 “指向”(或引用的)对象,则由 p3 引用的对象不会受到任何影响。测试结果正是如此。
3.1.10. 临时对象
在表达式中,我们可以显式调用对象的构造函数:该对象会被构建,但我们无法访问它(例如进行修改)。这个临时对象是为了评估表达式而构建的,随后会被释放。 它所占用的内存空间随后将由一个名为“垃圾回收器”的程序自动回收,该程序的作用是回收那些不再被程序数据引用的对象所占用的内存空间。
请看以下示例:
// import person;
public class test1{
public static void main(String arg[]){
new personne(new personne("Jean","Dupont",30)).identifie();
}
}
并修改类 personne 的构造函数,使其显示一条消息:
// 制造商
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);
}
我们得到以下结果:
显示了两个临时对象的连续构建过程。
3.1.11. 读取和写入私有属性的方法
我们在类 personne 中添加了用于读取或修改对象属性状态的必要方法:
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);
}
// 辅助人员
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//修改者
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
我们使用以下程序测试新类:
// 导入人员;
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()+")");
}
}
我们得到以下结果:
3.1.12. 类的方法和属性
假设我们想统计应用程序中创建的 personne 对象的数量。 虽然可以自行管理计数器,但可能会遗漏那些零散创建的临时对象。更稳妥的做法是在 personne 类的构造函数中加入一条递增计数器的语句。 问题在于需要传递该计数器的引用以便构造函数能对其进行递增:必须为其添加一个新参数。也可以将计数器包含在类定义中。由于它是类本身的属性,而非该类中某个特定对象的属性,因此需使用关键字 static 进行不同声明:
要引用它时,需写为 personne.nbPersonnes,以表明它是 personne 类本身的属性。在此,我们创建了一个私有属性,在类外部无法直接访问它。 因此,我们需要创建一个公共方法,以便访问类属性 nbPersonnes。 为了获取 nbPersonnes 的值,该方法无需特定的对象:因为 nbPersonnes 并非某个特定对象的属性,而是整个类的属性。 因此,我们需要一个同样声明为 static 的类方法:
该方法在外部调用时将采用 personne.getNbPersonnes() 的语法。以下是一个示例。
personne 类将变为如下形式:
public class personne{
// 类属性
private static long nbPersonnes=0;
// 对象属性
…
// 构造函数
public personne(String P, String N, int age){
initialise(P,N,age);
nbPersonnes++;
}
public personne(personne P){
initialise(P);
nbPersonnes++;
}
// 方法
…
// 类方法
public static long getNbPersonnes(){
return nbPersonnes;
}
}// 类
使用以下程序:
// import person;
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());
}// main
}//test1
得到以下结果:
3.1.13. 将对象传递给函数
我们之前已经提到,Java 通过值传递传递函数的实际参数:实际参数的值会被复制到形式参数中。因此,函数无法修改实际参数。
对于对象而言,切勿被系统性出现的语言误用所迷惑——人们常将“对象”一词误用于指代“对象引用”。 对象只能通过指向它的引用(指针)进行操作。因此传递给函数的并非对象本身,而是指向该对象的引用。因此,在形式参数中复制的是引用的值,而非对象本身的值:不会创建新的对象。
如果将对象引用 R1 传递给一个函数,它将被复制到相应的形式参数 R2 中。 因此,引用 R2 和 R1 指向同一个对象。 如果该函数修改了由 R2 指向的对象,那么它显然也会修改由 R1 引用的对象,因为它们是同一个对象。

下例说明了这一点:
// import Person;
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();
}// main
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();
}// 修改
}// 类
方法 modifie 被声明为 static,因为它是一个类方法:调用时无需在前面添加对象前缀。所得结果如下:
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
可以看出,仅构建了一个对象:即函数 main 中的对象 p1,且该对象确实已被函数 modifie 修改。
3.1.14. 将函数的输出参数封装到对象中
由于采用按值传递参数的方式,在Java中无法编写一个具有int类型输出参数的函数,因为无法传递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 整数;
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);
}
}
并得到以下结果:
3.1.15. 人员数组
对象与其他数据一样,因此可以将多个对象集合到一个数组中:
// import person;
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 个 personne 类型元素的数组。 这3个元素在此处被初始化为值 null、c.a.d,它们并不引用任何对象。再次强调,这只是语言上的误用,我们称之为“对象数组”,而实际上它只是一个对象引用的数组。 对象数组的创建(该数组本身即为一个对象,如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. 概述
本文将探讨继承的概念。继承的目的是“定制”现有类,使其满足我们的需求。假设我们要创建一个名为 enseignant 的类:教师是一个特殊的人。 他拥有其他人所没有的属性:例如他所教授的学科。但他同时也具备所有人的通用属性:名字、姓氏和年龄。因此,教师完全属于类 personne,但拥有额外的属性。 与其从零开始编写类 enseignant,不如沿用类 personne 的现有内容,并根据教师的特殊性进行调整。正是继承的概念使我们能够做到这一点。
为了表示类 enseignant 继承了类 personne 的属性,我们将写为:
public class enseignant extends personne
personne 被称为父类(或母类),而 enseignant 被称为派生类(或子类)。 一个 enseignant 对象具备 personne 对象的所有特性:它拥有相同的属性和方法。父类的这些属性和方法在子类的定义中不会重复:我们只需列出子类新增的属性和方法:
class enseignant extends personne{
// 属性
private int section;
// 构造函数
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
}
我们假设类 personne 定义如下:
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+")";
}
// 访问器
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//修饰符
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
方法 identifie 已稍作修改,用于返回标识该人员的字符串,现更名为 identite。 在此,类 enseignant 在类 personne 的方法和属性基础上新增了:
- 一个 section 属性,表示教师在教师团队中所隶属的分组编号(大致上每门学科一个分组)
- 一个新的构造函数,用于初始化教师的所有属性
3.2.2. 创建教师对象
类 enseignant 的构造函数如下:
// 构造函数
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
语句 super(P,N,age) 是对父类构造函数的调用,此处为类 personne。 我们知道,该构造函数会初始化 étudiant 对象内部包含的 personne 对象的 prenom、nom 和 age 字段。这看起来相当复杂,因此我们可能更倾向于这样写:
// 构造函数
public enseignant(String P, String N, int age,int section){
this.prenom=P;
this.nom=N
this.age=age
this.section=section;
}
这不可能。类 personne 将其三个字段 prenom、nom 和 age 声明为私有(private)。 只有同一类的对象才能直接访问这些字段。 所有其他对象,包括此处的子对象,都必须通过公共方法才能访问这些字段。如果 personne 类将这三个字段声明为受保护的(protected),情况就会不同:它将允许派生类直接访问这三个字段。 因此,在本例中,使用父类的构造函数是正确的解决方案,这也是常规做法:在构建子对象时,首先调用父对象的构造函数,然后完成子对象(本例中为 section)特有的初始化操作。
让我们尝试编写第一个程序:
// 导入人员;
// import 教师;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
该程序仅创建了一个 enseignant 对象(new)并对其进行标识。 类 enseignant 没有方法 identité,但其父类有一个,而且该方法是公共的:通过继承,它成为类 enseignant 的公共方法。
将类源文件集中到同一个目录中,然后进行编译:
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:
3.2.3. 方法重载
在上一个示例中,我们已获取了教师部分 personne 的身份信息,但缺少 enseignant 类(班级)的特定信息。因此,我们需要编写一个方法来识别教师:
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+")";
}
}
类 enseignant 中的方法 identite 基于其父类中的方法 identite (super.identite)来显示其“personne”部分,然后通过 enseignant 类特有的字段 section 进行补充。
类 enseignant 现在拥有两个方法 identite:
- 一个是从父类 personne 继承而来的
- 以及其自身的方法
若 E 是 enseignant 对象,则 E.identite 指代类 enseignant 中的方法 identite。 我们说父类的 identite 方法被子类的 identite 方法“重载”了。 一般而言,如果 O 是一个对象,M 是一个方法,那么为了执行方法 O.M,系统将按以下顺序查找方法 M:
- 在对象 O 的类中
- 如果存在,则在其父类中
- 在其父类的父类中(如果存在)
- 等等……
因此,继承允许在子类中重载父类中同名的方法。正是这一点使得子类能够根据自身需求进行适配。结合稍后将要介绍的多态性,方法重载是继承的主要优势。
让我们考虑与之前相同的示例:
// import 人员;
// import 教师;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
此次获得的结果如下:
3.2.4. 多态性
考虑以下类继承序列:C0 C1 C2 … Cn
其中 Ci Cj 表示类 Cj 是从类 Ci 派生而来的。这意味着类 Cj 不仅具有类 Ci 的所有特征,还具有其他特征。 设 Oi 是类型 Ci 的对象。以下写法是合法的:
事实上,通过继承,类 Cj 不仅拥有类 Ci 的所有特征,还包含其他特征。因此,类型为 Cj 的对象 Oj 内部包含一个类型为 Ci 的对象。操作
使得 Oi 成为对象 Oj 中所包含的 Ci 类型对象的引用。
事实上,Ci类的变量Oi不仅可以引用Ci类的对象,实际上还可以引用所有从Ci类派生的对象,这一现象被称为多态性: 变量能够引用不同类型对象的能力。
让我们通过一个示例来考察以下与任何类都无关的函数:
类 Object 是所有 Java 类的“父类”。因此,当我们编写:
就隐含地写成了:
因此,所有 Java 对象内部都包含一个 Object 类型的部分。因此,我们可以这样写:
显示函数中类型为 Object 的形式参数将接收类型为 enseignant 的值。由于教师类继承自 Object,因此这是合法的。
3.2.5. 重载与多态
让我们完善我们的函数 affiche:
方法 obj.toString() 返回一个字符串,该字符串以 nom_de_la_classe@adresse_de_l'objet 的形式标识对象 obj。那么,在之前的示例中会发生什么情况:
系统将执行指令 System.out.println(e.toString()),其中 e 是一个教师对象。 它将从通向类 enseignant 的类层次结构中查找方法 toString,并从最后一个类开始:
- 在类 enseignant 中,未找到方法 toString()
- 在父类 personne 中,未找到方法 toString()
- 在父类 Object 中,它找到了方法 toString() 并执行了它
如下面的程序所示:
// import 人员;
// 导入教师;
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());
}
}
所得结果如下:
即 nom_de_la_classe@adresse_de_l 对象。由于这并不十分直观, 我们可能会考虑为类 personne 和 etudiant 定义一个方法 toString,该方法将重载父类 ObjectQX。与其编写与类 personne 和 enseignant 中已存在的 identite 方法相似的方法, 我们只需将这些 identite 方法重命名为 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+")";
}
}
使用与之前相同的测试程序,所得结果如下:
3.3. 内部类
一个类可以包含另一个类的定义。请看以下示例:
// 导入的类
import java.io.*;
public class test1{
// 内部类
private class article{
// 定义结构
private String code;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
// 构造函数
public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
// 属性初始化
this.code=code;
this.nom=nom;
this.prix=prix;
this.stockActuel=stockActuel;
this.stockMinimum=stockMinimum;
}//构造函数
//toString
public String toString(){
return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
}//toString
}//物料类别
// 本地数据
private article art=null;
// 制造商
public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
// 属性定义
art=new article(code, nom, prix, stockActuel,stockMinimum);
}//test1
// 访问器
public article getArticle(){
return art;
}//getArticle
public static void main(String arg[]){
// 创建 test1 实例
test1 t1=new test1("a100","velo",1000,10,5);
// 显示 test1.art
System.out.println("art="+t1.getArticle());
}//主程序
}// 类结束
类 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
针对类 test1 内部的类 article,生成了一个名为 test1$article.class 的文件。若运行上述程序,将得到以下结果:
3.4. 接口
接口是一组方法或属性的原型,共同构成一个契约。决定实现某个接口的类,即承诺提供该接口中定义的所有方法的实现。由编译器负责验证该实现。
以下是接口 java.util.Enumeration 的定义示例:
方法摘要 | ||
boolean | hasMoreElements() 测试此枚举是否包含更多元素。 | |
Object | nextElement() 如果该枚举对象至少还有一个元素可提供,则返回该枚举的下一个元素。 | |
任何实现此接口的类都将被声明为
方法 hasMoreElements() 和 nextElement() 必须在类 C 中定义。
请看以下代码,其中定义了一个名为 élève 的类,用于记录学生的姓名及其某门课程的成绩:
// 一个学生类
public class élève{
// 公共属性
public String nom;
public double note;
// 构造函数
public élève(String NOM, double NOTE){
nom=NOM;
note=NOTE;
}//构造函数
}//学生
我们定义了一个名为 notes 的类,用于汇总某门课程中所有学生的成绩:
// 导入的类
// 导入学生
// 成绩类
public class notes{
// 属性
protected String matière;
protected élève[] élèves;
// 构造函数
public notes (String MATIERE, élève[] ELEVES){
// 学生与科目存储
matière=MATIERE;
élèves=ELEVES;
}//成绩
// toString
public String toString(){
String valeur="matière="+matière +", notes=(";
int i;
// 合并所有成绩
for (i=0;i<élèves.length-1;i++){
valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
};
//最后一个成绩
if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
valeur+=")";
// 结束
return valeur;
}//toString
}//调式
属性 matière 和 élèves 被声明为 protected,以便从派生类中访问。 我们决定将类 notes 派生为类 notesStats,该类将增加两个属性:成绩的平均值和标准差:
public class notesStats extends notes implements Istats {
// 属性
private double _moyenne;
private double _écartType;
类 notesStats 继承自类 notes,并实现了以下接口 Istats:
// 一个接口
public interface Istats{
double moyenne();
double écartType();
}//
这意味着类 notesStats 必须包含两个方法,分别命名为 moyenne 和 écartType,且其签名应符合接口 Istats 中指定的格式。 类 notesStats 如下所示:
// 导入类
// import notes;
// 导入 Istats;
// 导入学生;
public class notesStats extends notes implements Istats {
// 属性
private double _moyenne;
private double _écartType;
// 构造函数
public notesStats (String MATIERE, élève[] ELEVES){
// 父类的构造
super(MATIERE,ELEVES);
// 计算平均分
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;
// 标准差
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;
}//构造函数
// ToString
public String toString(){
return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
}//ToString
// Istats 接口的方法
public double moyenne(){
// 计算评分平均值
return _moyenne;
}//平均值
public double écartType(){
// 返回标准差
return _écartType;
}//écartType
}//类
均值 _moyenne 和标准差 _ecartType 在对象构建时即被计算出来。 因此,方法 moyenne 和 écartType 只需返回属性 _moyenne 和 _ecartType 的值。如果学生数组为空,这两个方法将返回 -1。
以下是测试类:
// 导入的类
// 导入学生;
// 导入 Istats;
// 导入成绩;
// 导入 notesStats;
// 测试班级
public class test{
public static void main(String[] args){
// 若干学生及成绩
élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
// 将其保存到一个名为“notes”的对象中
notes anglais=new notes("anglais",ELEVES);
// 并进行显示
System.out.println(""+anglais);
// 同上,包含平均分和标准差
anglais=new notesStats("anglais",ELEVES);
System.out.println(""+anglais);
}//主程序
}//班级
结果如下:
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 本可以自行实现方法 moyenne 和 écartType,而无需声明它实现了接口 Istats。 那么,接口的意义何在?答案如下:一个函数可以接受类型为接口 I 的数据作为参数。因此,任何实现接口 I 的类 C 的对象都可以作为该函数的参数。考虑以下接口:
// 一个 Iexample 接口
public interface Iexemple{
int ajouter(int i,int j);
int soustraire(int i,int j);
}//接口
接口 Iexemple 定义了两个方法 ajouter 和 soustraire。以下类 classe1 和 classe2 实现了该接口。
// 导入的类
// 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;
}
}//类
// 导入的类
// 导入 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;
}
}//类
为了简化示例,这些类除了实现 Iexemple 接口外,不执行任何其他操作。现在考虑以下示例:
// 导入的类
// import 类1;
// 导入类2;
// 测试类
public class test{
// 一个静态函数
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//计算
// main函数
public static void main(String[] arg){
// 创建两个对象类1和类2
classe1 c1=new classe1();
classe2 c2=new classe2();
// 调用静态函数计算
calculer(4,3,c1);
calculer(14,13,c2);
}//main
}//测试类
静态函数 calculer 接受类型为 Iexemple 的参数。因此,该参数既可以接收类型为 classe1 的对象,也可以接收类型为 classe2 的对象。 函数 main 正是如此实现的,结果如下:
由此可见,这里存在一种与类中多态性相似的特性。因此,如果一组 Ci 类之间没有继承关系(因此无法使用继承的多态性),但具有一组签名相同的方法,那么将这些方法归入一个接口 I 中,并让所有相关类都继承该接口,可能会很有用。 这样,这些类 Ci 的实例就可以作为接受类型为 I 的参数的函数的参数,c.a.d。这些函数仅使用接口 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
最后需要注意的是,接口的继承可以是多重的,c.a.d。我们可以这样写
其中 ij 代表接口。
3.5. 匿名类
在上例中,类 classe1 和 classe2 本可以不显式定义。 请看以下程序,它与前一个程序的功能基本相同,但省略了 classe1 和 classe2 类的显式定义:
// 导入的类
// import Iexample;
// 测试类
public class test2{
// 一个内部类
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;
}
};//类3定义
// 一个静态函数
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//计算
// main函数
public static void main(String[] arg){
// 创建两个实现 Iexemple 接口的对象
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;
}
};//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;
}
};//定义 i2
// 另一个 Iexemple 对象
Iexemple i3=new classe3();
// 调用静态函数calculer
calculer(4,3,i1);
calculer(14,13,i2);
calculer(24,23,i3);
}//main
}//测试类
其特殊之处在于代码:
// 创建两个实现 IExample 接口的对象
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;
}
};//定义 i1
创建一个名为 i1 的对象,其唯一作用是实现接口 Iexemple。该对象的类型为 Iexemple。 因此,我们可以创建接口类型的对象。许多Java类的方法都会返回接口类型c.a.d的对象,这些对象的唯一作用就是实现接口的方法。为了创建对象i1,我们可能会想写:
Iexemple i1=new Iexemple()
但接口本身无法被实例化,只有实现该接口的类才能被实例化。在此,我们直接在 i1 对象定义的正文中“动态”定义了这样一个类:
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
// 定义添加
}
public int soustraire(int a, int b){
// 定义减法
}
};//定义 i1
此类指令的含义与以下序列类似:
public class test2{
................
// 一个内部类
private static class classe1 implements Iexemple{
public int ajouter(int a, int b){
// 加法的定义
}
public int soustraire(int a, int b){
// 减法的定义
}
};//类1的定义
.................
public static void main(String[] arg){
...........
Iexemple i1=new classe1();
}//main
}//类
在上例中,我们确实实例化了一个类,而不是一个接口。这种“即席”定义的类被称为匿名类。这是一种常用的方法,用于实例化那些唯一作用是实现接口的对象。
运行上述程序将得到以下结果:
上例使用匿名类来实现接口。这些类也可用于派生没有带参数构造函数的类。请看以下示例:
// 导入的类
// 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;
}
};//类3定义
public class test4{
// 一个静态函数
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//计算
// main方法
public static void main(String args[]){
// 定义一个继承自类3的匿名类
// 用于重定义减法
classe3 i1=new classe3(){
public int ajouter(int a, int b){
return a+b+10000;
}//减法
};//i1
// 调用静态函数计算
calculer(4,3,i1);
}//main
}//类
其中包含一个类 classe3,该类实现了接口 Iexemple。 在函数 main 中,我们定义了一个变量 i1,其类型为 classe3 的派生类。 该派生类是在匿名类中“动态”定义的,并重写了类 classe3 中的方法 ajouter。其语法与实现接口的匿名类完全相同。 只是在此处,编译器检测到 classe3 并非接口而是类。因此,它将其视为类派生。编译器在匿名类主体中找到的所有方法都将替换基类中同名的方法。
运行上述程序将得到以下结果:
3.6. 包
3.6.1. 在包中创建类
要在屏幕上输出一行内容,我们使用以下语句
如果查看类 System 的定义,我们会发现它实际上被调用为 java.lang.System:

让我们通过一个示例来验证一下:
public class test1{
public static void main(String[] args){
java.lang.System.out.println("Coucou");
}//main
}//类
编译并运行该程序:
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。因此,这相当于在程序开头添加了以下语句:
这条语句意味着什么?它提供了对 java.lang 包中所有类的访问权限。编译器将在该包中找到定义类 System 的文件 System.class。 目前尚不清楚编译器将从何处找到 java.lang 包,也不清楚包的具体结构。我们稍后会再讨论这一点。要在包中创建一个类,应编写如下代码:
作为示例,让我们在包中创建之前讨论过的类 personne。我们将包命名为 istia.st。类 personne 变为:
// 将创建 Person 类的包名称
package istia.st;
// person类
public class personne{
// 姓名、名字、年龄
private String prenom;
private String nom;
private int age;
// 构造函数 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+")";
}
}//类别
该类编译后被放置在当前目录下的 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
现在,让我们在第一个测试类中使用类 personne:
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);
}//手
}//测试类
请注意,类 personne 现在前面加上了其包名 istia.st。编译器将从何处找到类 istia.st.personne? 编译器会在预定义的目录列表以及从当前目录开始的目录树中查找所需的类。在此示例中,它将从文件 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);
可以使用 import 子句导入类 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);
测试程序如下:
// 导入的命名空间
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);
}//主函数
}//类 test2
编译并运行这个新程序:
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 与编译器一样,也提供了一个 classpath 选项。我们利用该选项来指定 istia.st 包的位置:
E:\data\serge\JAVA\classes\paquetages\personne>java.bat -classpath mesClasses test2
Exception in thread "main" java.lang.NoClassDefFoundError: test2
进展不大。现在是 test2 类本身无法被找到。原因如下:当缺少 classpath 关键字时,在搜索类时会系统地遍历当前目录;但当该关键字存在时,则不会遍历当前目录。 因此,位于当前目录中的类 test2.class 无法被找到。解决方案?将当前目录添加到 classpath 中。当前目录由符号 . 表示。
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)
为什么这么复杂?包的目的是为了避免类名冲突。 假设两家企业 E1 和 E2 分别在 com.e1 和 com.e2 包中分发打包类。 假设客户 C 购买了这两套类,其中两家公司都定义了一个名为 personne 的类。客户 C 将引用 E1 公司的企业E1的类personne时,将通过com.e1.personne进行引用,而引用企业E2的类时,则通过com.e2.personne进行引用,从而避免了名称冲突。
3.6.2. 包搜索
当我们在程序中编写
以访问 java.util 包中的所有类时,该包位于何处? 我们曾提到,系统默认会在当前目录或编译器 classpath 选项中声明的目录列表中搜索包,如果该选项存在,也会在 JVM 中搜索。 此外,系统还会在 lib 安装目录下的 JDK 目录中进行搜索。让我们来看一下这个目录:

在此示例中,将遍历目录树 jdk14\lib 和 jdk14\jre\lib,以查找其中的 .class 文件,或是作为类存档的 .jar 或 .zip 文件。 例如,我们来搜索位于前一个目录 jdk14 下的 .jar 文件:

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

管理这些包的一个简单方法是将其放置在 <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''archives existant
-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 pour les fichiers jar 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 doivent Ûtre spÚcifiÚs
dans l''ordre des indicateurs ''m'' et ''f''.
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 ''monmanifest'' pour archiver tous les fichiers du
rÚpertoire foo/ dans ''classes.jar'':
jar cvfm classes.jar monmanifest -C foo/ .
让我们回到之前在 istia.st 包中创建的 personne.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 的内容:

将文件 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. 示例 IMPÔTS
我们重新计算上一章已探讨过的税款,并使用一个类来处理。回顾一下问题:
我们考虑一个简化的情况,即纳税人仅需申报工资收入:
- 计算该雇员的税额份额 nbParts=nbEnfants/2 +1(若未婚), 已婚则为 nbEnfants/2+2,其中 nbEnfants 代表其子女数。
- 若其子女数≥3,则额外增加半份
- 计算其应税收入 R=0.72*S,其中 S 为其年薪
- 计算其家庭系数 QF=R/nbParts
- 计算其应缴税额 I。请看下表:
12620.0 | 0 | 0 |
13190 | 0.05 | 631 |
15640 | 0.1 | 1290.5 |
24740 | 0.15 | 2072.5 |
31810 | 0.2 | 3309.5 |
39970 | 0.25 | 4900 |
48360 | 0.3 | 6898.5 |
55790 | 0.35 | 9316.5 |
92970 | 0.4 | 12106 |
127860 | 0.45 | 16754.5 |
151250 | 0.50 | 23147.5 |
172040 | 0.55 | 30710 |
195000 | 0.60 | 39312 |
0 | 0.65 | 49062 |
每行有 3 个字段。要计算税款 I,需查找满足 QF<=字段1 的第一行。例如,若 QF=23000,则会找到该行
此时税额 I 等于 0.15*R - 2072.5*nbParts。 如果 QF 使得关系 QF<=field1 从未成立,则使用最后一行中的系数。此处:
由此得出的税额 I=0.65*R - 49062*nbParts。
税类将定义如下:
// 创建一个税类
public class impots{
// 计算税款所需的数据
// 来自外部来源
private double[] limites, coeffR, coeffN;
// 制造商
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
// 检查这3个数组是否大小相同
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+")");
// 没问题
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//生成器
// 计算税额
public long calculer(boolean marié, int nbEnfants, int salaire){
// 计算份额数
double nbParts;
if (marié) nbParts=(double)nbEnfants/2+2;
else nbParts=(double)nbEnfants/2+1;
if (nbEnfants>=3) nbParts+=0.5;
// 应税收入及家庭系数计算
double revenu=0.72*salaire;
double QF=revenu/nbParts;
// 税额计算
limites[limites.length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// 返回结果
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//计算
}//分类
创建了一个名为“impots”的对象,其中包含用于计算纳税人税款的数据。这是该对象的固定部分。创建该对象后,可以反复调用其“calculer”方法,该方法根据纳税人的婚姻状况(已婚或未婚)、子女数量和年薪来计算其应缴税款。
一个测试程序可以如下所示:
//导入的分类
// 导入税款;
import java.io.*;
public class test
{
public static void main(String[] arg) throws IOException
{
// 交互式税款计算程序
// 用户通过键盘输入三项数据:已婚 nbEnfants 工资
// 程序随后显示应缴税额
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";
// 计算税款所需的数据表
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};
// 创建读取流
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
// 创建税款对象
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
// 无限循环
while(true){
// 请求税款计算参数
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();
// 有什么需要做的吗?
if(paramètres==null || paramètres.equals("")) break;
// 验证输入行中的参数数量
String[] args=paramètres.split("\\s+");
int nbParamètres=args.length;
if (nbParamètres!=3){
System.err.println(syntaxe);
continue;
}//if
// 验证参数的有效性
// 已婚
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
// 工资
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
// 参数正确 - 计算税款
System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
// 下一位纳税人
}//while
}//主程序
}//类
以下是上述程序的运行示例:
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 :