滚动翻页
Computer Science A · AP Computer Science A
值、引用,以及使用它们的方法
变量是内存中一个有名字的盒子。基本类型变量保存它的值;引用变量保存对象的位置。
int score = 42; int copy = score; // copy 得到自己的 42 copy = 7; // score 仍然是 42 String name = "Ada"; // 一个引用 String other = name; // 同一个引用
图中运行了这几行代码。copy = score 把值 42 复制到第二个盒子里,所以当 copy 变成 7 时,score 不受影响。name 里根本没有字符:它的盒子里是一支(红色的)箭头,指向内存别处创建的 String 对象。other = name 复制的是箭头,而不是对象:两支箭头,一个对象。
这个区别贯穿整个 Java,也贯穿本章:变量里保存什么,方法接收什么、返回什么,new 创建出什么,以及为什么 String 一旦创建就不会改变。
本章是 AP 计算机科学 A 的第 1 单元,使用对象和方法,占选择题得分的 15–25%:编译与错误(1.1),类型、表达式与赋值(1.2–1.6),库、文档与方法(1.7–1.11),对象及其创建(1.12–1.14)以及 String(1.15)。考试的 42 道选择题附有 Java Quick Reference,其中列出了考试可能用到的每个库方法;第一道自由回答题要调用 String 方法。
以及三种错误
算法是完成一项任务或解决一个问题的分步过程。它可以用文字写出,画成图,或写成程序。
顺序结构把步骤按先后排列,它们一次执行一步:除非有东西改变执行流程,Java 方法会自上而下执行其中的语句。
代码可以用任何文本编辑器编写,但大多数程序员使用集成开发环境(IDE),它提供编写、编译和运行代码的工具。编译器检查源代码中的某些错误并进行翻译;Java 的编译器生成字节码,由 Java 虚拟机(JVM)运行。编译器发现的错误必须修正,程序才能运行。
图中让三个程序沿着生产线通过,每个程序有一种错误。
int total = 7 + 3 // 缺少 ; System.out.println(total); 语法错误违反了语言的规则。编译器能发现它(error: ';' expected),程序不会运行。
int count = 0; System.out.println(10 / count);
运行时错误发生在程序运行期间,通常会使程序异常终止。这个错误是一个异常:编译器没有检测到的错误,它会打断正常的执行流程(ArithmeticException: / by zero)。
double avg = (7 + 8) / 2; System.out.println(avg); // prints 7.0 逻辑错误能通过编译并运行,但行为不正确:7 and 8 的平均值应该是 7.5。只有用特定数据测试并与预期结果比较,才能发现它。(它为什么输出 7.0:见第 1.3 页。)
int、double、boolean 和引用
数据类型是一组值以及对这些值的操作。每种类型要么是基本类型,要么是引用类型。
本课程用到三种基本类型,用于数字和布尔值:
| type | values | examples |
|---|---|---|
int | integers | 7, -40, 0 |
double | 实数 | 2.5, 7.0, -0.01 |
boolean | true or false | true |
Java 还有另外五种基本类型(long、short、byte、float、char);它们不在课程和考试范围内。凡不是基本类型的,例如 String,都是对象,其类型是引用类型。
变量是保存一个值的存储位置,该值可以在程序运行期间改变。每个变量都有名称和类型,基本类型的变量保存该类型的值。
int n = 7; double d = 2.5; boolean ok = true; double w = 7; // stores 7.0 int m = 2.5; // 无法编译
int 可以放进 double:double w = 7; 存入 7.0。double 放不进 int,所以 int m = 2.5; 是编译错误:incompatible types: possible lossy conversion from double to int。
根据值可能的范围选择类型:学生人数是 int,价格或平均值是 double,灯是否亮着是 boolean。
print、println 和转义序列
System.out.print 和 System.out.println 在屏幕上显示信息。println 之后光标移到新的一行;print 则让光标留在原处。
字面量是固定值在代码中的表示:7、2.5、true。字符串字面量是用双引号括起来的字符序列:"CSA"。
有些字符不能直接输入在引号内。转义序列以反斜杠开头,具有特殊含义。本课程用到三个:
| 转义序列 | means |
|---|---|
\" | 双引号 |
\\ | 反斜杠 |
\n | 换行 |
System.out.print("AP "); System.out.println("CSA"); System.out.println("say \"hi\""); System.out.print("a\\b\nc"); 随后控制台显示(图中会逐字打出):
AP CSA say "hi" a\b c 第一个 print 让光标停在 AP 之后,所以 CSA 落在同一行。在最后一行中,\\ 输出一个反斜杠,\n 在字符串中间开始新的一行。
int 除法、余数和优先级
算术表达式把数值、变量与运算符 +、-、*、/ 和 % 组合在一起。两个 int 值运算得到 int;至少有一个 double 则得到 double。
System.out.println(17 / 5); // 3 System.out.println(17 % 5); // 2 System.out.println(17.0 / 5); // 3.4
两个 int 相除只保留商的整数部分:17 个方块可以分成 3 个整组,每组 5 个。取余运算符 % 给出剩下的部分:2。除法中有 double 时,结果就是商本身,3.4。(% 在本课程中只用于 a ≥ 0 且 b > 0 的情形。)
优先级:*、/ 和 % 先于 + 和 - 运算;优先级相同的运算符从左到右进行;括号会改变组合方式。编译器在编译时就按这种方式把值和它们的运算符组合起来。
| expression | value |
|---|---|
2 + 3 * 4 | 14 |
(2 + 3) * 4 | 20 |
10 - 4 - 3 | 3 |
7 + 9 % 4 | 8 |
1 / 2 * 4.0 | 0.0 |
1.0 / 2 * 4 | 2.0 |
在 1 / 2 * 4.0 中,1 / 2 是 int 除法,在 double 出现之前就已经是 0。
用 int 类型的零去除一个 int,在该行运行时会抛出 ArithmeticException。(用 double 除以零,以及由此产生的特殊 double 值,不在课程范围内。)
右边的值,存入左边
赋值运算符 = 把右边的表达式计算成一个值,存入左边的变量。
变量在表达式中使用之前必须先有值,并且该值的类型必须兼容。第一次赋值称为初始化。之后每次赋值都会替换原来的值:旧值就消失了。
int a = 3; // a 3 int b = 8; // a 3, b 8 int temp = a; // a 3, b 8, temp 3 a = b; // a 8, b 8, temp 3 b = temp; // a 8, b 3, temp 3
所以交换两个变量需要第三个变量。没有 temp,a = b 会在 b 拿到 a 原来的值 3 之前把它毁掉。图中把每份副本从一个盒子移到另一个盒子:来源盒子保留它的值。
程序运行时,表达式会被计算成一个值,这个值有类型:a + b 是 int,a + 0.5 是 double。像 count = count + 1 这样的语句不是等式;它读取 count,加 1,再把结果存回去。
引用变量可以被赋予新对象或 null。字面量 null 表示该引用不与任何对象关联。
表达式内部的赋值,例如 a = b = 4;,不在课程范围内。
输入可以是触觉、音频、视觉或文本形式。Scanner 类是读取键盘输入文本的一种方式;考试不会考查任何特定形式的用户输入。
截断、拓宽和四舍五入
类型转换运算符 (int) 和 (double) 把一个值从一种基本类型转换为另一种。
把 double 转换为 int 会截断:小数点后的数字被去掉,因此无论正负,值都向零靠拢。
| expression | value |
|---|---|
(int) 3.99 | 3 |
(int) -3.99 | -3 |
7 / 2 | 3 |
(double) 7 / 2 | 3.5 |
(double) (7 / 2) | 3.0 |
7 / 2.0 | 3.5 |
1 + 2.5 | 3.5 |
类型转换作用于紧跟在它后面的值。(double) 7 / 2 先把 7 转换为 double,再按 double 相除;(double) (7 / 2) 先做 int 除法,再转换其结果 3.0。
有些代码会自动把 int 拓宽为 double:在 1 + 2.5 中,以及把 int 存入 double 变量时(第 1.2 页)。
要把 double 四舍五入到最接近的整数,先把它向远离零的方向移动半个单位,再截断:用 (int) (x + 0.5) 处理非负的 x,用 (int) (x - 0.5) 处理负的 x。
| x | (int) x | rounded |
|---|---|---|
2.7 | 2 | 3 |
-2.7 | -2 | -3 |
3.2 | 3 | 3 |
-1.4 | -1 | -1 |
图中对其中每个值做类型转换:红色表示向零截断;蓝色表示先移动半个单位,再截断。
整数溢出和舍入误差
int 存储在固定大小的内存中,4 个字节,所以它有取值范围:从 Integer.MIN_VALUE 到 Integer.MAX_VALUE,包含两端。
| constant | value |
|---|---|
Integer.MAX_VALUE | 2147483647 |
Integer.MIN_VALUE | -2147483648 |
int n = Integer.MAX_VALUE - 2; // n 为 2147483645 n = n + 1; // n 为 2147483646 n = n + 1; // n 为 2147483647 n = n + 1; // n 为 -2147483648 n = n + 1; // n 为 -2147483647
如果某个表达式的结果会超出 int 的范围,就会发生整数溢出。结果是范围内的某个 int,但不是预期的值:MAX_VALUE 加一得到 MIN_VALUE。图中把这些 int 画成一个环(未按比例),接缝在顶部。不会有任何错误信息;程序带着错误的数继续运行。
double 的限制不同。double 也存储在固定大小的内存中,所以比它能容纳的更精确的结果,会被舍入为它能表示的最接近的值:这就是舍入误差。
System.out.println(0.1 + 0.2); // 0.30000000000000004 0.1 和 0.2 都没有精确的二进制形式,这些微小的误差在和中显现出来。要避免舍入误差,尽量使用 int 值:金额用分来计,而不是用元。(能避免舍入误差的特殊十进制类型不在课程范围内。)
+=、−=、*=、/=、%=、++ 和 −−
复合赋值运算符先对变量的值和右边的值做算术运算,再把结果赋给该变量。
score += 5 的作用与 score = score + 5 相同。-=、*=、/= 和 %= 同理;对 int 使用 /= 是 int 除法。
后置自增运算符 ++ 给数值变量加 1 并存回;后置自减运算符 -- 减 1。
int score = 10; // score 为 10 score += 5; // score 为 15 score -= 3; // score 为 12 score *= 2; // score 为 24 score /= 5; // score 为 4 score %= 3; // score 为 1 score++; // score 为 2 score--; // score 为 1
图中运行这几行,并把每个值记在一条纸带上。在纸上也这样追踪:一行,一个新值。
前置形式(++x)以及出现在其他表达式内部的自增(arr[x++])不在课程范围内。
使用不是你写的类
库是类的集合。应用程序编程接口(API)规格说明告诉程序员如何使用这些类。
类定义一种引用类型。库中的类被分组到包中:String 和 Math 在 java.lang 中,Scanner 和 ArrayList 在 java.util 中。无需看到代码就可以用现有的类创建对象;使它们可用的是它们的文档。
类的属性是与它相关的数据,存储在变量中:比如 String 的字符。它的行为是它的实例能做的事,或可以对它们做的事,由方法定义:length()、substring。
注释向原作者和其他人解释代码。编译器会忽略它们,它们也从不运行。Java 有三种注释:
// a comment to the end of the line /* a block of comment, over several lines */ /** a Javadoc comment, used to make API documentation */
图中从书架上取下 String,并打开它的 substring 文档。
前置条件必须在方法运行之前为真,方法才能按预期工作。方法并不需要检查它:调用 substring 时让 to 超出末尾就违反了前置条件,结果是抛出异常,而不是一个客气的答案。
后置条件必须在方法运行之后始终为真。它描述结果:返回什么,或之后对象各属性的值。
参数、实参、返回值
方法是一个有名字的代码块,只有被调用时才运行。代码块是花括号中的任何代码。
过程抽象:你可以在不知道方法如何编写的情况下使用它,只需知道它做什么。它的头部告诉你需要什么:
public static double pow(double base, double exponent)
参数是在头部声明、在方法内部使用的变量(base、exponent)。方法签名是方法名加上按顺序排列的参数类型列表:pow(double, double)。没有参数的方法,列表为空:length()。
实参是调用方法时传入的值。实参的个数和顺序必须与参数相匹配。它们是按值传递的:每个参数一开始都是其实参的副本。图中,2 和 10 变成了副本 2.0 和 10.0。
double r = Math.pow(2, 10); System.out.println(r); // 1024.0 非 void 方法会返回其返回类型的值;要使用它,就把它存入变量,或用在表达式中。void 方法不返回任何东西,所以它不能成为表达式的一部分:int x = System.out.println(5); 无法编译。
当多个方法同名但签名不同时,这些方法被重载:Math.abs(int) 和 Math.abs(double)。由实参决定运行哪一个。
调用会打断语句序列:先运行该方法的语句,当最后一条(或 return)运行完后,控制回到调用之后的位置。
静态方法,以及 Math.random()
类方法属于类,而不属于任何一个对象。它们的头部有关键字 static,调用时用类名加点运算符:Math.sqrt(2.0)。在定义它们的类内部,可以省略类名。
Math 类在 java.lang 中,所以不用导入就可以使用,并且它只有类方法。Java Quick Reference 列出了五个:
| method | returns |
|---|---|
static int abs(int x) | int 的绝对值 |
static double abs(double x) | double 的绝对值 |
static double pow(double base, double exponent) | base 的 exponent 次幂 |
static double sqrt(double x) | 非负平方根 |
static double random() | 一个 ≥ 0.0 且 < 1.0 的 double |
Math.random() 的范围包含 0.0(闭端点)而不含 1.0(开端点)。通过算术运算和类型转换,可以把它调整到你想要的范围:
int roll = (int) (Math.random() * 6) + 1;
图中分步做了这件事。* 6 把 [0.0, 1.0) 拉伸为 [0.0, 6.0)。(int) 把每一部分截断为 0, 1, …, 5:共 6 个相等的部分,所以有 6 个等可能的值。+ 1 把它们平移为 1, …, 6。一般地,(int) (Math.random() * count) + low 给出 count 个 int 中的一个,这些 int 从 low 开始。
它的 30 次投掷结果为 5 on 1, 5 on 2, 6 on 3, 4 on 4, 6 on 5, 4 on 6:等可能并不意味着短期内出现的次数也相等。
蓝图及其制造出的东西
类是对象的属性和行为的正式实现,即蓝图。对象是类的一个具体实例,对这些属性有自己的值。
图中使用了一个假设的类 Book,它有两个属性(书名和页数)和两个行为(getTitle() 和 getPages())。由它制出的每本 Book 都有自己的书名和页数:"Maps", 120; "Birds", 340; "Stars", 96。
相关的类可以把共同的属性和行为放进同一个类,即超类,从而共享它们。扩展它的类,即子类,无需重复编写就能使用这些属性和行为:这是一种继承关系。(设计和编写继承不在课程范围内。)
Java 中的每个类都是 Object 类的子类,所以每个对象都有 toString() 和 equals 之类的方法。
引用类型的变量保存一个对象引用,可以把它看成对象的内存地址:就是图中的箭头,而不是对象本身。
Book b; // b 可以保存指向 Book 的引用 String s; // s 可以保存指向 String 的引用
new、构造方法和 null
对象用关键字 new 加上对该类某个构造方法的调用来创建。
构造方法与它的类同名。它的签名是这个名字加上按顺序排列的参数类型列表;它的头部列出参数的类型和名称。假设的 Book 的文档列出了:
Book(String title, int pages) Book(String title) String getTitle() int getPages()
两个签名不同的构造方法:这些构造方法被重载,由实参决定调用哪一个。new Book("Maps") 调用的是第二个。
Book b = new Book("Maps", 120); Book c = null; 图中按顺序运行第一行:new 为 Book 分配内存;构造方法运行,其参数设置属性的初始值;然后指向新对象的引用被存入 b。与方法调用一样,构造方法调用会打断语句序列,其最后一条语句运行完后,控制回到调用之后。
构造方法的实参在个数和顺序上必须与参数类型兼容,并按值传递:new Book(120, "Maps") 无法编译,因为没有哪个构造方法先接收 int 再接收 String。
引用类型的变量保存一个对象引用,如果没有对象,则保存 null:c 不指向任何东西。
点运算符和 null
实例方法在类的对象上调用,写法是对象名、点运算符和方法:b.getPages()。
int p = b.getPages(); // p 为 120 int q = c.getPages(); // NullPointerException
这次调用顺着 b 中的引用找到它的对象,在该对象上运行 getPages(),返回的 int(120)被存入 p。换一个 Book 对象,会给出它自己的页数。
c 是 null:没有可以顺着箭头找到的对象。对 null 引用调用方法,在该行运行时会抛出 NullPointerException。它能通过编译:编译器只检查 Book 有没有 getPages()。
对比第 1.10 页的类方法:Math.sqrt(x) 在类上调用;s.length() 在某一个 String 对象上调用,只回答这个字符串自己的情况。
不可变;连接会产生新对象
String 对象表示一个字符序列。它可以由字符串字面量 "cat" 创建,也可以用构造方法 new String("cat") 创建。String 在 java.lang 中,默认可用。
String 对象是不可变的:一旦创建,它的字符就永远不会改变。在 String 上调用的方法不会改变它;它们返回新的值。
String s = "cat"; // s 是“cat” s = s + "s"; // s 是“cats” String t = s + 1 + 2; // t 是“cats12” String v = 1 + 2 + s; // v 是“3cats”
+ 和 += 把 String 连接起来,生成一个新的 String 对象。图中,s = s + "s" 生成“cats”并让 s 指向它;“cat”对象仍然完全是“cat”。
基本类型与 String 连接时会被转换为 String。+ 从左到右进行,所以 String 最先出现的位置很重要:在 s + 1 + 2 中,每个数都作为文本接上去;在 1 + 2 + s 中,先把 1 + 2 作为 int 相加。
任何对象都可以与 String 连接:Java 会调用该对象的 toString 方法,每个类都从 Object 继承了它,它返回表示该对象的字符串。子类经常重写它(一个与超类中签名相同、行为自定的 public 方法);编写这样的重写不在课程范围内。
索引、length、substring、indexOf
String 的字符有索引,从 0 到比其长度小 1。使用超出这个范围的索引会抛出 StringIndexOutOfBoundsException。
Java Quick Reference 列出了这些 String 方法(以及构造方法;split 与文本文件一起出现,在主题 4.6):
| method | returns |
|---|---|
int length() | 字符的个数 |
String substring(int from, int to) | 从索引 from 到索引 to - 1 的字符 |
String substring(int from) | substring(from, length()) |
int indexOf(String str) | str 第一次出现处的索引;找不到则为 −1 |
boolean equals(Object other) | 下一页 |
int compareTo(String other) | 下一页 |
String s = "BANANAS"; s.length(); // 7 s.substring(1, 4); // "ANA" s.substring(4); // "NAS" s.indexOf("NA"); // 2 s.indexOf("X"); // -1 s.substring(3, 4); // "A"
to 是第一个被排除在外的索引,所以 substring(from, to) 有 to - from 个字符。indexOf 只找第一次出现的位置:“NA”也从索引 4 开始。
只包含 index 处那一个字符的字符串是 substring(index, index + 1)。
equals、compareTo,而不是 ==
equals 问的是两个 String 是否包含相同的字符序列。compareTo 问的是哪一个排在前面。
String a = "pear"; String b = new String("pear"); System.out.println(a == b); // false System.out.println(a.equals(b)); // true System.out.println(a.compareTo("peach") > 0); // true a 和 b 引用两个不同的对象,但它们包含相同的字符。a.equals(b) 逐对比较字符:true。a == b 比较引用,即它们是否指向同一个对象:false。比较 String 要用 equals。(用 == 比较引用将在主题 2.6 中再讲。)
a.compareTo(other) 在 a 排在 other 之前时返回小于 0 的值,相等时返回 0,a 排在之后时返回大于 0 的值。String 按字母表顺序排序:第一个不同的字符决定顺序。“pear”和“peach”在索引 3 之前都相同,而在该处 'r' 排在 'c' 之后,所以结果为正。只依赖它的符号:这是 Quick Reference 唯一保证的。
本课程只用 equals 比较一个 String 与另一个 String。