第四章 类
4.1 类的概念
Java语言是一种纯面向对象的编程语言(OOP,你所有使用的内容,都是从类开始。你设计的思维模式,就是对象,就是一个整体观),面向对象的程序设计是以类为基础的。面向对象的程序设计是以类为基础的,Java程序是由类构成的。一个Java程序至少包含一个或一个以上的类。我们可以把客观世界中的每一个实体都看作是一个对象,如一个人、一辆汽车、一个按钮、一只鸟等等。在我们的程序开发中,对象的定义并不局限于看得见摸得着的实体,诸如一个贸易公司,它作为一个机构,并没有物理上的形状,但却具有概念上的形状,它有明确的经营目的和业务活动。
面向对象的方法将系统看作是现实世界对象的集合,在现实世界中包含被归类的对象。面向对象系统是以类为基础的,我们把一系列具有共同属性和行为的对象划归为一类。
我们可以简单地把类定义为:“具有共同属性和行为的一系列对象的一个抽象概念”。
面向对象的特点:抽象、封装、继承、多态
类是对现实世界中实体的抽象,类是一组具有共同特征和行为的对象的抽象描述。因此,一个类的定义包括如下两个方面:
定义属于该类对象共有的属性(属性的类型和名称);
定义属于该类对象共有的行为(所能执行的操作即方法)。
类包含类的声明和类体两部分,其定义类的一般格式如下:
[访问限定符] [修饰符] class 类名 [extends 父类名] [implements 接口名列表>]//类声明
{ //类体开始标志
[类的成员变量说明] //属性说明
[类的构造方法定义]
[类的成员方法定义] //行为定义
} //类体结束标志
对类声明的格式说明如下:
1) 方括号“[]”中的内容为可选项,在下边的格式说明中意义相同,不再重述。
2) 访问限定符的作用是:确定该定义类可以被哪些类使用。可用的访问限定符如下:
a) public 表明是公有的。可以在任何Java程序中的任何对象里使用公有的类。该限定符也用于限定成员变量和方法。如果定义类时使用public进行限定,则类所在的文件名必须与此类名相同(包括大小写)
b) private表明是私有的。该限定符可用于定义内部类,也可用于限定成员变量和方法。
c) protected 表明是保护的。只能为其子类所访问。
d) 默认访问 若没有访问限定符,则系统默认是友元的 (friendly)。友元的类可以被本类包中的所有类访问。
3) 修饰符的作用是:确定该定义类如何被其他类使用。可用的类修饰符如下:
a) abstract 说明该类是抽象类。抽象类不能直接生成对象。
b) final 说明该类是最终类,最终类是不能被继承的。
4) class是关键字,定义类的标志(注意全是小写)。
5) 类名是该类的名字,是一个Java标识符,含义应该明确。一般情况下单词首字大写。
6) 父类名跟在关键字 “extends”后,说明所定义的类是该父类的子类,它将继承该父类的属性和行为。父类可以是Java类库中的类,也可以是本程序或其他程序中定义的类。
7) 接口名表是接口名的一个列表,跟在关键字“implements”后,说明所定义的类要实现列表中的所有接口。一个类可以实现多个接口,接口名之间以逗号分隔。如前所述,Java不支持多重继承,类似多重继承的功能是靠接口实现的。
以上简要介绍了类声明中各项的作用,我们将在后边的章节进行详细讨论。
类体中包含类成员变量和类方法的声明及定义,类体以定界符左大括号“{”开始,右大括号“}”结束。类成员变量和类方法的声明及定义将下边各节中进行详细讨论。
我们先看一个公民类的定义示例。
public class Citizen
{
[ 声明成员变量 ] //成员变量(属性)说明
[ 定义构造方法 ] //构造方法(行为)定义
[ 定义成员方法 ] //成员方法(行为)定义
}
我们把它定义为公有类,在任何其他的Java程序中都可以使用它。
4.1.1 一个类可以包含以下类型变量:
局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
类变量:类变量也声明在类中,方法体之外,但必须声明为static类型。
4.1.2 源文件声明规则
当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。
一个源文件中只能有一个public类
一个源文件可以有多个非public类
源文件的名称应该和public类的类名保持一致。例如:源文件中public类的类名是Employee,那么源文件应该命名为Employee.java。
如果一个类定义在某个包中,那么package语句应该在源文件的首行。
如果源文件包含import语句,那么应该放在package语句和类定义之间。
如果没有package语句,那么import语句应该在源文件中最前面。
import语句和package语句对源文件中定义的所有类都有效。
在同一源文件中,不能给不同的类不同的包声明。
类有若干种访问级别,并且类也分不同的类型:抽象类和final类等。这些将在访问控制章节介绍。
除了上面提到的几种类型,Java还有一些特殊的类,如:内部类、匿名类。
4.2 成员变量
成员变量用来表明类的特征(属性)。声明或定义成员变量的一般格式如下:
[访问限定符] [修饰符] 数据类型 成员变量名[=初始值];
其中:
1)访问限定符用于限定成员变量被其它类中的对象访问的权限,和如上所述的类访问限定符类似。
2)修饰符用来确定成员变量如何在其他类中使用。可用的修饰符如下:
a) static 表明声明的成员变量为静态的。静态成员变量的值可以由该类所有的对象共享,它属于类,而不属于该类的某个对象。即使不创建对象,使用“类名.静态成员变量”也可访问静态成员变量。
b) final 表明声明的成员变量是一个最终变量,即常量。
c) transient 表明声明的成员变量是一个暂时性成员变量。一般来说成员变量是类对象的一部分,与对象一起被存档(保存),但暂时性成员变量不被保存。
d) volatile 表明声明的成员变量在多线程环境下的并发线程中将保持变量的一致性。
3)数据类型可以是简单的数据类型,也可以是类、字符串等类型,它表明成员变量的数据类型。
类的成员变量在类体内方法的外边声明,一般常放在类体的开始部分。
下边我们声明公民类的成员变量,公民对象所共有的属性有:姓名、别名、性别、出生年月、出生地、身份标识等。
import java.util.*;
public class Citizen
{
//以下声明成员变量(属性)
String name;
String alias;
String sex;
Date brithday; //这是一个日期类的成员变量
String homeland;
String ID;
//以下定义成员方法(行为)
……
}
在上边的成员变量声明中,除出生年月被声明为日期型(Date)外,其他均为字符串型。由于Date类被放在java.util类包中,所以在类定义的前边加上import语句。
4.3 成员方法
方法用来描述对象的行为,在类的方法中可分为构造器方法和成员方法,本小节先介绍成员方法。
成员方法用来实现类的行为。方法也包含两部分,方法声明和方法体(操作代码)。
方法定义的一般格式如下:
[访问限定符] [修饰符] 返回值类型 方法名([形式参数表]) [throws 异常表]
{
[ 变量声明 ] //方法内用的变量,局部变量
[ 程序代码 ] //方法的主体代码
[ return [ 表达式 ] ] //返回语句
}
在方法声明中:
1)访问限定符如前所述。
2)修饰符用于表明方法的使用方式。可用于方法的修饰符如下:
a) abstract说明该方法是抽象方法,即没有方法体(只有“{}”引起的空体方法)。
b) final说明该方法是最终方法,即不能被重写。
c) static说明该方法是静态方法,可通过类名直接调用。
d) native说明该方法是本地化方法,它集成了其他语言的代码。
e) synchronized说明该方法用于多线程中的同步处理。
3)返回值类型应是合法的java数据类型。方法可以返回值,也可不返回值,可视具体需要而定。当不需要返回值时,可用void(空值)指定,但不能省略。
4)方法名是合法Java标识符,声明了方法的名字。
5)形式参数表说明方法所需要的参数,有两个以上参数时,用“,”号分隔各参数,说明参数时,应声明它的数据类型。
6)throws 异常表定义在执行方法的过程中可能抛出的异常对象的列表(放在后边的异常的章节中讨论)。
以上简要介绍了方法声明中各项的作用,在后边章节的具体应用示例中再加深理解。
方法体内是完成类行为的操作代码。根据具体需要,有时会修改或获取对象的某个属性值,也会访问列出对象的相关属性值。下边还以公民类为例介绍成员方法的应用,在类中加入设置名字、获取名字和列出所有属性值3个方法。
例4.1 完善公民类Citizen。程序如下:
/* 这是一个公民类的定义程序
* 程序的名字是:Citizen.java
*/
import java.util.*;
public class Citizen
{
//以下声明成员变量(属性)
String name;
String alias;
String sex;
Date brithday; //这是一个日期类的成员变量
String homeland;
String ID;
//以下定义成员方法(行为)
public String getName() //获取名字方法
{ //getName()方法体开始
return name; //返回名字
} //getName()方法体结束
/***下边是设置名字方法***/
public void setName(String name)
{ //setName()方法体开始
this.name=name;
} //setName()方法体结束
/***下边是列出所有属性方法***/
public void displayAll()
{ //displayAll()方法体开始
System.out.println(“姓名:”+name);
System.out.println(“别名:”+alias);
System.out.println(“性别:”+sex);
System.out.println(“出生:”+brithday.toLocaleString());
System.out.println(“出生地:”+homeland);
System.out.println(“身份标识:”+ID);
} //displayAll()方法体结束
}
在上边的示例中,两个方法无返回值(void),一个方法返回名字(String);两个方法不带参数,一个方法带有一个参数,有关参数的使用将在后边介绍。在显示属性方法中,出生年月的输出使用了将日期转换为字符串的转换方法toLocaleString()。
需要说明的是,在设置名字方法setName()中使用了关键字this,this代表当前对象,其实在方法体中引用成员变量或其他的成员方法时,引用前都隐含着“this.”,一般情况下都会缺省它,但当成员变量与方法中的局部变量同名时,为了区分且正确引用,成员变量前必须加“this.”不能缺省。
4.4 构造方法
构造方法用来构造类的对象,构造方法也叫做构造器。如果在类中没有构造方法,在创建对象时,系统使用默认的构造方法。定义构造方法的一般格式如下:
[public] 类名([形式参数列表])
{
[方法体]
}
我们可以把构造方法的格式和成员方法的格式作一个比较,可以看出构造方法是一个特殊的方法。应该严格按照构造方法的格式来编写构造方法,否则构造方法将不起作用。有关构造方法的格式强调如下:
1)构造方法的名字就是类名。
2)访问限定只能使用public或缺省。一般声明为public,如果缺省,则只能在同一个包中创建该类的对象。
3)在方法体中不能使用return语句返回一个值。
下边定义的公民类Citizen中添加如下的构造方法:
public Citizen(String name,String alias,String sex,Date brithday,String homeland,String ID)
{
this.name=name;
this.alias=alias;
this.sex=sex;
this.brithday=brithday;
this.homeland=homeland;
this.ID=ID;
}
4.5 对象
之前我们对类已经有了一个概要的了解,类作为一种对象的概括可数据的一个抽象。并没有实际的意义,只有将类的每一个属性赋予以特定值,才有使用的价值。如人类和一个具体的人物。赋予了具体属性值的类,叫做对象。
4.5.1. 声明对象
声明对象的一般格式如下:
类名 对象名;
例如:
Citizen p1,p2; //声明了两个公民对象
Float f1,f2; //声明了两个浮点数对象
声明对象后,系统还没有为对象分配存储空间,只是建立了空的引用,通常称之为空对象(null)。因此对象还不能使用。
4.5.2. 创建对象
对象只有在创建后才能使用,创建对象的一般格式如下:
对象名 = new 类构造方法名([实参表]);
其中:类构造方法名就是类名。new运算符用于为对象分配存储空间,它调用构造方法,获得对象的引用(对象在内存中的地址)。
例如:
p1=new Citizen(“丽柔”,”温一刀”,”女”,new Date(),”中国上海”,”410105651230274x”);
f1=new Float(30f);
f2=new Float(45f);
注意:声明对象和创建对象也可以合并为一条语名,其一般格式是:
类名 对象名 = new 类构造方法名([实参表]);
例如:
Citizen p1=new Citizen(“丽柔”,”温一刀”,”女”,new Date(),”中国上海”,”410105651230274x”);
Float f1=new Float(30f);
Float f2=new Float(45f);
4.5.3. 引用对象
在创建对象之后,就可以引用对象了。引用对象的成员变量或成员方法需要对象运算符“.”。
引用成员变量的一般格式是: 对象名.成员变量名
引用成员方法的一般格式是: 对象名.成员方法名([实参列表])
在创建对象时,某些属性没有给于确定的值,随后可以修改这些属性值。例如:
Citizen p2=new Citizen(“李明”,”“,”男”,null,”南京”,”50110119850624273x”);
对象p2的别名和出生年月都给了空值,我们可以下边的语句修正它们:
p2.alias=”飞翔鸟”;
p2.brithday=new Date(“6/24/85”);
名字中出现别字,我们也可以调用方法更正名字:
p2.setName(“李鸣”);
/* Citizen测试程序
* 程序的名字是:TestCitizenExam4_2.java
*/
import java.util.*;
public class TestCitizenExam4_2
{
public static void main(String [] args)
{
Citizen p1,p2; //声明对象
//创建对象p1,p2
p1=new Citizen("丽柔","温一刀","女",new Date("12/30/88"),"上海","421010198812302740");
p2=new Citizen("李明"," ","男",null,"南京","50110119850624273x");
p2.setName("李鸣"); //调用方法更正对象的名字
p2.alias="飞翔鸟"; //修改对象的别名
p2.brithday=new Date("6/24/85"); //修改对象的出生日期
p1.displayAll(); //显示对象p1的属性值
System.out.println("----------------------------");
p2.displayAll(); //显示对象p2的属性值
}
}
4.5.4. this关键字
每个类的每个非静态方法(没有被static修饰)都会隐含一个this关键字,this表示当前实例对象,即自己。在当前方法中使用本对象的属性时,都会隐含地使用this关键字,当然也可以明确指定。
使用语法:this.字段;
构造器中相互调用,但是此时this([参数])必须写在构造方法第一行。
this不能用在static修饰的方法里和static修饰的代码块里;
this 关键字只能在方法内部使用。
4.6 静态(static)变量
静态,就是以“static”修饰符说明,一般把静态变量称为类变量,而把非静态变量称为实例变量。
静态(类)变量是在不创建对象的前提下,可以直接引用的变量。
对静态变量说明若下:
1)以修饰符static说明的变量被称之为静态变量,其他为非静态变量;
2)以修饰符final说明的变量称之为最终变量即常量。它常常和static修饰符一起使用,表示静态的最终变量即静态常量。
3)静态变量在类被装入后即分配了存储空间,它是类成员,不属于一个具体的对象,而是所有对象所共享的。和静态方法类似,它可以由类直接引用也可由对象引用。
由类直接引用的格式是:类名.成员名
4)非静态变量在对象被创建时分配存储空间,它是实例成员,属于本对象。只能由本对象(this)引用。
变量的初始化在前边我们已经作过简单介绍。
这里介绍的初始化器是用大括号“{}”括起的一段程序代码为变量赋初值。初始化器可分为静态的(初始化静态变量)和非静态的(初始化非静态变量)两种。下边我们只介绍静态的初始化器,因为初始化非静态变量没有实际意义,非静态变量的初始化一般在构造方法中完成。静态的初始化器的一般格式如下:
static
{
……… //为静态变量赋值的语句及其他相关语句
}
这段代码称之为静态代码块,使用注意事项如下:
1)如果需要计算来初始化静态变量,可以声明一个静态块
2)静态块仅在该类被加载时执行一次。
3)只能初始化静态数据成员。
4)如果类包含多个静态块,则以在类中出现的顺序分别执行。
下边我们举一个简单的例子说明初始化器的应用。
例 在程序中定义两个静态变量,使用初始化器初始化其值。程序代码如下:
/*这是一个测试静态初始化器的程序
*程序的名字是:StaticExam4-10.java
*/
public class StaticExam4_10
{
static int var1;
static int var2;
public static void display() //显示属性值方法
{
System.out.println("var1="+var1);
System.out.println("var2="+var2);
} //显示方法结束
static //初始化静态变量
{
System.out.println("现在对变量进行初始化...");
var1=128; //为变量var1赋初值
var2=var1*8; //为变量var2赋初值
System.out.println("变量的初始化完成!!!");
} //初始化器结束
public static void main(String [] args) //主方法
{
display(); //显示属性值
} //主方法结束
}
4.7 静态(static)方法
所谓静态方法,就是以“static”修饰符说明的方法。在不创建对象的前提下,可以直接引用静态方法,其引用的一般格式为:
类名.静态方法名( [ 实参表 ] )
一般我们把静态方法称之为类方法,而把非静态方法称之为类的实例方法(即只能被对象引用)。
4.7.1. 使用方法注意事项
在使用类方法和实例方法时,应该注意以下几点:
1)当类被加载到内存之后,类方法就获得了相应的入口地址;该地址在类中是共享的,不仅可以直接通过类名引用它,也可以通过创建类的对象引用它。只有在创建类的对象之后,实例方法才会获得入口地址,它只能被对象所引用。
2)无论是类方法或实例方法,当被引用时,方法中的局部变量才被分配内存空间,方法执行完毕,局部变量立该释放所占内存。
3)在类方法里只能引用类中其他静态的成员(静态变量和静态方法),而不能直接访问类中的非静态成员。这是因为,对于非静态的变量和方法,需要创建类的对象后才能使用;而类方法在使用前不需要创建任何对象。在非静态的实例方法中,所有的成员均可以使用。
4)不能使用this和super关键字(super关键字在后面章节中介绍)的任何形式引用类方法。这是因为this是针对对象而言的,类方法在使用前不需创建任何对象,当类方法被调用时,this所引用的对象根本没有产生。
下边我们先看一个示例.
例:在类中有两个整型变量成员,分别在静态和非静态方法display()中显示它们。若如下的程序代码由错,错在哪里,应如何改正。
public class Example4_7 {
int var1, var2;
public Example4_7() {
var1 = 30;
var2 = 85;
}
public static void display() {
System.out.println("var1=" + var1);
System.out.println("var2=" + var2);
}
public void display(int var1, int var2) {
System.out.println("var1=" + var1);
System.out.println("var2=" + var2);
}
public static void main(String[] args) {
Example4_7 v1 = new Example4_7();
v1.display(v1.var1, v1.var2);
display();
}
}
我们可以看出两个错误均来自静态方法display(),错误的原因是在静态方法体内引用了外部非静态变量,这是不允许的。此外,也应该看到在非静态方法display()中设置了两个参数用于接收对象的两个属性值,这显然是多余的,因为非静态方法由对象引用,可以在方法体内直接引用对象的属性。
解决上述问题的方法是:
1) 将带参数的display()方法修改为静态方法,即添加修饰符static;
2) 将不带参数的display()方法修改为非静态方法,即去掉修饰符static;
3) 修改main()中对display()方法的引用方式,即静态的方法直接引用,非静态的方法由对象引用。
示例2:
public class ExampleStatic {
static int var1, var2;
static {
var1 = 1;
var2 = 2;
System.out.println("我是静态语句块1");
}
public Example4_7() {
System.out.println("我是构造方法");
}
static{
System.out.println("我是静态语句块2");
}
public static void display() {
System.out.println("var1=" + var1);
System.out.println("var2=" + var2);
}
public void display(int var1, int var2) {
System.out.println("var1=" + var1);
System.out.println("var2=" + var2);
}
public static void main(String[] args) {
System.out.println("创建对象前");
new ExampleStatic();
System.out.println("创建对象后");
}
}
4.7.2. main() 方法
我们在前边的程序中已经看到了main()方法的应用。main()方法就是一个静态的方法,main()方法也是一个特殊的方法,在Java应用程序中可以有许多类,每个类也可以有许多方法。但解释器在装入程序后首先运行的是main()方法。
main()方法和其他的成员方法在定义上没有区别,其格式如下:
public static void main(String [] args)
{
//方法体定义
………
}
其中:
1) 声明为public以便解释器能够访问它。
2) 修饰为static是为了不必创建类的实例而直接调用它。
3) void 表明它无返回值。
4) 它带有一个字符串数组参数(有关数组的内容,将在后边的章节介绍),它可以接收引用者向应用程序传递相关的信息,应用程序可以通过这些参数信息来确定相应的操作,就象其他方法中的参数那样。
main()方法不能象其他的类方法那样被明确地引用用,那么如何向main()方法传递参数呢?我们只能从装入运行程序的命令行传递参数给它。其一般格式如下:
java 程序名 实参列表
其中:
1)java是解释器,它将装入程序并运行之。
2)程序名是经javac 编译生成的类文件名,也就是应用程序的文件名。
3)当实参列表包含多个参数时,参数之间以空格分隔,每个参数都是一个字符串。需要注意的是,如果某个实参字符串中间包含空格时,应以定界符双引号(“ ”)将它们引起来。
尽管数组部分的内容还没有介绍,但还是要举一个简单的例子说明main()方法参数的传递及操作。
例4.8 从命令行传递”This is a simple Java program. ”,”ok!”两个字符串并显示。程序参考代码如下:
/**这是一个简单的演示程序,程序名为 CommandExam4_8.java
*其目的是演示接收从命令行传递的参数并显示。
*/
class CommandExam4_8
{
public static void main(String [ ] args)
{
System.out.println(args[0]); //显示第一个字符串
System.out.println(args[1]); //显示第二个字符串
}
}