From Thinking in Java 4th Edition
String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动:
import static net.mindview.util.Print.*; public class Immutable { public static String upcase(String s) { return s.toUpperCase(); } public static void main(String[] args){ String q = "howdy"; print(q); // howdy String qq = upcase(q); print(qq); // HOWDY print(q); // howdy } } /* Output: howdy HOWDY howdy */
当把q传给upcase()方法时,实际传递的是引用的一个拷贝。每当把String对象作为方法的参数时,都会复制一份引用,而该引用所指的对象其实一直在单一的物理位置上,从未动过。
不可变性会带来一定的效率问题。为String对象重载的“+”操作符就是一个好的例子:
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对象,以此类推。
[软件设计中的一个教训:除非你用代码将系统实现,并让它动起来,否则你无法真正了解它会有什么问题。]
可以在代码中使用StringBuilder来生成一个String:
public class WhitherStringBuilder { public String implicit(String[] fields){ String result = ""; for(int i = 0; i < fields.length; ++i) result += fields[i]; return result; } public String explicit(String[] fields){ StringBuilder result = new StringBuilder(); for(int i = 0; i < fields.length; ++i) result.append(fields[i]); return result.toString(); } }
1. 在implicit()方法中,编译器创建的StringBuilder实在循环体内够早的,这意味着每经过一次循环,就会创建一个新的StringBuilder对象。
2. 在explicit()方法中,它只生成了一个StringBuilder对象。
因此,当你为一个类编写toString()方法时,如果字符串操作比较简单,那就可以信赖编译器为你创建合理的字符串结果。但是,如果你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象用来构造最后的结果:
import java.util.*; public class UsingStringBuilder { public static Random rand = new Random(47); public String toString() { StringBuilder result = new StringBuilder("["); for(int i = 0; i < 25; ++i){ result.append(rand.nextInt(100)); result.append(", "); } result.delete(result.length() - 2, result.length()); result.append("]"); return result.toString(); } public static void main(String[] args){ UsingStringBuilder usb = new UsingStringBuilder(); System.out.println(usb); } } /* Output: [58, 55, 93, 61, 61, 29, 68, 0, 22, 7, 88, 28, 51, 89, 9, 78, 98, 61, 20, 58, 16, 40, 11, 22, 4] */
最终的结果是用append()语句一点点拼接起来的。如果你想走捷径,例如append(a + ":" + c),那编译器就会掉入陷阱,从而为你另外创建一个StringBuilder对象处理括号内的字符操作。
StringBuilder提供了丰富的方法,包括:insert(), replace(), substring()甚至reverse()。但最常用的还是append()和toString(),还有delete()方法。
无意识的递归
Java中的每个类从根本上都是继承自Object,标准容器也不例外,因此容器都有toString()方法:
import generics.coffee.*; import java.util.*; public class ArrayListDisplay { public static void main(String[] args){ ArrayList<Coffee> coffees = new ArrayList<Coffee>(); for(Coffee c : new vCoffeeGenerator(10)) coffees.add(c); System.out.println(coffees); } }
如果你希望toString()方法打印出对象的内存地址,也许你会考虑使用this关键字:
import java.util.*; public class InfiniteRecursion { public String toString() { return " InfiniteRecursion address: " + this + "\n"; } public static void main(String[] args){ List<InfiniteRecursion> v = new ArrayList<InfiniteRecursion>(); for(int i = 0; i < 10; ++i) v.add(new InfiniteRecursion()); System.out.println(v); } }
这里,当你运行
"InfiniteRecursion address " + this
时,发生了自动类型转化。由InfiniteRecursion类型转换成String类型。因为编译器看到一个String对象后面跟着一个“+”,而再后面的对象不是String,于是编译器试着将this转换成衣蛾String。它的转化真是通过调用this上的toString()方法,于是就发生了递归调用。
如果你真的想打印对象的内存地址,你应该调用Object.toString()方法,这才是负责此任务的方法。所以你不应该使用this,而是应该调用super.toString()方法。
String上的操作
当需要改变字符串的内容时,String类的方法都会返回一个新的String对象。同时,如果内容没有发生改变,String的方法只是返回指向原对象的引用而已。
格式化输出[System.out.format()]
Java SE5引入的format()方法可用于PrintStream或PrintWriter对象,其中也包括System.out对象。format()方法模仿自C的printf()。如果你比较怀旧,也可以使用printf():
public class SimpleFormat { public static void main(String[] args){ int x = 5; double y = 5.332542; // The old way: System.out.println("Row 1: [" + x + " " + y + "] "); // The new way: System.out.format("Row 1: [%d %f]\n", x, y); // or System.out.printf("Row 1: [%d %f]\n", x, y); } } /* Output: Row 1: [5, 5.332542] Row 1: [5, 5.332542] Row 1: [5, 5.332542] */
在Java中,所有新的格式化功能都由java.util.Formatter类处理。可以将Formatter看作一个翻译器,它将你的格式化字符串与数据翻译成所需要的结果:
import java.io.*; import java.util.*; public class Turtle { private String name; private Formatter f; public Turtle(String name, Formatter f) { this.name = name; this.f = f; } public void move(int x, int y) { f.format("%s The Turtle is at (%d, %d)\n", name, x, y); } public static void main(String[] args){ PrintStream outAlias = System.out; Turtle tommy = new Turtle("Tommy", new Formatter(System.out)); Turtle terry = new Turtle("Terry", new Formatter(outAlias)); tommy.move(0, 0); terry.move(4, 8); tommy.move(3, 4); terry.move(2, 5); tommy.move(3, 3); terry.move(3, 3); } } /* Output: Tommy The Turtle is at (0, 0) Terry The Turtle is at (4, 8) Tommy The Turtle is at (3, 4) Terry The Turtle is at (2, 5) Tommy The Turtle is at (3, 3) Terry The Turtle is at (3, 3) */
格式化说明符
以下是其抽象的用法:
%[argument_index$][flags][width][.precision]conversion
1. width: 用于指定一个域的最小尺寸。Formatter对象通过在必要时补零来确保一个域至少达到某个长度。默认右对齐,“-”可以改变对齐方向。
2. precision: 对于String对象,用于指定字符最大数量;对浮点数,表示小数部分要显示出来的位数(默认是6位)。不能应用于整数,会触发异常
下面用格式修饰符来打印一个购物收据:
import java.util.*; public class Receipt { private double total = 0; private Formatter f = new Formatter(System.out); public void printTitle() { f.format("%-15s %5s %10s\n", "Item", "Qty", "Price"); f.format("%-15s %5s %10s\n", "----", "---", "-----"); } public void print(String name, int qty, double price){ f.format("%-15.15s %5d %10.2f\n", name, qty, price); total += price; } public void printTotal() { f.format("%-15s %5s %10.2f\n", "Tax", "", total * 0.06); f.format("%-15s %5s %10s \n", "", "", "-----"); f.format("%-15s %5s %10.2f\n", "Total", "", total * 1.06); } public static void main(String[] args){ Receipt receipt = new Receipt(); receipt.printTitle(); receipt.print("Jack‘s Magic Beans", 4, 4.25); receipt.print("Princess Peas", 3, 5.1); receipt.print("Three Bears Porridge", 1, 14.29); receipt.printTotal(); } } /* Output: Item Qty Price ---- --- ----- Jack‘s Magic Be 4 4.25 Princess Peas 3 5.10 Three Bears Por 1 14.29 Tax 1.42 ----- Total 25.06 */
格式转换符b的转换结果为true或false。但只要其参数不是null,那转换结果就都是true,即使是数字0,其结果也是true。而在其他语言如C中,数字0的转换为false,这点需要注意。
String.format()
Java SE5也参考了C中的sprintf()方法, 以生成格式化的String对象。String.format()是一个static方法,它接受与Formatter.format()方法一样的参数,但返回一个String对象。当你只使用format()方法一次的时候,String.format()用起来很方便:
public class DatabaseException extends Exception { public DatabaseException(int transactionID, int queryID, String message){ super(Stirng.format("(t%d, q%d) %s", transactionID, queryID, message)); } public static void main(String[] args){ try { throw new DatabaseException(3, 7, "Write failed"); } catch(Exception e) { System.out.println(e); } } } /* Output: DatabaseException: (t2, q7) Write failed */
一个十六进制转存的工具
下面的小工具使用了String.format()方法,以可读的十六进制格式将字节数组打印出来:
package net.mindview.util; import java.io.*; public class Hex { public static String format(byte[] data) { StringBuilder result = new StringBuilder(); int n = 0; for(byte b : data) { if(0 == n % 16) result.append(String.format("%05X: ", n)); result.append(String.format("%02X ", b)); ++n; if(0 == n % 16) result.append("\n"); } result.append("\n"); return result.toString(); } public static void main(String[] args){ if(0 == args.length) // Test by display this class file: System.out.println(format(BinaryFile.read("Hex.class"))); else System.out.println(format(BinaryFile.read(new File(args[0])))); } }
正则表达式
一般说来,正则表达式以某种方式来描述字符串,因此你可以说“如果一个字符串含有这些东西,那么它就是我正在找的东西。
Java对反斜杠\有着特殊的处理。在正则表达式中\d表示一位数字。在其它语言中,\\表示“我想要在正则表达式中插入一个普通的(literal)反斜杠,请不要给它任何特殊的意义”。而在Java中,\\的意思是“我要插入一个正则表达式的反斜杠,所以其后的字符具有特殊的意义。”例如,如果你在Java中想要表示一位数,那么其正则表达式应该是\\d。如果要插入一个普通的反斜杠,则应该是\\\。不过换行符和制表符之类的东西只需要使用单反斜杠:\n\t。
1. 表示可能有,应该使用"?"
2. 表示一个或多个之前的表达式,应该使用"+"
所以要表示“可能有一个负号,后面跟着一位数或多位数”,可以这样:
-?\\d+
应用正则表达式的最简单的途径,就是利用String类内建的功能。例如,你可以检查一个String是否匹配如上所述的正则表达式:
public class IntegerMatch { public static void main(String[] args){ System.out.println("-1234".matches("-?\\d+")); System.out.println("5678".matches("-?\\d+")); System.out.println("+911".matches("-?\\d+")); System.out.println("+911".matches("(-|\\+)?\\d+")); } } /* Output: true true false true */
String类中还自带了一个非常有用的正则表达式工具——split()方法,其功能是“将字符串从正则表达式匹配的地方切开”:
import java.util.*; public class Splitting { public static String knights = "Then, when you have found the shrubbery, you must" + "cut down the mightiest tree in the forest ..." + "with ... a herring!"; public static void split(String regex){ System.out.println(Arrays.toString(knights.split(regex))); } public static void main(String[] args){ split(" "); // Doesn‘t have to contain regex chars split("\\W+"); // Non-word characters split("n\\W+"); // ‘n‘ followed by non-word characters } } /* Output: [Then,, when, you, have, found, the, shrubbery,, you, mustcut, down, the, mightiest, tree, in, the, forest, ...with, ..., a, herring!] [Then, when, you, have, found, the, shrubbery, you, mustcut, down, the, mightiest, tree, in, the, forest, with, a, herring] [The, whe, you have found the shrubbery, you mustcut dow, the mightiest tree i, the forest ...with ... a herring!] */
\W表示非单词字符(如果W小写,\w,则表示一个单词字符)。可以看到,在原始字符串中,与正则表达式匹配的部分,在最终的结果中都不存在了。
String.split()还有一个重载版本,可以限制字符串分割的次数。
String类自带的最后一个正则表达式工具是“替换”。你可以只替换正则表达式第一个匹配的子串,或是替换所有匹配的地方:
import static net.mindview.util.Print.*; public class Replacing { static String s = Splitting.knights; public static void main(String[] args){ print(s.replaceFirst("f\\w+", "located")); print(s.replaceAll("shrubbery|tree|herring", "banana")); } }
下面的每个正则表达式都能成功匹配字符序列“Rudolph”:
public class Rudolph { public static void main(String[] args){ for(String pattern : new String[]{"Rudolph", "[rR]udolph", "[rR][aeiou][a-z]ol.*", "R.*"}) System.out.println("Rudolph".matches(pattern)); } } /* Output: true true true true */
Pattern和Matcher
比起功能有限的String类,我们更愿意构造功能强大的正则表达式对象。只需导入java.util.regex包,然后用static Pattern.compile()方法来编译你的正则表达式。[在Unix/Linux上,命令行中的正则表达式必须用引号括起来。]
import java.util.regex.*; import static net.mindview.util.Print.*; public class TestRegularExpression { public static void main(String[] args){ if(args.length < 2) { print("Usage: \njava TestRegularExpression " + "characterSequence regularExpression+"); System.exit(0); } print("Input: \"" + args[0] + "\""); for(String arg : args){ print("Regular expression: \"" + arg + "\""); Pattern p = Pattern.compile(arg); Matcher m = p.matcher(args[0]); while(m.find()){ print("Match \"" + m.group() + "\" at positions " + m.start() + "-" + (m.end() - 1)); } } } }
通过调用Pattern.matcher()方法,并传入一个字符串参数,我们得到了一个Matcher对象。使用Matcher对象上的方法, 我们将能够判断各种不同类型的匹配是否成功:
boolean matches() boolean lookingAt() boolean find() boolean find(int start)
其中matches()方法用来判断整个输入字符串是否匹配正则表达式模式,而lookingAt()则用来判断该字符串的起始部分是否能够匹配模式。
Matcher.find()方法可用来在CharSequence中查找多个匹配:
import java.util.regex.*; import static net.mindview.util.Print.*; public class Finding { public static void main(String[] args){ Matcher m = Pattern.compile("\\w+").matcher("Evening is full of the linnet‘s wings"); while(m.find()) printnb(m.group() + " "); print(); int i = 0; while(m.find(i)){ printnb(m.group() + " "); ++i; } } } /* Output: Evening is full of the linnet s wings Evening vening ening ning ing ng g is is s full full ull ll l of of f the the he e linnet linnet innet nnet net et t s s wings wings ings ngs gs s */
find()像迭代器那样前向遍历输入字符串。而第二个find()能够接受一个整数作为参数,该整数表示字符串中字符的位置,并以其作为搜索的起点。