读thinking in java笔记(十二):字符串

    可以证明,字符串操作是计算机程序设计中最常见的行为。
1. 不可变String
    String对象是不可变的。查看JDK文档你就会发现,String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的对象,以包含修改后的字符串内容。而最初的String对象则丝毫不动。

看看下面的代码:
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对象作为方法的参数时,都会复制一份引用,而该引用所指的对象其实一直待在单一的物理位置上,从未动过。
    回到upcase()的定义,传入其中的引用有了名字s,只有upcase运行的时候,局部引用s才存在。一旦upcase运行结束,s就消失了。当然了,upcase()的返回值,其实只是最终结果的引用。这足以说明,upcase返回的引用已经指向了一个新的对象,而原本的q则还在原地。
    String的这种行为方式其实正是我们想要的。例如:
    String s = “asdf”;
    String x = Immutable.upcase(s);
    难道你真的希望upcase()改变其参数吗?对于一个方法而已,参数是为该方法提供参数信息的,而不是想让该方法改变自己的。
2. 重载 “+”与StringBuilder
    String对象是不可变的,你可以给一个String对象加任意多的别名。因为String对象具有只读特性,所以指向它的任何引用都不可能改变它的值,因此也就不会对其他的引用有什么影响。
    不可变会带来一定的效率问题。为String对象重载的“+”操作符就是一个例子。重载的意思是,一个操作符在应用于特定的类时,被赋予了特殊的意义(用于String的“+”和“+=”是Java中仅有的两个重载过的操作符)。
    操作符“+”可以用来连接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对象,以此类推。
    这种工作方式当然也行得通,但是为了生成最终的String,此方式会产生一大推需要垃圾回收的中间对象。可用jdk自带的工具javap来反编译以上代码。命令如下:javap -c Concatenation 这里的-c标志表示将生成jvm字节码。在字节码中需要注意的重点是:编译器自动引入了StringBuilder类。虽然我们在源代码中并没有使用StringBuilder类,但是编译器却自作主张使用了它,因为它更高效。
    在这个例子中,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符串调用一次StringBuilder的append()方法,总计四次。最后调用toString()生成结果。
    现在,也许你会觉得可以随意使用String对象,反正编译器会为你自动的优化性能,可是在这之前,让我们更深入的看看编译器能为我们优化到什么程度。下面的程序采用两种方式生成了一个String:方法一使用了多个String对象;方法二在代码中使用了StringBulider。

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();
  }
}

    现在运行javap -c WhitherStringBuilder 在字节码中implicit()方法,从第8到第35行构成了一个循环体。注意的重点是:StringBuilder是在循环之内构造的,这意味着每经过一次循环,就会创建一个StringBuilder对象。但是在explicit方法对应的字节码中,可以看到,不仅循环部分的代码更简短、更简单,而且它只生成了一个StringBuilder对象。显式的创建StringBuilder还允许你预先为其指定大小。如果你已经知道最终的字符串大概有多长,那么预先指定StringBulider的大小可以避免多次重新分配缓存。
    因此,当你为一个类编写toString()方法时,如果字符串操作比较简单,那就可以信赖编译器,它会为你合理的构造最终的字符结果。但是你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终的结果。

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 + “:” + b),那编译器就会掉入陷阱,从而为你另外创建一个StringBulider对象处理括号内的字符串操作。StringBuilder提供了丰富而全面的方法,也是JavaSE5引入的,在这之前用的是StringBuffer。后者是线程安全的,因此开销也会大一些。所以在Java5之后的字符串操作应该还会快一些。
3. 无意识的递归
    Java中的每个类从根本上都是继承自Object,标准容器类自然也不例外。因此,容器类都有toString()方法,并且复写了该方法,使得它生成的String结果能够表达容器自身,以及容器所包含的对象。例如ArrayList.toString(),它会遍历ArrayList中包含的所有对象,调用每个元素上的toString()方法。

public class ArrayListDisplay {
  public static void main(String[] args) {
    ArrayList<Coffee> coffees = new ArrayList<Coffee>();
    for(Coffee c : new CoffeeGenerator(10))
      coffees.add(c);
    System.out.println(coffees);
  }
} /* Output:
[Americano 0, Latte 1, Americano 2, Mocha 3, Mocha 4, Breve 5, Americano 6, Latte 7, Cappuccino 8, Cappuccino 9]
*/

    如果你希望toString()方法打印出对象的内存地址,也许你会考虑使用this关键字:

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对象,并将其打印出来的时候,你会得到一串非常长的异常。如果你将该InfiniteRecursion对象存入一个ArrayList中,然后打印该ArrayList,你也会得到同样的异常。其实,当如下代码运行时:" InfiniteRecursion address: " + this + "\n" 这里发生了自动类型转换,由InfiniteRecursion类型转换成String类型。因为编译器看到一个String对象后面跟着一个“+”,而再后面的对象不是String,于是编译器试着将this转换成一个String,它怎么转换呢?正是通过调用this的toString()方法,于是就发生了递归调用。
    如果你真的想要打印出对象的内存地址,应该调用Object.toString()方法,这才是负责任的方法。所以不该使用this,而是应该调用super.toString()方法。所以应该修改成如下:

public class InfiniteRecursion {
    public String toString() {
        return " InfiniteRecursion address: " + super.toString() + "\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);
    }
}

4. String上的操作(API)
    以下是String对象具备的一些基本方法。重载的方法归纳在同一行中:
  4.1构造器

String() 
    初始化一个新创建的 String 对象,使其表示一个空字符序列。 (String不可变,无需使用该构造方法)
String(byte[] bytes) 
    通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 StringString(byte[] bytes, Charset charset) 
    通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 StringString(byte[] bytes, int offset, int length) 
    通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。(offset:第一个索引;length:长度)
String(byte[] bytes, int offset, int length, Charset charset) 
    通过使用指定的 charset 解码指定的 byte 子数组,构造一个新的 StringString(byte[] bytes, String charsetName) 
    通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。(charsetName:受支持charset的名称)
String(byte[] bytes, int offset, int length, String charsetName) 
    通过使用指定的字符集解码指定的 byte 子数组,构造一个新的 StringString(char[] value) 
    分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。 
String(char[] value, int offset, int count) 
    分配一个新的 String,它包含取自字符数组参数一个子数组的字符。 
String(int[] codePoints, int offset, int count) 
    分配一个新的 String,它包含 Unicode 代码点数组参数一个子数组的字符。 
String(String original) 
    初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。 
String(StringBuffer buffer) 
    分配一个新的字符串,它包含字符串缓冲区参数中当前包含的字符序列。 
String(StringBuilder builder) 
    分配一个新的字符串,它包含字符串生成器参数中当前包含的字符序列。

  4.2 方法

 char charAt(int index) 
    返回指定索引处的 char 值。 
 int codePointAt(int index) 
    返回指定索引处的字符(Unicode 代码点)。 
 int codePointBefore(int index) 
    返回指定索引之前的字符(Unicode 代码点)。 
 int codePointCount(int beginIndex, int endIndex) 
    返回此 String 的指定文本范围中的 Unicode 代码点数。 
 int compareTo(String anotherString) 
    按字典顺序比较两个字符串。 (位于参数前:返回负数;参数后:返回正数;相等返回0)
 int compareToIgnoreCase(String str) 
    按字典顺序比较两个字符串,不考虑大小写。 
 String concat(String str) 
    将指定字符串连接到此字符串的结尾。 
 boolean contains(CharSequence s) 
    当且仅当此字符串包含指定的 char 值序列时,返回 trueboolean contentEquals(CharSequence cs) 
    将此字符串与指定的 CharSequence 比较。 
 boolean contentEquals(StringBuffer sb) 
    将此字符串与指定的 StringBuffer 比较。 
static String copyValueOf(char[] data) 
    返回指定数组中表示该字符序列的 Stringstatic String copyValueOf(char[] data, int offset, int count) 
    返回指定数组中表示该字符序列的 Stringboolean endsWith(String suffix) 
    测试此字符串是否以指定的后缀结束。 
 boolean equals(Object anObject) 
    将此字符串与指定的对象比较。 
 boolean equalsIgnoreCase(String anotherString) 
    将此 String 与另一个 String 比较,不考虑大小写。 
static String format(Locale l, String format, Object... args) 
    使用指定的语言环境、格式字符串和参数返回一个格式化字符串。 
static String format(String format, Object... args) 
    使用指定的格式字符串和参数返回一个格式化字符串。 
 byte[] getBytes() 
    使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。 
 byte[] getBytes(Charset charset) 
    使用给定的 charset 将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。 
 byte[] getBytes(String charsetName) 
    使用指定的字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。 
 void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 
    将字符从此字符串复制到目标字符数组。 
 int hashCode() 
    返回此字符串的哈希码。 
 int indexOf(int ch) 
    返回指定字符在此字符串中第一次出现处的索引。 
 int indexOf(int ch, int fromIndex) 
    返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索。 
 int indexOf(String str) 
    返回指定子字符串在此字符串中第一次出现处的索引。 
 int indexOf(String str, int fromIndex) 
     返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。 
 String intern() 
    返回字符串对象的规范化表示形式。 
 boolean isEmpty() 
    当且仅当 length() 为 0 时返回 true。 
 int lastIndexOf(int ch) 
    返回指定字符在此字符串中最后一次出现处的索引。 
 int lastIndexOf(int ch, int fromIndex) 
    返回指定字符在此字符串中最后一次出现处的索引,从指定的索引处开始进行反向搜索。 
 int lastIndexOf(String str) 
    返回指定子字符串在此字符串中最右边出现处的索引。(最后一次出现的位置) 
 int lastIndexOf(String str, int fromIndex) 
    返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。 
 int length() 
    返回此字符串的长度。 
 boolean matches(String regex) 
    告知此字符串是否匹配给定的正则表达式。 
 int offsetByCodePoints(int index, int codePointOffset) 
    返回此 String 中从给定的 index 处偏移 codePointOffset 个代码点的索引。 
 boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) 
    测试两个字符串区域是否相等。 
 boolean regionMatches(int toffset, String other, int ooffset, int len) 
    测试两个字符串区域是否相等。 
 String replace(char oldChar, char newChar) 
    返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 
 String replace(CharSequence target, CharSequence replacement) 
    使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。 
 String replaceAll(String regex, String replacement) 
    使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 
 String replaceFirst(String regex, String replacement) 
    使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 
 String[] split(String regex) 
    根据给定正则表达式的匹配拆分此字符串。 
 String[] split(String regex, int limit) 
    根据匹配给定的正则表达式来拆分此字符串。 
 boolean startsWith(String prefix) 
    测试此字符串是否以指定的前缀开始。 
 boolean startsWith(String prefix, int toffset) 
    测试此字符串从指定索引开始的子字符串是否以指定前缀开始。 
 CharSequence subSequence(int beginIndex, int endIndex) 
    返回一个新的字符序列,它是此序列的一个子序列。 
 String substring(int beginIndex) 
    返回一个新的字符串,它是此字符串的一个子字符串。 
 String substring(int beginIndex, int endIndex) 
    返回一个新字符串,它是此字符串的一个子字符串。 
 char[] toCharArray() 
    将此字符串转换为一个新的字符数组。 
 String toLowerCase() 
    使用默认语言环境的规则将此 String 中的所有字符都转换为小写。 
 String toLowerCase(Locale locale) 
    使用给定 Locale 的规则将此 String 中的所有字符都转换为小写。 
 String toString() 
    返回此对象本身(它已经是一个字符串!)。 
 String toUpperCase() 
    使用默认语言环境的规则将此 String 中的所有字符都转换为大写。 
 String toUpperCase(Locale locale) 
    使用给定 Locale 的规则将此 String 中的所有字符都转换为大写。 
 String trim() 
    返回字符串的副本,忽略前导空白和尾部空白。 
static String valueOf(boolean b) 
    返回 boolean 参数的字符串表示形式。 
static String valueOf(char c) 
    返回 char 参数的字符串表示形式。 
static String valueOf(char[] data) 
    返回 char 数组参数的字符串表示形式。 
static String valueOf(char[] data, int offset, int count) 
    返回 char 数组参数的特定子数组的字符串表示形式。 
static String valueOf(double d) 
    返回 double 参数的字符串表示形式。 
static String valueOf(float f) 
    返回 float 参数的字符串表示形式。 
static String valueOf(int i) 
    返回 int 参数的字符串表示形式。 
static String valueOf(long l) 
    返回 long 参数的字符串表示形式。 
static String valueOf(Object obj) 
    返回 Object 参数的字符串表示形式。 

5. 格式化输出
    JavaSE5推出了printf()风格的格式化输出这一功能。这不仅使得控制输出的代码更加简单,同时也给予Java开发者对于输出格式与排列更强大的控制能力。
  5.1 printf()
    C语言中的printf()并不能像Java那样连接字符串,它使用一个简单的格式化字符串,加上要插入其中的值,然后将其格式化输出。printf()并不使用重载的“+”操作符来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置。而且它还将插入格式化字符串的参数,以逗号分隔,排成一行。例如:

printf("Row 1:[%d %f]\n",x,y);

    这一行代码在运行的时候,首先将x的值插入到%d的位置,然后将y的值插入到%f的位置。这些占位符称作格式修饰符,它们不但说明了插入数据的位置,同时还说明了将插入什么类型的变量,以及如何对其格式化。在这个例子中,%d表示x是一个整数,%f表示y是一个浮点数变量(float或者double)。
  5.2 System.out.format()
    JavaSE5引入的format()方法可用于PrintStream或PrintWriter对象,其中也包括System.out对象。format()方法模仿自C的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]
*/

    可以看到,format()与printf()是等价的,它们只需要一个简单的格式化字符串,加上一串参数即可,每个参数对于一个格式修饰符。
  5.3 Formatter类
    在Java中所有新的格式化功能都由java.util.Formatter类处理。可以将Formmatter看作一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个Formatter对象的时候,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出:

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)
*/

    所有的tommy都将输出到System.out,而所有的terry则都输出到System.out的一个别名中。Formatter的构造器经过重载可以接受多种输出目的地,不过最常用的还是PrintStream()。例子中还使用了一个新的格式化修饰符%s,它表示插入的参数是String 类型。
  5.4 格式化说明符
    在插入数据时,如果想要控制空格和对齐,你需要更精细复杂的格式修饰符。语法为:

%[argument_index$][flags][width][.precision]conversion

    最常见的应用是控制一个域的最小尺寸,这可以通过指定width来实现。Formatter对象通过在必要时添加空格,来确保一个域至少达到某个长度。在默认的情况下,数据是右对齐,不过可以通过使用“-”标志来改变对齐方向。
    与width相对的是precision,它用来指明最大尺寸。width可以应用于各种类型的数据转换,并且其行为方式都一样。precision则不然,不是所有类型的数据都能使用precision,而且应用于不同类型的数据转换时,precision的意义也不同。在讲precision应用于String时,它表示打印String时输出字符的最大数量。而在将precision应用于浮点数时,它表示小数部分要显示出来的位数(默认是6位数),如果小数位数过多则舍入,太少则在尾部补零。由于整数没有小数部分,所有precision无法应用于整数,如果你对整数应用precision,则会触发异常。

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
*/

  5.5 Formatter转换
    下面的表格包含了最常用的类型转换:
    d 整数型(十进制)
    c Unicode字符
    b Boolean值
    s String
    f 浮点数(十进制)
    e 浮点数(科学计数)
    x 整数(十六进制)
    h 散列码(十六进制)
    % 字符“%”

演示:
public class Conversion {
  public static void main(String[] args) {
    Formatter f = new Formatter(System.out);

    char u = 'a';
    System.out.println("u = 'a'");
    f.format("s: %s\n", u);
    // f.format("d: %d\n", u);
    f.format("c: %c\n", u);
    f.format("b: %b\n", u);
    // f.format("f: %f\n", u);
    // f.format("e: %e\n", u);
    // f.format("x: %x\n", u);
    f.format("h: %h\n", u);

    int v = 121;
    System.out.println("v = 121");
    f.format("d: %d\n", v);
    f.format("c: %c\n", v);
    f.format("b: %b\n", v);
    f.format("s: %s\n", v);
    // f.format("f: %f\n", v);
    // f.format("e: %e\n", v);
    f.format("x: %x\n", v);
    f.format("h: %h\n", v);

    BigInteger w = new BigInteger("50000000000000");
    System.out.println(
      "w = new BigInteger(\"50000000000000\")");
    f.format("d: %d\n", w);
    // f.format("c: %c\n", w);
    f.format("b: %b\n", w);
    f.format("s: %s\n", w);
    // f.format("f: %f\n", w);
    // f.format("e: %e\n", w);
    f.format("x: %x\n", w);
    f.format("h: %h\n", w);

    double x = 179.543;
    System.out.println("x = 179.543");
    // f.format("d: %d\n", x);
    // f.format("c: %c\n", x);
    f.format("b: %b\n", x);
    f.format("s: %s\n", x);
    f.format("f: %f\n", x);
    f.format("e: %e\n", x);
    // f.format("x: %x\n", x);
    f.format("h: %h\n", x);

    Conversion y = new Conversion();
    System.out.println("y = new Conversion()");
    // f.format("d: %d\n", y);
    // f.format("c: %c\n", y);
    f.format("b: %b\n", y);
    f.format("s: %s\n", y);
    // f.format("f: %f\n", y);
    // f.format("e: %e\n", y);
    // f.format("x: %x\n", y);
    f.format("h: %h\n", y);

    boolean z = false;
    System.out.println("z = false");
    // f.format("d: %d\n", z);
    // f.format("c: %c\n", z);
    f.format("b: %b\n", z);
    f.format("s: %s\n", z);
    // f.format("f: %f\n", z);
    // f.format("e: %e\n", z);
    // f.format("x: %x\n", z);
    f.format("h: %h\n", z);
  }
} /* Output: (Sample)
u = 'a'
s: a
c: a
b: true
h: 61
v = 121
d: 121
c: y
b: true
s: 121
x: 79
h: 79
w = new BigInteger("50000000000000")
d: 50000000000000
b: true
s: 50000000000000
x: 2d79883d2000
h: 8842a1a7
x = 179.543
b: true
s: 179.543
f: 179.543000
e: 1.795430e+02
h: 1ef462c
y = new Conversion()
b: true
s: Conversion@9cab16
h: 9cab16
z = false
b: false
s: false
h: 4d5
*/

    被注释的代码表示,针对相应类型的变量,这些转换时无效的。如果执行这些转换则会触发异常。注意,程序中的每个变量都用到了b转换。虽然它对各种类型都是合法的,但其行为却不一定与你想象的一致。对于boolean基本类型或者Boolean对象,其转换结果是对于的true或false。但是对其他类型的参数,只要该参数不为null,那转换的结果就永远都是true。即使是数字0,转换结果依然为true。
  5.5 String.format()
    JavaSE5也参考了C中的printf(),以生成格式化的对象。String.format是一个静态方法,它接受与Formatter.format()一样的参数,但返回一个String对象。当你只需使用format()方法一次的时候,String.format()用起来很方便。

public class DatabaseException extends Exception {
  public DatabaseException(int transactionID, int queryID,String message) {
    super(String.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: (t3, q7) Write failed
*/

    其实在String.format内部,它也是创建一个format对象,然后将你传入的参数转给该Formatter。不过,与其自己做这些事情,不如使用便捷的String.formant()方法,何况这样的代码更清晰易懂。
6. 正则表达式
    在Java中字符串操作还主要集中在String、StringBuffer和StringTokenizer类。与正则表达式相比较,它们只能提供相当简单的功能。
  6.1 基础
    一般来说,正则表达式就是以某种方式来描述字符串,因此你可以说:“如果一个字符串含有这些东西,那么它就是我正在找的东西。”例如:要找一个数字,它可能有一个负号在最前面,那么你就写一个负号加上一个问号,就像这样: -?
    要描述一个整数,你可以说它有一位或多位阿拉伯数字。在正则表达式中,用\d表示一位数字。如果在其他语言中使用过正则表达式,那你立刻就能发现Java对反斜杠\的不同处理。在其他语言中,\\表示“我想要在正则表达式中插入一个普通的(字面上)反斜杠,请不要给它任何特殊的意义。”而Java中,\\的意思是“我要插入一个正则表达式是反斜杠,所以其后的字符具有特殊的意义。”例如:你想表示一位数字,那么正则表达式应该是\d。如果你想插入一个普通的反斜杠,则应该这样\\\\。不过换行和制表符之类的东西只需使用单反斜线:\n\t。
    要表示“一个或多个之前的表达式”,应该使用+。所以,如果要表示“可能有一个负号,后面跟着一位或者多位数字”,可以这样: -?\\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()方法,其功能是“将字符串从正则表达式匹配的地方切开。”

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, must, cut, down, the, mightiest, tree, in, the, forest..., with..., a, herring!]
[Then, when, you, have, found, the, shrubbery, you, must, cut, down, the, mightiest, tree, in, the, forest, with, a, herring]
[The, whe, you have found the shrubbery, you must cut dow, the mightiest tree i, the forest... with... a herring!]
*/

    首先看第一个语句,注意这里用的是普通的字符作为正则表达式,其中并不包含任何特殊的字符。因为第一个split()只是按空格来划分字符串。
    第二个和第三个spilt()都用到了\W,它的意思是非单词字符(如果W小写,\w,则表示一个单词字符)。通过第二个例子可以看到,它将标点字符删除了。第三个split()表示“字母n后面跟着一个或多个非单词字符。”可以看到,在原始字符串中,与正则表达式匹配的部分,在最终结果中都不存在了。
    String.split()还有一个重载的版本,它允许你限制字符串分割的次数。String类自带的最后一个正则表达式工具是“替换”。你可以只替换正则表达式第一个匹配的子串,或是替换所有匹配的部分。


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"));
  }
} /* Output:
Then, when you have located the shrubbery, you must cut down the mightiest tree in the forest... with... a herring!
Then, when you have found the banana, you must cut down the mightiest banana in the forest... with... a banana!
*/

    第一个表达式要匹配的是,以字母f开头,后面跟一个或多个字母(注意这里的w是小写)。并且只替换掉第一匹配的部分,所以,“found”被替换成“located”。
    第二个表达式要匹配的是三个单词中的任意一个,因为它们以竖线分割表示“或”的含义。
  6.2 创建正则表达式
    我们首先从正则表达式可能存在的构造集中选取一个很有用的子集,以此开始学习正则表达式。

                            字符
B                                   指定字符B
\xhh                                十六进制值为oxhh的字符
\uhhhh                              十六机制表示为oxhhhh的Unicode字符
\t                                  制表符Tab
\n                                  换行符
\r                                  回车
\f                                  换页
\e                                  转义
                            字符类
.                                   任意字符
[abc]                               包含a、b和c的任何字符(和a|b|c作用相同)
[^abc]                              除了a、b和c之外的任何字符(否定)
[a-zA-Z]                            从a到z或者A到Z的任何字符(范围)
[abc[hij]]                          任意a、b、c、h、i、j(与a|b|c|h|i|j作用相同)(合并)
[a-z&&[hij]]                        任意h、i或j(交)
\s                                  空白符(空格、tab、换行、换页和回车)
\S                                  非空白符([^\s])
\d                                  数字[0-9]
\D                                  非数字[^0-9]
\w                                  词字符[a-zA-Z0-9]
\W                                  非词字符[^\w]
                            逻辑操作符
XY                                  Y跟在X后面
X|Y                                 X或Y
(X)                                 捕获组
                            边界匹配符
^                                   一行的起始
$                                  一行的结束
\b                                  词的边界
\B                                  非词的边界
\G                                  前一个匹配的结束
演示,下面每个正则表达式都能成功匹配字符序列“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));
    }
    /**
     * true
     * true
     * true
     * true
     */
}

    当然了,我们的目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的、最简单以及最必要的正则表达式。一旦真正开始使用正则表达式了,你就会发现,在编写新的表达式之前,你通常会参考代码中已经用到的正则表达式。
  6.3 Pattern和Matcher
    一般来说,比起功能有限的String类,我们更愿意构造功能强大的正则表达式对象。只需导入java.util.regex包,然后用static Pattern.complier()方法来编译你的正则表达式即可。它会根据你的String类型的正则表达式生成一个Pattern对象。接下来,把你想要检索的字符串传入Pattern对象的matcher()方法。matcher()方法会生成一个Matcher对象,它有很多功能可用。
    作为第一个示例,下面的类可以用来检测正则表达式,看看它们能否匹配一个输入字符串。第一个控制台参数是将要用来搜索匹配的输入字符串,后面的一个或多个参数都是正则表达式,它们将被用来在输入的第一个字符串中查找匹配。在unix/linux上,命令行中的正则表达式必须用引号括起。

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));
      }
    }
  }
} /* Output:
Input: "abcabcabcdefabc"
Regular expression: "abcabcabcdefabc"
Match "abcabcabcdefabc" at positions 0-14
Regular expression: "abc+"
Match "abc" at positions 0-2
Match "abc" at positions 3-5
Match "abc" at positions 6-8
Match "abc" at positions 12-14
Regular expression: "(abc)+"
Match "abcabcabc" at positions 0-8
Match "abc" at positions 12-14
Regular expression: "(abc){2,}"
Match "abcabcabc" at positions 0-8
*/

    Pattern对象表示编译后的正则表达式。从这个例子可以看出,我们使用已编译的Pattern对象上的matcher()方法,加上一个输入字符串,加上一个输入字符串,从而构造了一个Matcher对象。同时,Pattern类还提供了static方法。

static boolean matchers(String regex,CharSequence input)

    该方法用以检查regex是否匹配整个CharSequence类型的input参数。编译后Pattern的对象还提供了split()方法,它从匹配了regex的地方分割输入字符串,返回分割后的子字符串String数组。
    通过调用pattern.matcher()方法,并传入一个字符串参数,我们得到了一个Matcher对象,使用Matcher上的方法,我们将能够判断各种不同类型的匹配是否成功:

boolean matches()
boolean lookingAt()
boolean find()
boolean find(int start)

    其中的matchers方法用来判断整个输入字符串是否匹配正则表达式模式,而lookingAt()则用来判断该字符串(不必是整个字符串)的始部分是否能够匹配模式。
    6.3.1 Matcher.find()
    Matcher.find()可用来在CharSequence中查找多个匹配。

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
*/

    模式\w+将字符串划分为单词。find()像迭代器那样前向遍历输入字符串。而第二个find()能够接受一个整数作为一个参数。该整数表示字符串中字符的位置,并以其作为搜索的起点。从结果中可以看出,后一个版本的find()能根据其参数的值,不断重新设定搜索的起始位置。
    6.3.2 组
    组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个表达式,组好1表示被第一对括号括起来的组,依次类推,因此这个表达式 A(B(C))D 有三个组:组0是ABCD,组1是BC,组2是C。
    Matcher对象提供了一系列方法,用以获取和组相关的信息:public int groupCount()返回该匹配器的模式中的分组数目,第0组不包括在内。public String group()返回前一次匹配操作的第0组。public String group(int i)返回在前一次匹配操作期间指定的组号,如果匹配成功,但是指定的组没有匹配输入字符串的任何部分,则将会返回null。public int start(int group)返回在前一次匹配操作中寻找到的组的起始索引。public int end(int group)返回在前一次匹配操作中寻找到的组的最后一个字符索引加1的值。

正则表达式组的例子:
public class Groups {
  static public final String POEM =
    "Twas brillig, and the slithy toves\n" +
    "Did gyre and gimble in the wabe.\n" +
    "All mimsy were the borogoves,\n" +
    "And the mome raths outgrabe.\n\n" +
    "Beware the Jabberwock, my son,\n" +
    "The jaws that bite, the claws that catch.\n" +
    "Beware the Jubjub bird, and shun\n" +
    "The frumious Bandersnatch.";
  public static void main(String[] args) {
    Matcher m =
      Pattern.compile("(?m)(\\S+)\\s+((\\S+)\\s+(\\S+))$")
        .matcher(POEM);
    while(m.find()) {
      for(int j = 0; j <= m.groupCount(); j++)
        printnb("[" + m.group(j) + "]");
      print();
    }
  }
} /* Output:
[the slithy toves][the][slithy toves][slithy][toves]
[in the wabe.][in][the wabe.][the][wabe.]
[were the borogoves,][were][the borogoves,][the][borogoves,]
[mome raths outgrabe.][mome][raths outgrabe.][raths][outgrabe.]
[Jabberwock, my son,][Jabberwock,][my son,][my][son,]
[claws that catch.][claws][that catch.][that][catch.]
[bird, and shun][bird,][and shun][and][shun]
[The frumious Bandersnatch.][The][frumious Bandersnatch.][frumious][Bandersnatch.]
*/

    6.3.3 split
    split()方法将输入字符串断开成字符串对象数组,断开边界有下列正则表达式确定:

String[] split(CharSequence input)
String[] split(CharSequence input, int limit)

    这是一个快速而方便的方法,可以按照通用边界断开输入文本。第二种形式的split()方法可以限制将输入分割成字符串的数量。

public class SplitDemo {
  public static void main(String[] args) {
    String input =
      "This!!unusual use!!of exclamation!!points";
    print(Arrays.toString(
      Pattern.compile("!!").split(input)));
    // Only do the first three:
    print(Arrays.toString(
      Pattern.compile("!!").split(input, 3)));
  }
} /* Output:
[This, unusual use, of exclamation, points]
[This, unusual use, of exclamation!!points]
*/
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值