AP计算机科学A复习:Unit 5 – Class的操作

Unit 5 – Class的操作

这一单元介绍Class的写作语法和常见的应用场景。Class 可以被看作是一个很高级的容器,在Java编程中被用来承载 Method,Constructor 等等。这个单元的内容相对而言比较抽象,所以在学习的时候花费更长时间是正常的,不必因此焦虑。

U5.1 Anatomy of a Class 定义Class的语法

【基本语法】

只需要写出 class 关键字,然后定义一个名字,比如下面的hahaha就是这个class 的名字,和一组大括号,就完成了Class的声明。

class hahaha {
	//这是class hahaha的里面
}

【关键字】

– static 关键字

在以上的基本构型之外,还可以在 class 关键字之前加上其他的关键字。比如在 U2.3 已经介绍过的 static 关键字。被声明在method或constructor外,但是在 Class 里面的变量可以被这个 Class 里面所有的method或constructor访问,这些变量默认为 non-static,除非在定义时使用了 static 关键字或者所在的 method 本身就是 static method。

访问修饰符 access modifiers

如果想要改变访问范围,可以使用关键字 public 和 private 进行设置,它们被称为访问修饰符。

AP考试中,class不使用 private 关键字;没有使用关键字修饰的 class 可以被同一个 package 里面的class, field, 和method等访问;使用 public 关键字修饰的 class 不仅对自己所在的 package 里面的class, field, 和method开放了访问权限,还允许被其他的 package 里面的 class, field, 和method 访问。

写成代码验证一下以上几种情况,首先测试没有使用关键字修饰的情况:

class a {
	void outTest () {
		System.out.println("我没有使用关键字");
	}
	}

eg. 不使用 public 关键字

上面的 class a 没有被任何关键字修饰。理论上可以被同一个 package 里面的其他 class 访问,但不能被其他 package 里面的 class 访问。所以先试试在相同 package 里面的另一个 class访问 class a:

在同一个 package – testKeywords 的另外一个 class 里面,我访问了 class a 里面的 outTest method。最后成功输出了「我没有使用关键字」的语句。这表示在相同 package 里面的另一个 class访问 class a 是成功的。

然后试试从另一个 package 访问class a:

此时,因为访问范围的限制,就无法访问到 class a 了。报错。

然后我们来看使用 public 关键字的效果:

public class test {
	public static void outTest () {
		System.out.println("我使用了 public 关键字");
	}
}

eg. 使用 public 关键字

此处的 class test 被 public 关键字修饰,理论上就可以从其他 package 里面的 class 来访问这个 class。所以试一试:

成功输出了「我使用了 public 关键字」。这证明从外界的 package 访问 class test 是成功的。

注意,这里访问到 class test 之后,还需要有权限访问其下的 method,所以 main method也需要被 public 修饰。否则仍然会因为访问权限不足而报错:

所以,public 关键字的作用就是扩大 class, field, 和method 可以被访问的范围。相反,private 关键字就是缩小它们可以被访问的范围。

eg. private 关键字

在下面这段程序中,有两个class。我在名叫 test 的 class 里面声明了两个变量 a 和 b,并且赋予了他们各自的初值。变量 a 被 private 关键字修饰,而变量 b 没有。

当我们回到 Main class 中的 main method 开始运行程序时,电脑尝试在 Main class 中访问 a, b 两个变量。此时我们发现变量 a 无法被访问到,但是变量 b 可以被访问。这是因为 private 关键字将变量 a 的被访问范围限制在了它所在的 class 及以下的层级,如果从另一个 class 访问就超过了这个访问范围,因此访问失败。

相对的,变量 b 没有使用关键字,所以我们可以从相同 package 里面的所有 class 访问到 test class 里面的 b 变量。尝试在你的 eclipse 里面看看这段程序怎么报错。

public class Main {
	public static void main (String[] args) {
		test one = new test();
		System.out.println(one.a);//尝试输出 a
		System.out.println(one.b);//尝试输出 b
		}
}

class test {
	private int a = 1;
	int b = 2;
}

在编程时,为了保证数据安全应该尽可能的隐藏好隐私数据。这样相当于把数据放进一个封闭的盒子里,只有通过特定的 method 才可以存取这些数据。这些特定的 method 被称为「accessor method」和「mutator method」:

我们将在U5.4, U5.5介绍这些 method 的写法。

U5.2 Constructors 构造器

Constructor 的写作语法已经在U2.2 介绍过,建议先行回顾。

作为一种特别的 method,constructors 也拥有 parameter list ,用来为初始化 instance variable 提供数据。

Instance variable 是依附于 object 的,因此是 non-static variable。如果没有使用 constructor 在创建 object 的时候赋予 instance variable 初值,则对应的变量被赋默认的初始值:int 类型默认值为 0;double 类型默认值为 0.0;boolean 类型默认值为 false。

尝试编写一个class:有三个 instance variable,分别为姓名、年龄、班级。然后编写一个 constructor 用来在创建 object 的时候对这些 instance variable 赋初始值。

U5.3 Documentation with Comments 编写注释

编写注释可以让自己的程序有更强的可读性。程序编译时会自动忽略注释内容,所以写什么奇奇怪怪的内容都不会导致编译出现问题。注释只是提供给人类阅读,方便自己和别人更好的理解程序的内容和逻辑而免受「我也忘了我这里写的是啥」的痛苦。

/**
 * 
 * @author oscarleung
 * 这是一个示例程序
 *
 */

/*
 *  我们用 Main class 来输出一段文字
 */
public class Main {
	public static void main (String[] args) {
		System.out.print("这是一段文字");//这是一个输出语句
		}
}

上面的示例程序展示了 Java 编程中的三种注释编写格式。以「 /**」开始,「*/」结束的注释一般用来介绍整个程序的功能、特性,这种注释可以跨越多行;以「 /*」开始,「*/」结束的注释一般用来介绍程序中某个部分的功能、特性,比如 class 或者 method,这种注释也可以跨越多行;「//」开头的注释不能跨越多行,跨行自动结束,一般用于介绍某个变量、细节的功能、特性。

U5.4 Accessor Methods

我们通过一个例子来理解 accessor method。假设我们有一个 class 用来管理游戏账户,其中有两个 private data:id(账户),以及 level(游戏等级)。初始值分别为 admin 和 123456:

public class Account {
	private String id = "admin";
	private String level = "123456";
}

我们已经知道 private 关键字的作用,因此在这里并不能从其他的 class 访问这两个 private data。但是,在很多场景下从其他 class 访问账户和游戏等级这一类数据是非常必要的。那么 accessor method 的作用就是在保护数据隐秘的同时从外界获取到 private data。这样写:

public class Account {
	private String id = "admin";
	private String level = "123456";
	
	public String getId () {
		return this.id;
	}
}

上面的 「getId」 method 就是我们用来从外界访问 id 这个 private data 用到的 accessor method。这就相当于在 Account class 的里面放置了一个间谍,有需要的时候调用 「getId」,让它帮忙获取private data 再传出,就实现了从其他 class 获取 Account class 里面的 private data的操作。比如我们从 Main class 调用「getId」输出 id 的值:

这样,变量 id 虽然是位于 Account class 里面的 private data,但也可以通过 accessor method 被外界的其他 class 访问了。

U5.5 Mutator Methods

前面刚刚了解的 accessor methods 可以用来读取 private data,我们在某些情况下还可能需要对 private data 进行修改

从 private data 本身所在的 class 以外的其他 class 对这个 private data 进行修改就需要用到 「mutator methods」。

假设有两个 class,分别为 Main 和 Test。在 Test class 内部有一个 private data,用于存储学号。此时如果要在 Main class 里面对学号的数据进行修改,就必须通过 mutator method 完成。这是因为存储学号的变量被 private 关键字修饰后就不再对外界的 class 开放访问权限了。

//这里是 Main class
public class Main {
	public static void main (String[] args) {
		Test a = new Test();
		int in = new Scanner(System.in).nextInt();//输入学号的新数据
		a.setStudentID(in);//把新的学号数据传入 mutator method
	}
}
//这里是 Test class
public class Test {
	private int studentID = 1001;
	
	public void setStudentID (int newID) {
		System.out.println("原来的值是" + this.studentID);
		this.studentID = newID;
		System.out.println("修改后的值是" + this.studentID);
	}
}

Test class 里面的 setStudentID 就是一个 mutator method 的例子。相对于习惯使用 「get + 变量名」来命名的 accessor method ,这里的 mutator method 一般使用 「set + 变量名」来作为 method 的名字。例如这里要修改的变量是 studentID,所以它的 mutator method 就叫 setStudentID 了。这样做是增加程序的可读性,即使别人来阅读这段代码也可以立刻明白这一个 method 是干嘛的。

这个例子里面的 mutator method 中有两个输出语句用来在这个例子里面展示变量的值前后发生的变化,你不需要在自己的写程序时添加这两个输出语句。这两个输出语句中间的赋值语句才是实际上 mutator methods 发挥作用的语句。

mutator methods 一般不具有返回值,所以声明 methods 时返回值的部分要写作void,它的任务只是接受一个新的数据然后把这个数据赋值给对应的 private data。这个新数据通过 parameter 传递,所以你可以在上面的例子里看到 setStudentID 的 parameter list 中有一个叫做 newID 的变量。

例子中,我在 main method 里面输入了一个新的整数,把它存入了变量 in,随后就在 Main class 里面调用 mutator method 。这时候变量 in 存储的数据会传递到位于mutator method 变量 newID,最后由赋值语句完成 studentID 的数据修改。

U5.6 Writing Methods 方法的写作

在 method 之间,数据的传递有不同形式。在U2.2已经简单介绍了 primitive data 作为参数被传递时的特性。对于 primitive data,被作为参数传递的同时会直接把原数据复制存储到目的地的一个 local variable,这个local variable 被称为 formal parameter 形式参数。

但是,如果被传递的数据不是 primitive data 而是 reference data 就需要注意一个微小的差异。这个差异是,当 reference data 作为参数被传递时,最终送到 formal parameter 的并不是复制品,是 reference data 在内存中的「地址」。

因此,如果在这样的情况下对 formal parameter 的变量进行修改就会影响到原数据。因为我们发出修改指令的时候电脑就会通过 formal parameter 存储的「地址」找到数据存储的位置并直接对原数据进行修改,而不是对复制品进行更改。在你的 eclipse 里面运行并观察下面的例子。

public class Main {
	public static void main(String[] args) {
	showRef t = new showRef(2333, 666);
	System.out.println("刚赋完初值"+t.getA() +" "+ t.getB());
	Main.test(t);//传递变量,在test method里修改 formal parameter
	System.out.println("原数据也被改变"+t.getA() +" "+ t.getB());
	}
	
	static void test (showRef example) {
		System.out.println("收到传入数据"+example.getA() +" "+ example.getB());
		example.setA(0);
		example.setB(0);
		System.out.println("完成传入数据的修改"+example.getA() +" "+ example.getB());
	}
}

class showRef {
	private int a,b;
	
	showRef(int x, int y){//赋初值
		a = x;
		b = y;
	}
	void setA (int i) {
		this.a = i;
	}
	void setB (int j) {
		this.b = j;
	}
	int getA () {
		return this.a;
	}
	int getB () {
		return this.b;
	}
}

我向名叫 t 的 showRef instance 传入了两个初始值,分别是 2333 和 666。t 的数据类型是showRef 所以自然就是一个 reference data。然后把 instance t 的数据传递给了位于 test method 的formal parameter「example」。

再然后对 example 里面的 object a 和 b 进行修改,这时输出发现原来 instance t 存储的数据也在此时被修改了。

所以简单的记住,传递参数时如果 primitive data 作为原始数据被传,则对 formal parameter 的修改不影响原变量;如果 reference data 作为原数据被传,则对 formal parameter 进行修改将会直接改变原变量。

U5.7 Static Variables and Methods

所谓的 Static Variables and Methods 用肉眼看就是在声明变量前或者定义 method 前加上 static 关键字。被「static」keyword 修饰的Variables and Methods不需要创建 instance 就可以使用。但是加了 static 关键字和不加对程序的后续写作有什么样的影响呢?

【Static Variables】

Static Variables 依附于 class,而不是依附于 instance 或者 object。所以,同一个 class 下面的所有的 instances 将会共用 static variables 的数据。

比如,在下面这段程序中我声明了两个变量,a 和 b。其中 a 被 static 关键字修饰,而 b 没有。随后新建两个 instance,分别名叫 test1 和 test2。然后试图把 instance test1 的 a 变量修改成 4321,再把 instance test2 的 a 变量修改成 1234。

使用输出语句分别输出就会发现,test1.a 和 test2.a 的输出值都是1234。这个程序中我们先对 test1.a 赋值 4321,再对 test2.a 赋值1234,但是变量 a 是 static variable,所有的 instances 事实上共用的是一个数据存储空间,因此两次赋值实际上是在对同一个数据进行修改,第一次赋值语句把这个数据被修改为4321,第二次赋值语句运行时又将这个数据修改为1234,所以输出时变量 a 的数据为 1234。第一个输出语句的结果就成了 「1234 1234」而不是「4321 1234」。

相比之下,变量 b 并没有被 static 关键字修饰。因此变量 b 是一个 non-static 的变量,新建的每一个instance 都会有下属的独立的 instance variale b。这样的话,我们对 test1 的 b 和 test2 的 b 分别进行修改就不会相互影响了。

因此,下面做这段程序第二个输出语句给出的结果是「6789 9876」。

public class Main {
	static int a = 233;
	int b = 666;
	public static void main(String[] args) {
		Main test1 = new Main();
		Main test2 = new Main();
		test1.a = 4321;
		test2.a = 1234;
		
		test1.b = 6789;
		test2.b = 9876;

		System.out.println(test1.a+ " " + test2.a);
		System.out.println(test1.b+ " " + test2.b);
	}
}

注意,根据AP CSA考试的要求,要直接使用 class name 来调用static variable,而不是新建一个 object 然后使用 object name 调用。上面的例子只是方便你理解 static variable 的存储空间被所有instances 共用。考试中按要求写才可以:

public class Main {
	static int a = 233;
	public static void main(String[] args) {
		Main test1 = new Main();
		Main test2 = new Main();
		test1.a = 4321;//AP 考试不用object 的名称调用 static variable
		test2.a = 1234;//AP 考试不用object 的名称调用 static variable
		Main.a = 666;////AP 考试用class 的名称调用 static variable,这才是对的
		System.out.println(test1.a+ " " + test2.a + " " + Main.a);
	}
}

【Static Methods】

Static Methods 也直接依附于 class,而不是 class 的 instance。static methods 无法访问 non-static variable;但 static variable 却可以被 non-static variable 和 static variable 访问。

以及,因为 static methods 不属于任何 instance,所以也不能使用 this 关键字(后面会详细解释)来表示「对当前正在使用的 instance 进行操作」。直接使用 class name + method name即可,例如下面的「 Main.test( ) 」。

观察这一段程序:

在左边的图中,test method 是一个 non-static method,运行它会输出一段文字。但是在 main method 里调用 test method 就会报错。因为 main method 是一个 static method,而static method 不能访问 non-static method。

而在右边的图中,test method 被修改为了 static method。此时就可以正常在 main method 里面调用了。这是因为 static method 可以被 static method 访问。

U5.8 Scope and Access 变量的有效范围

我们已经学习了各种语法,你可能已经发现了不同的变量有不同的有效范围。例如,for 循环括号里定义的计数变量就只能在循环语句的内部被访问、修改。所以这个变量的访问范围就是它所在的 for 循环语句之内:

	public static void main(String[] args) {
		for(int i = 1; i <= 6; i++) {
			System.out.println(i);
			i = 7;
		}
		System.out.println(i);
	}

在上面这段代码中,我尝试在 for 循环的外面访问变量 i 。你可以用自己的 eclipse 尝试一下。结果会报错,因为第二个输出语句已经在 for 循环之外,超出了变量 i 的有效范围,无法成功访问。所以要么删除这个输出语句,要么在 for 循环开始前就对变量 i 进行声明:

	public static void main(String[] args) {
		int i = 1;
		for(; i <= 6; i++) {
			System.out.println(i);
			i = 7;
		}
		System.out.println(i);
	}

这样就可以成功输出 i 在循环前后的数值了。因为对于现在定义在 main method 里面的变量 i 来说,它的访问范围是 main method。所以在这个 method 里的任何位置都可以访问它。

同样的,你可以推测一下 formal parameter 的有效范围是什么?

public class Main {
	public static void main(String[] args) {
		Main para = new Main();
		para.test("你好呀");
	}
	
	public void test (String a) {
		System.out.println(a);
	}
}

在上面的程序中有两个 method。我在 test method 里面声明的 formal parameter 是一个字符串变量 a 。其实对于formal parameter,在这里就是 String a ,来说,和一个被定义在它所在的 test method 里的其他变量没有区别 —— 他们的访问范围都是所在的 method 之内。也就是说,在这里 test method 里面的任何地方都可以访问到变量 a。但是一旦离开了 test method 的大括号,就无法接触到变量 a 了。比如:

public class Main {
	public static void main(String[] args) {
		Main para = new Main();
		para.test("你好呀");
	}
	
	public void test (String a) {
		System.out.println(a);
	}
	
	public void anotherMethod () {
		System.out.println(a);//不能在这里访问 a 
	}
}

最后的 anotherMethod 里面的输出语句就无法访问到变量 a。同理,定义在 class 里的变量的访问范围就是它所在的 class;定义在 constructor 里的变量的访问范围就是它所在的 constructor……

U5.9 this Keyword

this 关键字指代的就是当前正在操作的 object。既然指的是 object,那么 this 关键字自然只能对 non-static method 使用。this 关键字还可以与 dot operator 一起使用,这样就可以访问正在操作的 object 下属的其他数据。比如:

public class Main {
	int a;
	public static void main(String[] args) {
		Main x = new Main();
		x.a = 2;
		Main y = new Main();
		y.a = 6;
		
		System.out.println(x.threeTimes()+ " "+ y.threeTimes());
	}
	
	public int threeTimes () {
		return (this.a * 3);
	}
}

在上面这个程序中,有一个 class Main。Main class 包含一个整数数据 a。然后我创建了两个 Main 的实例,分别取名为 x 和 y。x 和 y 下属的 a 变量被分别赋值为 2 和 6。接着,我调用了「threeTimes」这个 method 来把 x 和 y 下属的 a 变量各自变为原来的三倍。

可以看到,在 threeTimes method 里面我并没有直接写「乘三」的到底是instance x 的 a 变量还是instance y 的 a 变量。我简单的使用了「this.a 」的写法表达了「属于正在操作的 instance 的 a 变量」。

因为在调用方法时,输出语句中的「x.threeTimes() 」这一个语句就已经明确了被操作的 instance 是x,此时this 指代的就是 instance x。所以 x 的 a 变量乘三,得到结果 6.

而当「y.threeTimes() 」被执行的时候,this 指代的就是instance y 了。所以 y 的 a 变量乘三,得到结果 18。在你的 eclipse运行一下上面的这段程序,看看结果是否符合预期。

U5.10 Ethical and Social Implications of Computing Systems 计算机伦理

计算机系统及相关领域的发展对社会和人类文明产生了深远的影响。例如,分布在世界各地的无数服务器现今存储着海量的政府数据、个人隐私数据等等。

但是,计算机系统的可靠性是有限的。衍生的各类法律问题、知识产权问题仍被热议。作为编程者的我们一直在尽量优化程序写作的逻辑和算法,期望给使用者最好的体验,尽量减少包括但不限于数据泄露、程序不可用这类问题带来的损失。更要考虑自己所编写的程序给社会、经济、文化多方面带来的影响。

总结

本单元介绍了 Class 写作的基本语法。然后介绍了访问、修改 private data 的 method 的常用写法;复习了method 间的参数传递的细节;了解了变量的有效访问范围等。第五单元的内容和第二单元关联较大。如果有时间可以把这两个单元连起来再看一遍,它们都是有关于「面向对象编程」的重要内容。

练习

2008-8

2009-9

2009-10

by Oscar.L
E-mail [email protected]
No Comments

Send Comment Edit Comment

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
Previous
Next