Files
OnJava8/docs/book/18-Strings.md
2019-05-09 17:19:57 +08:00

5.7 KiB
Raw Blame History

[TOC]

第十八章 字符串

可以证明,字符串操作是计算机程序设计中最常见的行为之一。 在Java大展拳脚的Web系统中更是如此。在本章中我们将深入学习在Java语言中应用最广泛的String类,并研究与之相关的类及工具。

字符串的不可变

String对象是不可变的。查看JDK文档你就会发现String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动。

看看下面的代码:

// strings/Immutable.java
public class Immutable { 
    public static String upcase(String s) { 
        return s.toUpperCase(); 
    } 
    public static void main(String[] args) { 
        String q = "howdy"; System.out.println(q); // howdy 
        String qq = upcase(q); 
        System.out.println(qq); // HOWDY 
        System.out.println(q); // howdy 
    } 
} 
/* Output: 
howdy
HOWDY 
howdy
*/ 

当把q传递给upcase()方法时实际传递的是引用的一个拷贝。其实每当把String对象作为方法的参数时都会复制一份引用而该引用所指向的对象其实一直待在单一的物理位置上从未动过。

回到upcase()的定义传入其中的引用有了名字s只有upcase()运行的时候局部引用s才存在。一旦upcase()运行结束s就消失了。当然了upcase()的返回值其实是最终结果的引用。这足以说明upcase()返回的引用已经指向了一个新的对象而q仍然在原来的位置。

String的这种行为正是我们想要的。例如:

String s = "asdf";
String x = Immutable.upcase(s);

难道你真的希望upcase()方法改变其参数吗?对于一个方法而言,参数是为该方法提供信息的,而不是想让该方法改变自己的。在阅读这段代码时,读者自然会有这样的感觉。这一点很重要,正是有了这种保障,才使得代码易于编写和阅读。

重载和StringBuilder

String对象是不可变的,你可以给一个String对象添加任意多的别名。因为String是只读的,所以指向它的任何引用都不可能改它的值,因此,也就不会影响到其他引用。

不可变性会带来一定的效率问题。为String对象重载的“+”操作符就是一个例子。重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义(用于String的“+”与“+=”是Java中仅有的两个重载过的操作符Java不允许程序员重载任何其他的操作符)。

操作符“+”可以用来连接String

// strings/Concatenation.java

public class Concatenation {
     public static void main(String[] args) { 
         String mango = "mango"; 
         String s = "abc" + mango + "def" + 47; 
         System.out.println(s);
    } 
} 
/* Output:
abcmangodef47 
*/

可以想象一下,这段代码可能是这样工作的:String可能有一个append()方法,它会生成一个新的String对象以包含“abc”与mango连接后的字符串。然后该对象再与“def”相连生成另一个新的对象依此类推。

这种方式当然是可行的,但是为了生成最终的String对象会产生一大堆需要垃圾回收的中间对象。我猜想Java设计者一开始就是这么做的(这也是软件设计中的一个教训:除非你用代码将系统实现,并让它运行起来,否则你无法真正了解它会有什么问题),然后他们发现其性能相当糟糕。

想看看以上代码到底是如何工作的吗可以用JDK自带的javap来反编译以上代码。命令如下

javap -c Concatenation

这里的-c标志表示将生成JVM字节码。我们剔除不感兴趣的部分然后做细微的修改于是有了一下的字节码

public static void main(java.lang.String[]); 
Code:
Stack=2, Locals=3, Args_size=1
0: ldc #2; //String mango 
2: astore_1 
3: new #3; //class StringBuilder 
6: dup 
7: invokespecial #4; //StringBuilder."<init>":() 
10: ldc #5; //String abc 
12: invokevirtual #6; //StringBuilder.append:(String) 
15: aload_1 
16: invokevirtual #6; //StringBuilder.append:(String) 
19: ldc #7; //String def 
21: invokevirtual #6; //StringBuilder.append:(String) 
24: bipush 47 
26: invokevirtual #8; //StringBuilder.append:(I) 
29: invokevirtual #9; //StringBuilder.toString:() 
32: astore_2 
33: getstatic #10; //Field System.out:PrintStream;
36: aload_2 
37: invokevirtual #11; //PrintStream.println:(String) 
40: return

如果你有汇编语言的经验,以上代码应该很眼熟(其中的dupinvokevirtual语句相当于Java虚拟机上的汇编语句。即使你完全不了解汇编语言也无需担心)。需要重点注意的是:编译器自动引入了java.lang.StringBuilder类。虽然源代码中并没有使用StringBuilder类,但是编译器却自作主张地使用了它,就因为它更高效。 在这里,编译器创建了一个StringBuilder对象,用于构建最终的String s,并对每个字符串调用了一次append()方法共计4次。最后调用toString()生成结果,并存为s(使用的命令为astore_2)。

意外递归

字符串操作

格式化输出

常规表达式

扫描输入

StringTokenizer类

本章小结