標籤:
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);// howdyString qq = upcase(q);print(qq);// HOWDYprint(q);// howdy}} /* Output:howdyHOWDYhowdy*/
當把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.25Princess Peas 3 5.10Three Bears Por 1 14.29Tax 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")));elseSystem.out.println(format(BinaryFile.read(new File(args[0]))));}}
Regex
一般說來,Regex以某種方式來描述字串,因此你可以說“如果一個字串含有這些東西,那麼它就是我正在找的東西。
Java對反斜線\有著特殊的處理。在Regex中\d表示一位元字。在其它語言中,\\表示“我想要在Regex中插入一個普通的(literal)反斜線,請不要給它任何特殊的意義”。而在Java中,\\的意思是“我要插入一個Regex的反斜線,所以其後的字元具有特殊的意義。”例如,如果你在Java中想要表示一位元,那麼其Regex應該是\\d。如果要插入一個普通的反斜線,則應該是\\\。不過分行符號和定位字元之類的東西只需要使用單反斜線:\n\t。
1. 表示可能有,應該使用"?"
2. 表示一個或多個之前的運算式,應該使用"+"
所以要表示“可能有一個負號,後面跟著一位元或多位元”,可以這樣:
-?\\d+
應用Regex的最簡單的途徑,就是利用String類內建的功能。例如,你可以檢查一個String是否匹配如上所述的Regex:
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:truetruefalsetrue*/
String類中還內建了一個非常有用的Regex工具——split()方法,其功能是“將字串從Regex匹配的地方切開”:
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 charssplit("\\W+");// Non-word characterssplit("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,則表示一個單詞字元)。可以看到,在原始字串中,與Regex匹配的部分,在最終的結果中都不存在了。
String.split()還有一個重載版本,可以限制字串分割的次數。
String類內建的最後一個Regex工具是“替換”。你可以只替換Regex第一個匹配的子串,或是替換所有匹配的地方:
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"));}}
下面的每個Regex都能成功匹配字元序列“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:truetruetruetrue*/
Pattern和Matcher
比起功能有限的String類,我們更願意構造功能強大的Regex對象。只需匯入java.util.regex包,然後用static Pattern.compile()方法來編譯你的Regex。[在Unix/Linux上,命令列中的Regex必須用引號括起來。]
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()方法用來判斷整個輸入字串是否匹配Regex模式,而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()能夠接受一個整數作為參數,該整數表示字串中字元的位置,並以其作為搜尋的起點。
Thinking in Java Chapter 13