Java 之道/字符串和其他
在图形部分,我们使用 Graphics 对象在窗口中绘制圆圈,并且我使用了“在对象上调用方法”这个短语来指代类似这样的语句
g.drawOval (0, 0, width, height);
在本例中,drawOval 是在名为 g 的对象上调用的方法。当时我没有提供对象的定义,现在也不能提供完整的定义,但现在该尝试了。
在 Java 和其他面向对象的语言中,对象是包含一组相关数据的集合,这些数据附带一组方法。这些方法作用于对象,执行计算,有时还会修改对象的数据。
到目前为止,我们只见过一个对象 g,因此这个定义可能还没有什么意义。另一个例子是字符串。字符串是对象(而整数和双精度数不是)。根据对象的定义,你可能会问“字符串对象中包含哪些数据?”以及“我们可以对字符串对象调用哪些方法?”
字符串对象中包含的数据是字符串的字母。有很多方法可以操作字符串,但我在这本书中只使用其中的几个。其余内容在Java 网站中有记录。
我们将要看的第一个方法是 charAt,它允许你从字符串中提取字母。为了存储结果,我们需要一种可以存储单个字母(而不是字符串)的变量类型。单个字母称为字符,存储它们的变量类型称为 char。
字符的工作方式与我们已经见过的其他类型一样
char fred = 'c';
if (fred == 'c')
System.out.println (fred);
字符值用单引号 ( 'c' ) 括起来。与字符串值(用双引号括起来)不同,字符值只能包含单个字母或符号。
以下是 charAt 方法的使用方法
String fruit = "banana";
char letter = fruit.charAt(1);
System.out.println (letter);
语法 fruit.charAt 表示我正在对名为 fruit 的对象调用 charAt 方法。
我向此方法传递了参数 1,表示我想知道字符串的第一个字母。结果是一个字符,存储在名为 letter 的 char 中。当我打印 letter 的值时,我感到意外
a
“a”不是“banana”的第一个字母。除非你是计算机科学家。由于某种奇怪的原因,计算机科学家总是从零开始计数。“banana”的第 0 个字母(第零个)是“b”。第 1 个字母(第一个)是“a”,第 2 个字母(第二个)是“n”。
如果你想要字符串的第 0 个字母,你需要传递 0 作为参数
char letter = fruit.charAt(0);
我们将要看的第二个 String 方法是 length,它返回字符串中的字符数。例如
int length = fruit.length();
length 不接受任何参数,如 () 所示,它返回一个整数,在本例中为 6。请注意,使用与方法相同的名称来命名变量是合法的(尽管这会让人类读者感到困惑)。
要找到字符串的最后一个字母,你可能会尝试以下方法
int length = fruit.length();
char last = fruit.charAt (length); // WRONG!!
这行不通。原因是“banana”中没有第 6 个字母。因为我们从 0 开始计数,所以 6 个字母的编号从 0 到 5。要获取最后一个字符,你需要从 length 中减去 1。
对字符串进行的操作通常是先从开头开始,依次选择每个字符,对其进行某种操作,然后继续到结尾。这种处理模式称为遍历。用 while 语句编码遍历的一种自然方式是
int index = 0;
while (index < fruit.length())
char letter = fruit.charAt (index);
System.out.println (letter);
index = index + 1;
此循环遍历字符串,并将每个字母分别打印在一行上。请注意,条件是 index < fruit.length(),这意味着当 index 等于字符串的长度时,条件为假,循环体不会执行。我们访问的最后一个字符是索引为 fruit.length()-1 的字符。
循环变量的名称是 index。索引是一个变量或值,用于指定有序集合中的一个成员(在本例中是字符串中的字符集合)。索引指示(因此得名)你想要哪一个。集合必须是有序的,以便每个字母都有一个索引,每个索引都对应一个字符。
作为练习,编写一个方法,它接受一个字符串作为参数,并按逆序打印字母,所有字母都放在一行上。
早在运行时部分,我就谈到了运行时错误,即直到程序开始运行才会出现的错误。在 Java 中,运行时错误称为异常。
到目前为止,你可能还没有见过很多运行时错误,因为我们还没有做过会导致运行时错误的事情。嗯,现在我们正在做。如果你使用 charAt 命令并提供一个负值或大于 length-1 的索引,你将得到一个异常:具体来说,是 StringIndexOutOfBoundsException。试试看,看看它的样子。
如果你的程序导致异常,它会打印一条错误消息,指示异常类型以及它在程序中的位置。然后程序终止。
如果你访问
并点击 charAt,你会得到以下文档(或类似内容)
public char charAt(int index) Returns the character at the specified index. An index ranges from 0 to length() - 1. Parameters: index - the index of the character. Returns: the character at the specified index of this string. The first character is at index 0. Throws: StringIndexOutOfBoundsException if the index is out of range. verbatim
第一行是方法的原型(参见原型部分),它指示方法的名称、参数的类型和返回类型。
下一行描述了方法的作用。接下来的两行解释了参数和返回值。在本例中,说明有点多余,但文档应该符合标准格式。最后一行解释了此方法可能导致哪些异常(如果有)。
在某种程度上,indexOf 与 charAt 相反。charAt 接受一个索引,并返回该索引处的字符。indexOf 接受一个字符,并找到该字符出现的索引。
如果索引超出范围,charAt 将失败并导致异常。如果字符未出现在字符串中,indexOf 将失败并返回 -1。
String fruit = "banana";
int index = fruit.indexOf('a');
这将找到字母 'a' 在字符串中的索引。在本例中,字母出现了三次,因此 indexOf 应该做什么并不明显。根据文档,它返回第一个出现的索引。
为了找到后续的出现,indexOf 有一个备用版本(有关此类重载的解释,请参见重载部分)。它接受第二个参数,指示在字符串中的什么位置开始查找。如果我们调用
int index = fruit.indexOf('a', 2);
它将从第二个字母(第一个 n)开始,并找到第二个 a,它位于索引 3 处。如果字母恰好出现在起始索引处,则起始索引就是答案。因此,
int index = fruit.indexOf('a', 5);
返回 5。根据文档,要弄清楚起始索引超出范围时会发生什么情况有点棘手
indexOf 返回此对象表示的字符序列中大于或等于 fromIndex 的第一个出现的字符的索引,如果该字符不存在,则返回 -1。
弄清楚这意味着什么的一种方法是尝试几个案例。以下是我实验的结果
- 如果起始索引大于或等于 length(),则结果为 -1,表示该字母不会出现在大于起始索引的任何索引处。
- 如果起始索引为负数,则结果为 1,表示字母在起始索引之后的第一个出现位置。
如果你回顾一下文档,你会发现这种行为与定义一致,即使它并不立即显而易见。现在我们对 indexOf 的工作原理有了更好的了解,我们可以将其用作程序的一部分。
循环和计数
[edit | edit source]以下程序计算字母 'a' 在字符串中出现的次数
String fruit = "banana";
int length = fruit.length();
int count = 0;
int index = 0;
while (index < length)
if (fruit.charAt(index) == 'a')
count = count + 1;
index = index + 1;
System.out.println (count);
该程序演示了一种常见的习惯用法,称为计数器。变量 count 初始化为零,然后每当我们找到一个 'a' 时就增加它(增加是指增加一个;它是递减的相反,与粪便无关,粪便是一个名词)。当我们退出循环时,count 包含结果:a 的总数。
作为练习,将此代码封装在一个名为 countLetters 的方法中,并将其泛化,使其接受字符串和字母作为参数。
作为第二个练习,重写该方法,使其使用 indexOf 来定位 a,而不是逐个检查字符。
递增和递减运算符
[edit | edit source]递增和递减是如此常见的操作,以至于 Java 为它们提供了特殊的运算符。++ 运算符将 int 或 char 的当前值加一。-- 减一。这两个运算符都不适用于 double、boolean 或 String。
从技术上讲,在表达式中同时递增变量是合法的。例如,你可能会看到类似的东西
System.out.println (i++);
看看这个,不清楚递增是在值被打印之前还是之后生效。因为像这样的表达式容易造成混淆,我建议你不要使用它们。事实上,为了让你更加反感,我不会告诉你结果是什么。如果你真的想知道,你可以尝试一下。
使用递增运算符,我们可以重写字母计数器
int index = 0;
while (index < length)
if (fruit.charAt(index) == 'a')
count++;
index++;
常见的错误是写下类似的东西
index = index++; // WRONG!!
不幸的是,这在语法上是合法的,因此编译器不会警告你。该语句的效果是使 index 的值保持不变。这通常是一个难以追踪的错误。
记住,你可以写 index = index +1;,或者你可以写 index++;,但你不应该将它们混合使用。
字符算术
[edit | edit source]可能看起来很奇怪,但你可以对字符进行算术运算!表达式 'a' + 1 得出值 'b'。类似地,如果你有一个名为 letter 的变量包含一个字符,那么 letter - 'a' 将告诉你它在字母表中的位置(记住 'a' 是字母表的第零个字母,'z' 是第 25 个字母)。
这种方法对于在包含数字的字符(如 '0'、'1' 和 '2')与相应的整数之间进行转换很有用。它们不是一回事。例如,如果你尝试这样
char letter = '3';
int x = (int) letter;
System.out.println (x);
你可能期望得到值 3,但根据你的环境,你可能会得到 51,它是用于表示字符 '3' 的 ASCII 代码,或者你可能会得到完全不同的东西。要将 '3' 转换为相应的整数值,你可以减去 '0'
int x = (int)(letter - '0');
从技术上讲,在这两个例子中,类型转换 ((int)) 都是不必要的,因为 Java 会自动将类型 char 转换为类型 int。我添加了类型转换是为了强调类型之间的差异,因为我是一个坚持这种事情的人。
由于这种转换可能有点难看,因此最好使用 Character 类中的 digit 方法。例如
int x = Character.digit (letter, 10);
将 letter 转换为相应的数字,将其解释为十进制数。
字符算术的另一个用途是按顺序遍历字母表中的字母。例如,在罗伯特·麦考斯基的书《给鸭子让路》中,小鸭子的名字构成了一个字母顺序的序列,类似于 Jack、Kack、Lack、Mack、Nack、Oack、Pack 和 Qack。以下是一个按顺序打印这些名字的循环
char letter = 'J';
while (letter <= 'Q')
System.out.println (letter + "ack");
letter++;
注意,除了算术运算符外,我们还可以对字符使用条件运算符。该程序的输出是
Jack Kack Lack Mack Nack Oack Pack Qack
当然,这不太正确,因为我拼错了 Ouack 和 Quack。作为练习,修改程序以更正此错误。
类型转换(面向专家)
[edit | edit source]这是一个难题:通常,语句 x++ 与 x = x + 1 完全等价。除非 x 是一个 char!在这种情况下,x++ 是合法的,但 x = x + 1 会导致错误。
尝试一下看看错误消息是什么,然后看看你是否能弄清楚发生了什么。
字符串是不可变的
[edit | edit source]当你查看 String 方法的文档时,你可能会注意到 toUpperCase 和 toLowerCase。这些方法通常是混乱的来源,因为它们听起来像是改变(或修改)现有字符串。实际上,这些方法或任何其他方法都不能改变字符串,因为字符串是不可变的。
当你对 String 调用 toUpperCase 时,你会得到一个新的 String 作为返回值。例如
String name = "Alan Turing";
String upperName = name.toUpperCase ();
在第二行执行之后,upperName 包含值 "ALAN TURING",但 name 仍然包含 "Alan Turing"。
字符串是不可比较的
[edit | edit source]通常需要比较字符串以查看它们是否相同,或者查看哪个在字母顺序中排在前面。如果我们可以使用比较运算符(如 == 和 >)就好了,但我们不能。
为了比较字符串,我们必须使用 equals 和 compareTo 方法。例如
String name1 = "Alan Turing";
String name2 = "Ada Lovelace";
if (name1.equals (name2))
System.out.println ("The names are the same.");
int flag = name1.compareTo (name2);
if (flag == 0)
System.out.println ("The names are the same.");
else if (flag < 0)
System.out.println ("name1 comes before name2.");
else if (flag > 0)
System.out.println ("name2 comes before name1.");
这里的语法有点奇怪。要比较两个东西,你必须对其中一个调用一个方法,并将另一个作为参数传递。
equals 的返回值很简单;如果字符串包含相同的字符,则为真,否则为假。
compareTo 的返回值有点奇怪。它是字符串中第一个不同的字符之间的差值。如果字符串相等,则为 0。如果第一个字符串(调用该方法的字符串)在字母表中排在前面,则差值为负数。否则,差值为正数。在这种情况下,返回值为正数 8,因为 Ada 的第二个字母在字母表中比 Alan 的第二个字母提前 8 个字母。
使用 compareTo 通常很棘手,我总是记不住哪种方式是哪种方式,但我可以告诉你的是,这个接口对于比较许多类型的对象来说都是非常标准的,所以一旦你理解了它,你就可以一劳永逸了。
为了完整起见,我应该承认,使用 == 运算符操作字符串是合法的,但很少是正确的。但这只有在稍后才能理解,所以现在,不要这样做。
词汇表
[edit | edit source]- 对象 一个相关数据的集合,它附带了一组对其进行操作的方法。到目前为止我们使用过的对象是系统提供的 Graphics 对象和 String。
- 索引 一个变量或值,用于选择一个有序集合中的一个成员,例如字符串中的一个字符。
- 遍历 对一个集合中的所有元素进行迭代,对每个元素执行类似的操作。
- 计数器 用于计数的变量,通常初始化为零,然后递增。
- 递增 将变量的值增加一。Java 中的递增运算符是 ++。
- 递减 将变量的值减少一。Java 中的递减运算符是 --。
- 异常 运行时错误。异常会导致程序的执行终止。