20 KiB
1. 初探类与对象
1.1. 现实生活中类和对象的关系
我们首先看看日常生活中的一些例子,假如我们要做一辆模型汽车,我们需要有哪些步骤? 首先我们需要设计,可能你需要画图纸,进行一些标注和说明。当设计完成后,就可以准备材料,按照图纸进行制作了。如果你愿意,你可以按照图纸再做几辆模型车,可能会改变颜色,不同的涂装,大一点的只需要按照比例放大就可以了。
我们制作出来的所有模型车都是按照图纸的,因此具备设计中所包含的所有特性。假如我们把这个设计叫做A100,那么所有成品模型车都是一类车(Class)每一辆模型车都是A100的一个实例(Object 可能有些属性不一样,例如颜色)。
好了,这就是类与对象的基本内涵。类是一个模板(抽象的描述),对象是模板的一个实例(具体的)。这一点有点类似C中结构体的定义与结构体变量的关系,定义是抽象描述,变量是实体。
我们再深入讨论一下对象。对象(Object)这个概念广泛存在于我们的生活当中,一般只一个独立的个体,具备一定的特性(properties),并有一定的能力(capability)。例如,一辆A100:
1.2. 程序空间中类与对象的关系
请思考在我们学习的C语言中,有没有抽象描述和具体实例化的应用?例如,在C语言中,如果描述一个平面中的圆?
考虑到已经学习过C语言,使用C语言中的结构体来帮助理解抽象描述与实例的关系。
这个例子是描述平面中的一个圆。这里我们需要两组参数来描述圆,第一个参数是坐标位置(X、Y);另外一个是圆本生的描述,例如大小(半径r),可能还有颜色等等,简单来说,我们目前只有半径。如果从C语言出发,需要建立一个结构体来描述圆,请同学们写出该结构体的代码。
如果从C语言出发,需要建立一个结构体来描述圆,代码如下:
struct Circle
{
float x;
float y;
float r;
};
上面的代码是抽象描述(Class)还是一个具体的实例(Object)?
上面只是定义了一个结构体(抽象,代表所有圆),但是还不存在一个真正的圆的实例(具体的对象),因此需要定义一个变量,其类型是Circle这个结构体。
struct Circle c1;
定义变量后,变量c1在内存中就分配了存储空间,这样好比是把一个抽象的描述(Class)实例化了,因此c1在这里可以看成是一个对象(Object),这好像是从图纸到成品的过程。
上面只是生成了一个圆的数据存储空间,描述了圆的属性。但是一个完整的圆是不是应该还有行为能力?例如我们希望打印这个圆的基本信息,应该如何做?
C语言的实现需要利用一个函数(行为能力),其输入参数是结构体类型的变量,然后在函数中对传输参数进行解析与打印。
问题:既然这个打印函数与圆的属性紧密相关,为什么不把圆的数据描述与行为能力(打印函数)放在一起?这样做有什么好处?
其实我们最希望的是结构体的定义和结构体的操作函数可以打包在一个更优化的代码当中。例如上述的打印函数应该和结构体进行强关联,最好成为结构体的一部分。这样做如同我的化学实现工具和化学实验用的材料应该放在一个地方一样。
显然,C语言并没有提供相应的语法结构用于描述一个类/对象(包含数据和函数)。如果把相关数据和函数封装在一起,具备更好的程序结构,也便于我们阅读和理解。当然C++可以扩展一个结构体,使其可以包含函数,这不是我们要讨论的内容。
1.3. 用Java来实现
我们来看看Java是如何做的。首先,我们定义一个圆,不过这圆的属性(变量)与行为能力(函数)在一起。
class Circle {
float x;
float y;
float r;
public void printCircle() {
System.out.printf("The position x=%f, y=%f, radius=%f!\n", x, y, r);
}
}
大家会发现,这个圆的定义和C中结构体的定义类似,只不过包含了打印函数。因为打印函数在Circle这个类中,可以直接调用这个类的变量(x、y、r),因此不用再通过参数来传递一个圆的数据。
接下来我们看看如何使用这个类:
public class Circle_Test {
public static void main(String[] args) {
Circle circle = new Circle();
circle.x = 1.2F;
circle.y = 2.4F;
circle.r = 3.3F;
circle.printCircle();
}
}
注意前面字面量的1.2F,2.4F等,因为如果不写,字面量的类型是double,所以不能赋值给 float 的变量。
问题:Java的语法与C的语法有什么不一样? 1、看起来和C的语法很像; 2、所有的代码都必须定义在类当中; 3、有更多的关键字,如static、public,这些我们以后再分析; 4、主函数不一样,且一定要这样写; 5、没有结构体的定义,而是类(class)的定义; 6、从类生成一个对象使用的是 new 操作;
1.4. 优化代码
对圆的参数赋值需要三条语句,太麻烦了,可以如何改进?对,我们可以使用一个函数来对圆的属性进行赋值,代码如下:
class Circle {
float x;
float y;
float r;
public void printCircle() {
System.out.printf("The position x=%f, y=%f, radius=%f!\n", x, y, r);
}
public void setCircle(float x, float y, float r) {
this.x = x;
this.y = y;
this.r = r;
}
}
public class Circle_Test {
public static void main(String[] args) {
Circle circle = new Circle();
circle.setCircle(1.1F, 2.2F, 3.3F);
circle.printCircle();
}
}
注意setCircle
函数中的this.x = x
这样的写法。因为这个对象本生有变量x、y、z;这些变量叫做成员变量,其作用域是在整个类的定义范围内;但是setCircle
的形式参数中的x、y、z与成员变量重名了,但是语义不同。为了区分这两个变量,使用this关键字,this.x
代表对象的成员变量,而x
代表传递进来的参数。
1.5. 构造函数
构造函数是一个特殊的函数,在调用的时候产生一个新对象。构造函数名永远和类的名字相同。不带参数的构造函数对一个类来说非常重要。
在定义一个类的时候,可以不提供构造函数,这时,编译器将自动产生一个不带参数的构造函数。如果你定义了一个带参数的构造函数,编译器将不再自动产生一个不带参数的构造函数。这时,建议你写一个不带参数的构造函数,这样会避免出错,特别是这个类有子类的时候。
在上面一个例子中,我们并没有定义Circle的构造函数,系统自动为这个类分配了一个不带参数的构造函数,因此我们可以使用new Circle()
来构造一个Circle对象。出现在new 操作符后面的函数就是构造函数。
下面我们给Circle增加一个带参数的构造函数,这样可以在new 操作的时候就对圆的参数进行初始化;同时,我们也编写了一个不带参数的构造函数,在这个构造函数中,调用了带参数的构造函数来对圆的数据进行初始化。
class Circle {
float x;
float y;
float r;
public Circle() {
Circle(0, 0, 1);
}
public Circle(float x, float y, float r) {
setCircle(x, y, r);
}
public void printCircle() {
System.out.printf("The position x=%f, y=%f, radius=%f!\n", x, y, r);
}
public void setCircle(float x, float y, float r) {
this.x = x;
this.y = y;
this.r = r;
}
}
public class Circle_Test {
public static void main(String[] args) {
Circle circle = new Circle(1.1F, 2.2F, 3.3F); // 或者 Circle circle = new Circle();
circle.printCircle();
}
}
如果你写了一个带参数的构造函数,建议写一个不带参数的构造函数,这样会省掉很多麻烦,后面会讲述。关于构造函数,会在后面的学习中详细讲述。
注意,构造函数不能声明为私有的,不能声明为静态的,不能写返回参数!
2. 变量的缺省值
The default value of a data field is null for a reference type, 0 for a numeric type, false for a boolean type, and '\u0000' for a char type. However, Java assigns no default value to a local variable inside a method.
成员变量(类中定义的变量)的缺省值:
- 引用变量:缺省是 null;
- 基本数值类型(小写的 int、float、double):缺省是0;
- char:缺省是 '\u000';
- boolean:缺省是null;
数组中的变量缺省值遵循同样的标准
局部变量(函数中定义的变量)没有缺省值!因此如果不赋值进行操作可能会出现问题(编译错误)。
public class Test {
public static void main(String[] args) {
int x; // x has no default value
String y; // y has no default value
System.out.println("x is " + x);
System.out.println("y is " + y);
}
}
上述代码会产生编译错误。
3. 引用变量的赋值
前面学习的很多基本类型(Primitive),都是以小写字母开始的(int,float,char,boolean等),这是基本类型。而预定的很多类(Scanner,String)这些叫做引用类型(使用 new 关键字建立对象/实体)。基本类型在赋值和参数传递的时候使用值传递;引用类型使用引用(Reference)传递。引用传递类似C中的指针地址传递。
引用赋值(对象)和C的指针非常类似,只不过在Java中,不用关心指针悬空(未回收的内存空间)的问题,因为Java有一种叫做垃圾回收机制,可以自动清理没有引用的内存空间(如上图中赋值后o1的存储空间)。
引用类型有一个特殊值是null,对应C语言中的空指针。
Circle c1; // 缺省值是 null
c1 = new Circle(); // 引用变量指向一个对象
c1 = null; // 引用变量可以赋值成 null
if (c1 == null) { // 可以判断一个引用变量是否没有被赋值
...
}
4. 类的成员
在本章节以前,我们也看到过类,也有相应的变量和函数,但是大多是静态的。本章开始,如同Circle类一样,定义的变量、函数都没有static的修饰,叫做实体变量和实体函数(区别于动态)。实体类型的变量和函数只能在引用对象的变量上面进行调用,而不能在类上进行调用。
定义在类中的变量、常量、函数都叫做类的成员,根据这些成员的不同性质,总结一下:
- 常量成员:一定是静态的,且使用final进行修饰。对于常量来说,整个程序空间内只存在一份。
- 静态变量成员:前面的章节大多使用静态变量,一个静态变量在整个程序空间内只存在一份拷贝。静态变量可以理解成C的全局变量,只不过其包含在一个特定的类(盒子)当中。
- 静态函数成员:前面章节大多使用静态函数。静态函数不用和对象进行绑定,可以在类上面进行调用,例如前面的Math中的静态函数。当然,在对象的引用变量上调用该对象对应的类的静态函数也是合法的。
- 实例(instance)变量成员:实例变量成员和实例(对象)绑定,如同C语言结构体中的变量,每个实例对象的实例成员变量都可能有不同的值。
- 实例函数成员:只能在实例(对象)上面进行调用。
静态变量是全局共享的。实例变量是每个对象所独享的,有多个对象就存在多个不同成员变量。例如前面圆的半径,有多个不同的圆的对象,其半径都是不同的(因此叫做实例/对象变量)。
下面的例子中在CircleWithStaticMembers
这个类中定义了一个静态变量static int numberOfObjects = 0;
用来统计CircleWithStaticMembers
这个类一共被创建出了几个对象。
class CircleWithStaticMembers {
double radius;
static int numberOfObjects = 0;
static int getNumberOfObjects() {
return numberOfObjects;
}
CircleWithStaticMembers() {
radius = 1.0;
numberOfObjects++;
}
CircleWithStaticMembers(double newRadius) {
radius = newRadius;
numberOfObjects++;
}
double getArea() {
return radius * radius * Math.PI;
}
}
public class Test {
public static void main(String[] args) {
System.out.println("Before creating objects");
System.out.println("The number of Circle objects is " + CircleWithStaticMembers.getNumberOfObjects());
// Create c1
CircleWithStaticMembers c1 = new CircleWithStaticMembers();
// Display c1 BEFORE c2 is created
System.out.println("\nAfter creating c1");
System.out.println("c1: radius (" + c1.radius + ") and number of Circle objects ("
+ CircleWithStaticMembers.numberOfObjects + ")");
// Create c2
CircleWithStaticMembers c2 = new CircleWithStaticMembers(5);
// Modify c1
c1.radius = 9;
// Display c1 and c2 AFTER c2 was created
System.out.println("\nAfter creating c2 and modifying c1");
System.out.println("c1: radius (" + c1.radius + ") and number of Circle objects (" + c1.numberOfObjects + ")");
System.out.println(
"c2: radius (" + c2.radius + ") and number of Circle objects (" + c2.getNumberOfObjects() + ")");
}
}
- 静态变量
numberOfObjects
的初始值是0,因此在主函数的第二行,使用CircleWithStaticMembers.getNumberOfObjects()
取得这个静态变量的值的时候是0。这里证明了静态变量不依附对象存在,可以在类上面直接调用。 CircleWithStaticMembers
的两个构造函数(重载的构造函数)都对numberOfObjects
进行了加1操作,语义是当创建一个CircleWithStaticMembers’
对象的时候,让静态变量numberOfObjects
加1,这样就得到了创建CircleWithStaticMembers
对象的总数量。- 注意后面的代码,得到
CircleWithStaticMembers
类实例化对象的个数可以使用静态变量numberOfObjects
,也可以使用静态函数getNumberOfObjects()。
且这两个静态成员即可以在类上面调用,也可以在对象的引用变量上调用。
静态成员可以在类上访问,也可以在该类的引用变量上访问;这两种方式都是合法的,且没有任何的区别。
5. 访问修饰
对任何一个类成员(常量、变量、函数),有一个访问修饰,来说明该成员对与外部调用的可见性。在一个类的内部,好像是一个大家庭,所有成员相互都是可以调用的;但是对于外部来说,就需要进行一定的限制。限制外部对类成员的访问有很多好处,第一位的就是安全性。你肯定不希望一个人闯入你的家里指手画脚,对于类也是一样的。有些函数和变量是在类的内部使用的,而不需要外部来进行读写,这时就可以使用访问修饰符来限定该成员的访问级别。
成员访问修饰包括:private(私有);缺省;protected(保护);public(公开)。这里我们讨论除protected外的三种访问修饰,因为protected需要到类的继承才有用。
对于类成员来说,访问限制如下图:
上述的三个类都有包的定义,请注意第一行的package
定义。包的概念可以参考第一章,可以简单理解为目录,上述的C1和C2在一个包(p1)中;C3在包p2中。根据上述的可以总结为:
The private modifier restricts access to within a class, the default modifier restricts access to within a package, and the public modifier enables unrestricted access.
- private:该类的内部可以访问;
- 缺省(没有修饰):同一个包可以访问;
- public:任何外部均可访问。
对于类来说,一般是public,但是也可以使用缺省,但是一般不会是private。
类的访问限制和类成员是一致的。
一个Java源代码文件中,只能存在一个 public 类,且这个类与文件名相同;如果要定义其他的类,请使用缺省。这个在拼题平台中经常使用到。
5.1. 为什么要私有?
一个类当中可能存在一些私有成员,例如私有的函数或者是变量。对于私有的函数,我只想让类内部的其他函数调用,而不会让外部调用,这个比较好理解,可能是为了安全与保密。但是有些成员变量设置成私有就有点感觉不应该了。例如在Circle的这个例子中,最好把圆的半径设置成私有。这不是有点多此一举吗?我们不是希望外部的代码来读取和设置圆的半径吗?对我们希望外部代码读写圆的半径,但是不希望乱读写。圆的半径是不能为负数的,否则可能出现逻辑错误。如果我们把圆的半径公开,那么你就把保证圆半径这个责任交给了外部代码,这样做是非常危险的。
如果我们把圆的半径设置成私有,那么外部代码如何读写这个变量?想一想,在类的内部,所有成员都是公开的,那么我们设置两个函数,一个负责设置半径,一个负责读取半径,并把这两个函数设置成public不就行了?Java把这两个函数叫做getter和setter函数。看看下面的例子:
public class CircleWithPrivateDataFields {
/** The radius of the circle */
private double radius = 1;
/** The number of the objects created */
private static int numberOfObjects = 0;
/** Construct a circle with radius 1 */
CircleWithPrivateDataFields() {
numberOfObjects++;
}
/** Construct a circle with a specified radius */
public CircleWithPrivateDataFields(double newRadius) {
radius = newRadius;
numberOfObjects++;
}
/** Return radius */
public double getRadius() {
return radius;
}
/** Set a new radius */
public void setRadius(double newRadius) {
radius = (newRadius >= 0) ? newRadius : 0;
}
/** Return numberOfObjects */
public static int getNumberOfObjects() {
return numberOfObjects;
}
/** Return the area of this circle */
public double getArea() {
return radius * radius * Math.PI;
}
}
这样清楚了,radius
这个变量被设置成私有,getRadius()
是共有函数,用于读取半径;setRadius()
是共有函数,用于设置半径。这两个函数就是 getter 和 setter 函数。特别注意,在 setRadius
函数中对传入参数进行了判断。这个函数好像是看门人,保证半径永远不为复数。
因为getter和setter函数用得太多了,eclipse 和大多Java的IDE为这个函数提供了快速构建方式。你只需要选择菜单Source
-Generate Getters and Setters ...
就会出现下面的窗口。
选择你需要生成getter和setter的私有变量,然后点击Generate按钮,就会自动生成getter及或是setter函数。
6. 对象作为函数参数
值传递和引用传递
7. 对象数组
8. Immutable 类与对象
9. 变量作用域
10. This关键字
10.1. 构造函数重载
11. 本章重点
除了特别标注,都是重点