You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

27 KiB

1. 初探类与对象

1.1. 现实生活中类和对象的关系

我们首先看看日常生活中的一些例子,假如我们要做一辆模型汽车,我们需要有哪些步骤? 首先我们需要设计,可能你需要画图纸,进行一些标注和说明。当设计完成后,就可以准备材料,按照图纸进行制作了。如果你愿意,你可以按照图纸再做几辆模型车,可能会改变颜色,不同的涂装,大一点的只需要按照比例放大就可以了。

Alt text

我们制作出来的所有模型车都是按照图纸的因此具备设计中所包含的所有特性。假如我们把这个设计叫做A100那么所有成品模型车都是一类车Class每一辆模型车都是A100的一个实例Object 可能有些属性不一样,例如颜色)。

好了这就是类与对象的基本内涵。类是一个模板抽象的描述对象是模板的一个实例具体的。这一点有点类似C中结构体的定义与结构体变量的关系定义是抽象描述变量是实体。

我们再深入讨论一下对象。对象Object这个概念广泛存在于我们的生活当中一般只一个独立的个体具备一定的特性properties并有一定的能力capability。例如一辆A100

Alt text

1.2. 程序空间中类与对象的关系

请思考在我们学习的C语言中有没有抽象描述和具体实例化的应用例如在C语言中如果描述一个平面中的圆

Alt text

考虑到已经学习过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这好像是从图纸到成品的过程。

Alt text

上面只是生成了一个圆的数据存储空间,描述了圆的属性。但是一个完整的圆是不是应该还有行为能力?例如我们希望打印这个圆的基本信息,应该如何做?

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.2F2.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() {
        this(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();
    }
}

A class may be defined without constructors. In this case, a no-arg constructor with an empty body is implicitly defined in the class. This constructor, called a default constructor, is provided automatically only if no constructors are explicitly defined in the class.

如果你写了一个带参数的构造函数,建议写一个不带参数的构造函数,这样会省掉很多麻烦,后面会讲述。关于构造函数,会在后面的学习中详细讲述。

注意,构造函数一般不声明为私有,不能声明为静态的,不能写返回值的类型!

其他的一些例子:

  1. TestSimpleCircle
  2. TestTv

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.  

成员变量(类中定义的变量)的缺省值:

  1. 引用变量:缺省是 null
  2. 基本数值类型(小写的 int、float、double缺省是0
  3. char缺省是 '\u000'
  4. boolean缺省是false

数组中的变量缺省值遵循同样的标准

局部变量(函数中定义的变量)没有缺省值!因此如果不赋值进行操作可能会出现问题(编译错误)。

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都是以小写字母开始的intfloatcharboolean等这是基本类型。而预定的很多类ScannerString这些叫做引用类型使用 new 关键字建立对象/实体。基本类型在赋值和参数传递的时候使用值传递引用类型使用引用Reference传递。引用传递类似C中的指针地址传递。

赋值.drawio

引用赋值对象和C的指针非常类似只不过在Java中不用关心指针悬空未回收的内存空间的问题因为Java有一种叫做垃圾回收机制可以自动清理没有引用的内存空间如上图中赋值后o1的存储空间

引用类型有一个特殊值是null对应C语言中的空指针。

Circle c1;                // 缺省值是 null
c1 = new Circle();        // 引用变量指向一个对象
c1 = null;                // 引用变量可以赋值成 null
if (c1 == null) {         // 可以判断一个引用变量是否没有被赋值
  ...
}

SDK中的一些类

  1. The Random Class
  2. The Point2D Class

4. 类的成员

在本章节以前我们也看到过类也有相应的变量和函数但是大多是静态的。本章开始如同Circle类一样定义的变量、函数都没有static的修饰叫做实体变量和实体函数区别于动态。实体类型的变量和函数只能在引用对象的变量上面进行调用而不能在类上进行调用。

定义在类中的变量、常量、函数都叫做类的成员,根据这些成员的不同性质,总结一下:

  1. 常量成员一定是静态的且使用final进行修饰。对于常量来说整个程序空间内只存在一份。
  2. 静态变量成员前面的章节大多使用静态变量一个静态变量在整个程序空间内只存在一份拷贝。静态变量可以理解成C的全局变量只不过其包含在一个特定的类盒子当中。
  3. 静态函数成员前面章节大多使用静态函数。静态函数不用和对象进行绑定可以在类上面进行调用例如前面的Math中的静态函数。当然在对象的引用变量上调用该对象对应的类的静态函数也是合法的。
  4. 实例instance变量成员实例变量成员和实例对象绑定如同C语言结构体中的变量每个实例对象的实例成员变量都可能有不同的值。
  5. 实例函数成员:只能在实例(对象)上面进行调用。

静态变量是全局共享的。实例变量是每个对象所独享的,有多个对象就存在多个不同成员变量。例如前面圆的半径,有多个不同的圆的对象,其半径都是不同的(因此叫做实例/对象变量)。

下面的例子中在CircleWithStaticMembers这个类中定义了一个静态变量static int numberOfObjects = 0;用来统计CircleWithStaticMembers这个类一共被创建出了几个对象。

image-20250312141800266

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() + ")");
	}

}
  1. 静态变量numberOfObjects的初始值是0因此在主函数的第二行使用CircleWithStaticMembers.getNumberOfObjects()取得这个静态变量的值的时候是0。这里证明了静态变量不依附对象存在可以在类上面直接调用。
  2. CircleWithStaticMembers的两个构造函数(重载的构造函数)都对numberOfObjects进行了加1操作语义是当创建一个CircleWithStaticMembers对象的时候,让静态变量numberOfObjects加1这样就得到了创建CircleWithStaticMembers对象的总数量。
  3. 注意后面的代码,得到CircleWithStaticMembers类实例化对象的个数可以使用静态变量numberOfObjects,也可以使用静态函数getNumberOfObjects()。且这两个静态成员即可以在类上面调用,也可以在对象的引用变量上调用。

静态成员可以在类上访问,也可以在该类的引用变量上访问;这两种方式都是合法的,且没有任何的区别。

5. 访问修饰

对任何一个类成员(常量、变量、函数),有一个访问修饰,来说明该成员对与外部调用的可见性。在一个类的内部,好像是一个大家庭,所有成员相互都是可以调用的;但是对于外部来说,就需要进行一定的限制。限制外部对类成员的访问有很多好处,第一位的就是安全性。你肯定不希望一个人闯入你的家里指手画脚,对于类也是一样的。有些函数和变量是在类的内部使用的,而不需要外部来进行读写,这时就可以使用访问修饰符来限定该成员的访问级别。

成员访问修饰包括private私有缺省protected保护public公开。这里我们讨论除protected外的三种访问修饰因为protected需要到类的继承才有用。

对于类成员来说,访问限制如下图:

image-20230305160811305

上述的三个类都有包的定义,请注意第一行的package定义。包的概念可以参考第一章可以简单理解为目录上述的C1和C2在一个包p1C3在包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.

  1. private该类的内部可以访问
  2. 缺省(没有修饰):同一个包可以访问;
  3. public任何外部均可访问。

对于类来说一般是public但是也可以使用缺省但是一般不会是private。

image-20230305210652299

类的访问限制和类成员是一致的。

一个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 ...就会出现下面的窗口。

image-20230305213614501

选择你需要生成getter和setter的私有变量然后点击Generate按钮就会自动生成getter及或是setter函数。

6. 对象作为函数参数

如果是基本类型的变量那么使用的是值传递上一章已经说明了。如果是引用类型大写的类型对象类型那么使用的是引用传递。引用传递如同C语言的指针一样我们可以在函数的内部改变引用对象内部成员的值。

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++;
	}

	// get radius
	public double getRadius() {
		return radius;
	}

	// set radius
	public void setRadius(double radius) {
		this.radius = radius;
	}

	/** Return numberOfObjects */
	public static int getNumberOfObjects() {
		return numberOfObjects;
	}

	/** Return the area of this circle */
	public double getArea() {
		return radius * radius * Math.PI;
	}
}

public class TestPassObject {
	/** Main method */
	public static void main(String[] args) {
		// Create a Circle object with radius 1
		CircleWithPrivateDataFields myCircle = new CircleWithPrivateDataFields(1);

		// Print areas for radius 1, 2, 3, 4, and 5.
		int n = 5;
		printAreas(myCircle, n);

		// See myCircle.radius and times
		System.out.println("\n" + "Radius is " + myCircle.getRadius());
		System.out.println("n is " + n);
	}

	/** Print a table of areas for radius */
	public static void printAreas(CircleWithPrivateDataFields c, int times) {
		System.out.println("Radius \t\tArea");
		while (times >= 1) {
			System.out.println(c.getRadius() + "\t\t" + c.getArea());
			c.setRadius(c.getRadius() + 1);
			times--;
		}
	}
}

printAreas函数接受两个参数,第一个参数是CircleWithPrivateDataFields类型的引用变量,第二个参数是循环的次数;主函数先生成一个CircleWithPrivateDataFields对象,然后使用printAreas函数;在printAreas函数内部进行了5次循环每次通过CircleWithPrivateDataFieldssetRadius函数使得CircleWithPrivateDataFields对象的半径+1然后再打印CircleWithPrivateDataFields对象的面积;当printAreas函数执行完成后,我们在主函数中打印CircleWithPrivateDataFields对象c的半径发现半径已经变成了6初始值是15次累加后变成6

这个例子证明如果是引用变量在函数内部可以改变传入对象的成员变量值其实这里使用的是setter函数改变半径的因为半径是私有的如果半径是public的使用直接赋值也可以改变。

如果我么在printAreas函数执行完成后打印n这个变量作为参数times传入printAreas函数会发现这个值还是5因为基本类型的变量使用的是值传递。

7. 对象数组

上一章我们学了数组,不过数组中元素的类型是基本类型。其实数组也是一个特殊的类(其变量也是引用变量),那么我们也可以在函数内部改变数组元素的值,无论数组内部是基本类型还是引用类型。

数组元素是基本类型:

public class Test {

	public static void main(String[] args) {
		int a[] = { 1, 2, 3, 4, 5 };
		System.out.printf("Before run changeArray, a[0] is %d\n", a[0]);
		changeArray(a);
		System.out.printf("After run changeArray, a[0] is %d\n", a[0]);

	}

	static void changeArray(int a[]) {
		a[0] = 1000;
	}

}

运行后我们发现a[0]的值已经被修改成了1000这种例子前面也遇到过。

下面看看数组中的元素是引用类型的情况,借用上个例子的CircleWithPrivateDataFields类型。

image-20250312141956130

public class TotalArea {
	/** Main method */
	public static void main(String[] args) {
		// Declare circleArray
		CircleWithPrivateDataFields[] circleArray;

		// Create circleArray
		circleArray = createCircleArray();

		// Print circleArray and total areas of the circles
		printCircleArray(circleArray);
	}

	/** Create an array of Circle objects */
	public static CircleWithPrivateDataFields[] createCircleArray() {
		CircleWithPrivateDataFields[] circleArray = new CircleWithPrivateDataFields[5];

		for (int i = 0; i < circleArray.length; i++) {
			circleArray[i] = new CircleWithPrivateDataFields(Math.random() * 100);
		}

		// Return Circle array
		return circleArray;
	}

	/** Print an array of circles and their total area */
	public static void printCircleArray(CircleWithPrivateDataFields[] circleArray) {
		System.out.printf("%-30s%-15s\n", "Radius", "Area");

		for (int i = 0; i < circleArray.length; i++) {
			System.out.printf("%-30f%-15f\n", circleArray[i].getRadius(), circleArray[i].getArea());
		}

		System.out.println("-----------------------------------------");

		// Compute and display the result
		System.out.printf("%-30s%-15f\n", "The total areas of circles is", sum(circleArray));
	}

	/** Add circle areas */
	public static double sum(CircleWithPrivateDataFields[] circleArray) {
		// Initialize sum
		double sum = 0;

		// Add areas to sum
		for (int i = 0; i < circleArray.length; i++)
			sum += circleArray[i].getArea();

		return sum;
	}
}
  1. 数组可以作为函数的返回值,例如createCircleArray函数生产了5个元素的数组其半径都随机的。
  2. CircleWithPrivateDataFieldssum函数都是数组作为参数。

你可以在函数中改变数组中引用对象内部成员的值,如同对象作为函数参数中一样。

8. Immutable 类与对象(了解)

If the contents of an object cannot be changed once the object is created, the object is called an immutable不变的 object and its class is called an immutable class. If you delete the set method in the Circle class in Listing 8.10, the class would be immutable because radius is private and cannot be changed without a set method.

这个概念需要了解我们目前使用到的Immutable类是Spring下一章中所有的 warp 类型也是 Immutable。

9. 变量作用域

The scope of instance and static variables is the entire class. They can be declared anywhere inside a class.

The scope of a local variable starts from its declaration and continues to the end of the block that contains the variable. A local variable must be initialized explicitly before it can be used.

10. This关键字

The this keyword is the name of a reference that refers to an object itself. One common use of the this keyword is reference a classs hidden data fields. Another common use of the this keyword to enable a constructor to invoke another constructor of the same class.

public class Circle {
	public double radius;

	public Circle() {
		this(1.0);
	}

	public Circle(double radius) {
		this.radius = radius;
	}
}

上个例子有两个构造函数这是构造函数的overload。在public Circle(double radius) 这个构造函数中,因为参数列表中有一个radius,这时在构造函数内部如果使用radius这个变量,则是指该函数参数列表中的radius变量。那么类Circle中的radius变量就无法直接访问了。因此使用this关键字代表本对象,那么this.radius就代表Circle的成员变量radius。最后this.radius = radius;就没有歧义了。

不带参数的构造函数public Circle()可以调用其他构造函数这里用this(1.0),表示调用该类中带参数的构造函数。

11. 本章重点

除了特别标注,都是重点