跳转到内容

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。试试看,看看它的样子。

如果你的程序导致异常,它会打印一条错误消息,指示异常类型以及它在程序中的位置。然后程序终止。

阅读文档

[编辑 | 编辑源代码]

如果你访问

http://java.sun.com/j2se/1.4/docs/api/java/lang/String.html

并点击 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 方法

[编辑 | 编辑源代码]

在某种程度上,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

当然,这不太正确,因为我拼错了 OuackQuack。作为练习,修改程序以更正此错误。

类型转换(面向专家)

[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 中的递减运算符是 --。
  • 异常 运行时错误。异常会导致程序的执行终止。
华夏公益教科书