4.2 类与对象
4.2.1 类
在面向对象的语言中,类是个很重要的概念。面向对象的方法把所有的处理对象进行归类。具有相同性质的对象归为一类。例如学校里有很多学生,每个学生都是一个对象,而学生则是一个类,它包含了所有在学校学习的人。
在Java语言里,对象是一组描述对象的属性和操作方法的集合,其中属性表明对象的状态,方法表明对象的行为。类是对象的定义。一个对象具有哪些属性和方法,由类来决定。从编程角度看,类是一种复合数据类型,它封装了一组变量和方法(函数)。
声明一个类的一般形式如下所示:
修饰符 class 类名{
//类体
}
其中修饰符说明了类的属性,可以是public、abstract、final等。这些修饰符的含意将会在后继的章节中介绍。类体就是类的成员变量和成员方法的声明。修饰符和类体的声明都是可以选的。下面是一个人的类的声明,虽然类体是空的:
class Person{
}
4.2.2 对象
对象与类是不同但是又紧密相联的概念。类是对象的定义,对象由类来生成。类与对象的关系好比铸造车间里模具与产品的关系。模具只有一个,但是用这个模具可以浇铸出很多成型的产品出来,模具的形状决定了浇铸出来的产品的外形。在Java语言里用new关键字来生成对象,通常的格式为:
类名 对象名=new 类名([参数]);
这里的对象名可以是任意合法的Java标识符。new关键字后带小括号的类名称为构造方法(函数)。默认的、也是最简单的构造方法是不带参数的,也可以自定义不同形式的构造方法以供不时之需。下例利用刚才的Person类来生成两个Person对象Mike和John:
Person Mike=new Person();
Person John=new Person();
用new Person()生成一个对象时不仅分配了内存空间还进行了一些初始化的工作,对象所包含的不仅只是各个属性名了,而是属性的具体值。如果没有给属性赋值,虚拟机会自动给它们赋于相应数据类型默认的初值。生成一个对象的过程也称为实例化,所以一个对象就是一个实例。Mike和John是对象名,对象名是用来引用对象的,对象里的变量和方法可以通过对象名来引用,所以对象名也称为引用(Reference)。引用类似于C/C++中指针的概念,与指针不同,引用不是直接指向对象所在的内存位置,但是它包含了内存地址的信息。Java中并没有指针,以指针进行内存操作常造成不预知的错误,还可能破坏安全性,也正是因为如此,Java被认为是安全的编程语言。上例中生成的对象和引用如下图所示:
图4-1 引用与对象
在某些特殊的情况下,可能生成实例但不需要引用,那可以直接用new来实例化。如下所示:
new Person();
new Person();
上面两行程序生成两个Person对象,但是每次生成的对象分别占用不同的内存空间,改变其中一个对象的状态不会影响其它对象的状态。就象每次用模具倒出一个毛坯一样, 尽管两个毛坯看起来很象,但它们绝对是两个不同的毛坯。
4.2.3 成员变量
在面向对象的思想中,通常用属性来描述对象的特征。在编程语言里,这一组从属于某类对象的属性是用变量来表示的,例如前文提到用来表示人的特征的身高、体重、姓名等等,就可以分别用不同类型的变量:double型的height、float型weight、String型的name来表示。这些属于类的变量,称为类的成员变量。
声明成员变量的一般形式如下:
修饰符 数据类型 成员变量名;
其中修饰符说明了类的成员变量的属性,可以是public、protected、private、static、transient、final、volatile等等。成员变量的类型可以是Java内置的或者是自定义的复杂数据类型,包括简单类型、类、接口、数组。例如,描述一个人有一座房产可以用House类型的变量house来表示,这里的House是用户自定义的类(复杂数据类型)。调用成员变量的一般形式是:
实例名.成员变量名
下面是一个人的类的声明,类名是Person。描述人的成员变量有:height、weight、sex、name、age等,还有房产house的声明。
【例4-1】
class Person{
double height;
float weight;
char sex;
String name;
int age;
House house;
}
class House{}
public class E4_1{
public static void main(String args[]){
Person p_Bill=new Person();
p_Bill.age=26;
p_Bill.height=180;
p_Bill.weight=145;
p_Bill.sex=m;
p_Bill.name="Bill Gates";
p_Bill.house=new House();
}
}
Person类有6个成员变量,其中5个是简单数据类型,1个是复合数据类型House。在类E4_1的main函数里生成了一个引用名为p_Bill的对象。通过这个p_Bill这个引用可以访问到这个人的年龄、身高、体重等属性。
上例的程序可以在全部放在一个源文件中,源文件名为必须为E4_1。Java的一个源文件允许多个类的存在,但是文件名必须和public类一致,一个源文件中只也能有一个public类。
成员变量不需要显式初始化可以直接使用。其实,它们还是有被初始化过的,称为隐式的初始化。没有显式初始化的成员变量是在为对象分配存储空间时自动被初始化,各种类型的变量被赋于默认的初始化值,如下所示:
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char \u0000(null)
boolean false
所有复合数据类型 null
下例打印了隐式初始化的Person类成员变量。
【例4-2】
class Person{
double height;
float weight;
char sex;
String name;
int age;
House house;
}
class House{}
public class E4_2{
public static void main(String args[]){
Person p_Bill=new Person();
System.out.println(p_Bill.age);
System.out.println(p_Bill.height);
System.out.println(p_Bill.weight);
System.out.println(p_Bill.sex);
System.out.println(p_Bill.name);
System.out.println(p_Bill.house);
}
}
得到的结果是:
0
0.0
0.0
null
null
4.2.4 成员方法
对象的方法可以用来改变对象的属性,或者用来接收来自其它对象的信息以及向其它对象发送消息。从编程角度看,类是一组Java语句的集合。这里说的方法和面向过程语言C、Pascal等中的函数其实是一个概念。但是在象Java这样的纯面向对象的语言里,函数只能包含在类里面,而且是用来描述类的行为的,一般称为方法。
定义方法的一般形式如下所示:
修饰符 返回类型 方法名([参数类型 参数名,…])[throws 异常列表] {
//方法体
}
方括号中为可选项。修饰符可以是public、protected、private、static、final、abstract、native、synchronized等。返回类型既可以是简单数据类型,也可以是复合数据类型(数组、类或接口)。如果方法没有返回值,也必须在返回类型处用void声明。或者可以说,这个方法返回void类型。例如:void setHeight(){},但是,一旦方法声明某种返回类型,方法体中必须用return关键字返回和声明类型一致的数据。throws用来声明抛出异常,见第七章异常处理与断言。
调用对象方法的一般格式是:
引用名.方法名([参数列表]);
下例是一个person类比较完整的定义:
【例4-3】
public class E4_3{
public static void main(String args[]){
Person p=new Person();
p.setAge(30);
System.out.println(p.getAge());
}
}
class Person{
private double height;
private float weight;
private char sex;
private String name;
private int age;
public void setAge(int age) {
this.age = age;
}
public void setHeight(double height) {
this.height = height;
}
public void setName(String name) {
this.name = name;
}
public void setSex(char sex) {
this.sex = sex;
}
public void setWeight(float weight) {
this.weight = weight;
}
public int getAge() {
return age;
}
public float getWeight() {
return weight;
}
public char getSex() {
return sex;
}
public String getName() {
return name;
}
public double getHeight() {
return height;
}
}
在上例里,每个成员变量都有两个方法与之对应,分别是set和get方法。set方法用来改变成员变量的值,返回类型是void,而get方法用来获取成员变量的值,它们的方法体中都用return来返回与方法声明时一致的数据类型。该例中的this关键字用来引用当前对象。
方法的用法和面向过程语言中的函数的用法是一样的。但是,在Java语言里,没有独立于类之外的方法,所有的方法都在类里面,所以一般通过对象名来调用方法。有些静态(static)方法则可以用类名来调用。上例E4_3类的main方法里生成一个名为p的Person对象,通过p这个引用调用setAge()方法设置p的年龄,最后调用getAge()打印p的年龄。
方法体内可以包含所有合法的Java指令。可以声明该方法所用到的局部变量,它的作用域只在该方法内部,当方法调用结束时,局部变量也就失效了。如果方法体内的局部变量名和类的成员变量的名字相同,则类的成员变量被隐藏,也就是失效了,当前有效的是局部变量。
【例4-4】
public class E4_4{
int x=0,y=0,z=0;
void action(int x){
x=11;//参数x
y=12;//类成员变量y
int z=13;//方法内部变量z
System.out.println("在init()方法内部:");
System.out.println("成员变量:"+"x="+this.x+" y="+this.y+" z="+this.z);
System.out.println("局部变量:"+"x="+x+" z="+z);
}
public static void main(String args[]){
E4_4 e=new E4_4();
System.out.println("调用前:");
System.out.println("成员变量:"+"x="+e.x+" y="+e.y+" z="+e.z);
e.action(1);
System.out.println("调用后:");
System.out.println("成员变量:"+"x="+e.x+" y="+e.y+" z="+e.z);
}
}
程序的输出是:
调用前:
成员变量:x=0 y=0 z=0
在init()方法内部:
成员变量:x=0 y=12 z=0
局部变量:x=11 z=13
调用后:
成员变量:x=0 y=12 z=0
在上例的action方法里,参数x和新定义的变量z作用范围都只在action方法内部,因为它们和类成员变量x,z同名,所以类成员变量被覆盖。在action方法内部使用x和z都将调用参数x和局部变量z,而不是类成员变量。但这不并意味着无法在方法内部调用到成员变量。在上例中,我们用到了this。this代表调用该方法的当前对象(实例)。所以可以用this来调用该对象所有的成员变量和成员方法。
方法可以带多个参数,用逗号分隔开。对于简单数据类型,Java实现的是值传递, 方法接收参数的值,但不改变实参的值。对于复合数据类型,Java实现的是引用的传递,可以改变原参数的值。下例说明了简单数据类型与引用数据类型的区别。
【例4-5】
public class E4_5{
int i=0;
public void primary(int x){
x=3;
}
public void complex(E4_5 e2){
e2.i=3;
}
public static void main(String args[]){
E4_5 e=new E4_5();
System.out.println("调用primary前:e="+e.i);
e.primary(0);
System.out.println("调用primary后:e="+e.i);
System.out.println("调用complex前:e="+e.i);
e.complex(e);
System.out.println("调用complex后:e="+e.i);
}
}
上例的输出是:
调用primary前:e=0
调用primary后:e=0
调用complex前:e=0
调用complex后:e=3
我们在类E4_4中定义了两个方法:primary(int x)和 complex(E4_4 e)。primary方法接收的参数是int类型的值,方法内部对接收到值赋于新值3,但由于该方法接收的是简单数据类型,所以方法内值的修改不会影响方法外成员变量的值。而complex接收的参数是引用类型。这时,引用e和引用e2指向同一个对象,如下图所示:
图4-2 e和e2指向同一个对象
既然它们代表同一个对象,e2对该对象的修改自然就影响了引用e。