本文为Java编程思想第四版的学习笔记,在此感谢作者Bruce Eckel给我们带来这样一本经典著作,也感谢该书的翻译及出版人员。本文为原创笔记,转载请注明出处,谢谢。
1.不可变String
String对象是不可变的(final类)。查看JDK文档你就会发现,String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动。
2.重载“+”与StringBuilder
String对象是不可变的,你可以给一个String对象加惹你多的别名。因为String对象只读特性,所以指向它的任何引用都不可能改变它的值,因此,也就不会对其他的引用有什么影响。
不可变性会带来一定的效率问题。为String对象重载的“+”操作符就是一个例子。重载的意思是,一个操作符在应用于特定的类时,被赋予了特殊的意义(用于String时,“+”和“+=”是Java中仅有的两个重载过的操作符,而Java并不允许程序员重载任何操作符)。操作符“+”可以用来连接String。可以想象一下,String s = "abc" + "mango" + "def" + 47;这段代码是如何工作的:String可能有一个append()方法,它会生成一个新的String对象,以包含“abc"与”mango"连接后的字符串。然后,该对象再与“def”项链,生成另一个新的String对象,以此类推。这种方式当然也行得通,但是为了生成最终的String,此方式会产生一大堆需要垃圾回收的中间对象。我猜想,Java设计师一开始就是这么做的(这也是软件设计中的一个教训:除非你用代码将系统实现,并让它动起来,否则你无法真正了解它会有什么问题),然后他们发现其性能相当糟糕。通过反编译那段代码,我们可以发现它真正的工作情况:编译器自动引入了java.lang.StringBuilder类。虽然我们在源代码中并没有使用StringBuilder类,但是编译器却自作主张地使用了它,因为它更高效。在这个例子中,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符串调用一次StringBuilder的append()方法,总计四次。最后调用toString()生成结果,并存为s。
现在,也许你会觉得可以随意使用String对象,反正编译器会为你自动优化性能。可是在这之前,让我们更深入地看看编译器能为我们优化到什么程度。通过循环分别通过追加创建一个字符串,反编译源码,我们可以看到:利用String创建时,编译器通过StringBuilder优化了字符串的创建,但是由于循环的存在,导致每次循环编译器都会创建一个StringBuilder对象;相比之下,利用StringBuilder创建字符串则只创建了一个StringBuilder对象,反编译的汇编指令也比前一个简洁的多。因此,当你为一个类编写toString()方法时,若字符串操作比较简单,那就可以新来编译器,它会为你合理地构造最终的字符串结果。但是,如果你要在toString方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终结果。如果拿不准该用哪种方式,随时可以用javap来分析你的程序。
StringBuilder是在Java SE5引入的,在这之前Java用的是StringBuffer。后者是线程安全的,因此开销会大些,所以在Java SE5/6中,字符串操作应该还会更快一些。
3.无意识的递归
Java中的每个类从根本上都是继承自Object,标准容器类自然也不例外。因此容器类都有toString()方法,并且重写了该方法,使得它生成的String结果能够表达容器自身,以及容器所包含的对象。如果你希望toString()方法打印出对象的内存地址,也许你会考虑使用关键字this.但是此时,如果你将多个这样的对象放进一个容器中,并试图调用容器的toString方法打印该容器的内容,你会得到遗传非常长的异常。这是因为编译器在通过this上的toString()方法将this变成字符串时,发生了递归调用。因此,如果你真的想要打印出对象的内存地址,应该调用Object.toString()方法。
4.String上的操作
String类有很多函数,在此不一一列举。在此,我们要注意一点,当需要改变字符串的内容时,String类的方法都会返回一个新的String对象。同时,如果内容没有发生改变,String的方法只是返回指向源对象的引用而已。这可以节约存储空间以及避免二外的开销。
5.格式化输出
在长久的等待之后,Java SE5终于推出了C语言中printf()风格的格式化输出这一功能。这不仅是的控制输出的代码更加简单,同时也给与Java开发者对于输出格式与排列更强大的孔子能力。
5.1 printf()
C语言中printf()用法,不多介绍。
5.2 System.out.format();
Java SE5引入的format方法可用于PrintSteam或PrintWriter对象,其中包括System.out对象。format()方法模仿自C的printf()。如果你比较怀旧的话,也可以使用printf();
5.3 Formatter类
在Java中,所有新的格式化功能都由java.util.Formatter类处理。可以将Formatter类看做一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个Formatter对象的时候,需要向其构造器攒低一些信息,告诉它最终的结果将向哪里输出。
5.4 格式化说明符
在插入数据时,如果想要控制空格与对齐,你需要更精细复杂的更是修饰符。以下是其抽象的语法:
%[argument_index$][flags][width][.precision]conversion
最常见的应用是控制一个域的最小尺寸,这可以通过指定width来实现。Formatter对象通过在必要时添加空格,来确保一个域至少达到某个长度。在默认的情况下,数据是右对齐的,不过可以通过使用“-”编制来改变对其方向。与width相对的是precision,它用来指明最大尺寸。width可以应用于各种类型的数据转换,并且其行为方式都一样。precision则不然,不是所有类型的数据都能使用precision,而且,应用于不同类型的数据转换时,precision的意义也不同。在将precision应用于String时,它表示打印String时输出字符串的最大数量。而在将precision应用于浮点数时,它表示小数部分要显示出来的位数(默认是6位小数),如果小数位过多则舍入,太少则在尾补零。由于整数没有小数部分,所以precision无法应用于整数。
5.5 Formatter转换
类似于C语言中printf()的那些特殊的占位符,如%d,表示整数型。在此,不一一列举。
5.6 String.format()
Java SE5也参考了C中的sprintf()方法,以生成格式化的String对象。String.format()是一个static方法,它接受与Formatter.format()方法一样的参数,但是返回一个String对象。当你只需要使用format()方法一次是,String.format()用起来很方便。其实,在String.format()内部,它也是创建了一个Formatter对象,然后将你传入的参数转给该Formatter。不过,与其自己做这些事情,不如使用便捷的String.format()方法,何况这样的代码更清晰易读。
6.正则表达式
正则表达式是一个非常好用的工具,但是有些过于难于记忆和复杂,因此,本菜鸟也一直将正则表达式作为个人了解的知识,在需要时去查阅相关的内容。在此不作叙述和学习。
7.扫描输入
到目前为止,从文件或标准输入读取数据还是一件相当痛苦的事情。一般的解决之道是读入一行文本,对其进行分词,然后使用Integer、Double等类的各种解析方法来解析数据。
终于,Java SE5新增了Scanner类。
7.1 Scanner定界符
在默认情况下,Scanner更具空白字符对输入进行分词,但是你可以用正则表达式指定自己所需的定界符。
7.2 用正则表达式扫描
除了能够扫描基本类型之外,你还可以使用自定义的正则表达式进行扫描,这在扫描复杂数据是非常有用。
8.StringTokenizer
在Java引入正则表达式和Scanner类之前,分割字符串的唯一方法是使用StringTokenizer来分词。不过现在有了正则表达式和Scanner,我们可以使用更加简单的方式完成同样的工作。基本上,我们可以放心的说,StringTokenizer已经可以废弃不用了。
本菜鸟总结:
本章介绍的内容是我不太满意的地方,“Java编程思想”在我看来是一本让我们可以触及java本质的书,然而本章作者的介绍似乎与简单的类库说明没什么区别(文章开头通过反编译代码研究String的地方非常精彩)、而且花费了大量的篇幅进行正则表达式的介绍。或许是我的水平未达到,我一直觉得正则表达式是一个非常好用的工具,但是有些过于难于记忆和复杂,因此,本菜鸟也一直将正则表达式作为个人了解的知识,在需要时去查阅相关的内容。对于String在内存中的存储、以及为什么将String定义为一个finall类等问题,作者没有做相应的讲解,我对此表示很遗憾。此外,我也好奇,为何Java如此强大的语言,却在早期连基本的输入都如此的麻烦。求指教。