Thinking in Java——String及相关类库的使用
  • 作者:ZJWave
  • 分类: java java基础
  • 发表:2019-01-29 11:32
  • 围观:2914
  • 评论:0

字符串操作是计算机程序设计中最常见的行为。

尤其是在JavaWeb系统中更是如此。本文将深入学习在Java中应用最广泛的String类,并研究与之相关的类及工具。

1.不可变String

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

看看下面的代码:

package com.zjwave.thinkinjava.string;

public class Immutable {
    public static String upcase(String s){
        return s.toUpperCase();
    }

    public static void main(String[] args) {
        String q = "holiday";
        System.out.println(q);
        String qq = upcase(q);
        System.out.println(qq);
        System.out.println(q);
    }
}

当把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中仅有的两个重载过的操作符,而Java并不允许程序员重载任何操作符)。

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

package com.zjwave.thinkinjava.string;

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

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

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

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

javap -c Concatenation.class

这里-c标志表示将生成JVM字节码。

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

可以看到:编译器自动引入了java.lang.StringBuilder类,虽然我们在源码中并没有使用StringBuilder类,但是编译后的字节码中却使用了它,原因是StringBuilder更高效。

在这个例子中,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符串调用一次StringBuilderappend()方法,总计四次。最后调用toString()生成结果,并存为s

现在,也许你会觉得可以随意使用String对象,反正编译器会为你自动地优化性能。可是在这之前,让我们更深入地看看编译器能为我们优化到什么程度。下面的程序采用两种方式生成一个String:方法一使用了多个String对象;方法二在代码中使用了StringBuilder

package com.zjwave.thinkinjava.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();
    }
}

现在运行javap -c WhitherStringBuilder.class,可以看到两个方法对应的(简化过)字节码。首先是implicit()方法:

public java.lang.String implicit(java.lang.String[]);
    Code:
       0: ldc           #2                  // String
       2: astore_2
       3: iconst_0
       4: istore_3
       5: iload_3
       6: aload_1
       7: arraylength
       8: if_icmpge     38
      11: new           #3                  // class java/lang/StringBuilder
      14: dup
      15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      18: aload_2
      19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lan
g/StringBuilder;
      22: aload_1
      23: iload_3
      24: aaload
      25: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lan
g/StringBuilder;
      28: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      31: astore_2
      32: iinc          3, 1
      35: goto          5
      38: aload_2
      39: areturn

注意从第8行到第35行构成了一个循环体。第8行:对堆栈中的操作数进行“大于或等于的整数比较运算”,循环结束时跳到第38行。第35行:返回循环体的起始点(第5行)。要注意的重点是:StringBuilder是在循环之内构造的,这意味着每经过循环一次,就会创建一个新的StringBuilder对象。

下面是explicit()方法对应的字节码:

public java.lang.String explicit(java.lang.String[]);
    Code:
       0: new           #3                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
       7: astore_2
       8: iconst_0
       9: istore_3
      10: iload_3
      11: aload_1
      12: arraylength
      13: if_icmpge     30
      16: aload_2
      17: aload_1
      18: iload_3
      19: aaload
      20: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lan
g/StringBuilder;
      23: pop
      24: iinc          3, 1
      27: goto          10
      30: aload_2
      31: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      34: areturn

可以看到,不仅循环部分的代码更简短,更简单,而且它只生成了一个StringBuilder对象。显式的创建StringBuilder还允许你预先为其指定大小。如果你已经知道最终的字符串大概有多,那预先指定StringBuilder的大小可以避免多次重新分配缓冲,从而提升性能。

因此,当你为一个类编写toString()方法时,如果字符串操作比较简单,那就可以信赖编译器,它会为你合理地构造的最终的字符串结果。但是,如果你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终的结果。请参考以下示例:

package com.zjwave.thinkinjava.string;

import java.util.Random;

public class UsingStringBuilder {
    public static Random rand = new Random(47);

    @Override
    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);
    }

}

最终的结果是用append()语句一点点拼接起来的。如果你想走捷径,例如:append(a+" : "+ c),那么编译器就会掉入陷阱,从而为你另外创建一个StringBuilder对象处理括号内的字符串操作。

事实上,Java集合类库的toString()大部分都是显式在循环体外创建了StringBuilder对象,如AbstractMaptoString()方法:

如果你拿不准该用哪种方式,随时可以用javap来分析你的程序。

StringBuilder提供了丰富而全面的方法,包括insert()replace()substring()甚至reverse(),但是最常用的还是append()toString()。还有delete()方法,上面的例子中用它删除最后一个逗号和空格,以便添加右括号。

3.无意识的递归

Java中的每个类从根本上都是继承自Object,标准容器类自然也不例外。因此容器类都有toString()方法,并且覆盖了该方法,使得它生成的String结果能够表达容器自身,以及容器所包含的对象。例如ArrayList.toString(),它会遍历ArrayList中包含的所有对象,调用每个元素上的toString()方法:

package com.zjwave.thinkinjava.string;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ArrayListDisplay {
    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        Collections.addAll(users,new User("A"),new User("B"),new User("C"));
        System.out.println(users);
    }
}

class User{
    private static int counter = 1;
    private final int id = counter++;

    String name;

    public User(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return this.getId() + " : " + this.getName();
    }
}

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

package com.zjwave.thinkinjava.string;

import java.util.ArrayList;
import java.util.List;

public class InfiniteRecursion {

    @Override
    public String toString() {
        return "InfiniteRecursion Address" + this + "\n";
    }

    public static void main(String[] args) {
        List<InfiniteRecursion> v = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            v.add(new InfiniteRecursion());
        }
        System.out.println(v);
    }
}

当你创建的InfiniteRecursion对象,并将其打印出来的时候,你会得到一串非常长的异常。如果你将该InfiniteRecursion对象存入一个ArrayList中,然后打印该ArrayList,你也会得到同样的异常。其实,当如下代码运行时:

"InfiniteRecursion Address" + this

这里自动发生了类型转换,由InfiniteRecursion类型转换成String类型。因为编译器看到一个String对象后面跟着一个“+”,而再后面的对象不是String,于是编译器试着将this转换成一个String。它怎么转换呢?正式通过调用this上的toString()方法,于是就发生了递归调用。

如果你真的想要打印出对象的内存地址,应该调用Object.toString()方法,这才是负责此任务的方法。所以,你不该使用this,而是应该调用super.toString()方法。

4.String上的操作

String对象具备的一些基本方法,读者可自行前往在线文档-jdk-zh中查看。

当余姚改变字符串的内容时,String类是方法都会返回一个新的String对象。同时,如果内容没有发生改变,String的方法只是返回指向原对象的引用而已。这可以节约存储空间以及避免而外的开销。

5.格式化输出

Java SE5中提供了C语言中printf()风格的格式化输出这一功能。这不仅使得控制输出的代码更加简单,同时也给与Java开发者对于输出格式与排列更强大的控制能力。

5.1 printf()

C语言中的printf()并不能像Java那样连接字符串,它使用一个简单的格式化字符串,加上要插入其中的值,然后将其格式化输出。printf()并不使用重载的“+”操作符(C没有重载)来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置。而且它还将插入格式化字符串的参数,以逗号分隔,排成一行。

例如:

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

这一行代码在运行的时候,首先将x的值插入到%d的位置,然后将y的值插入到%f的位置。这些占位符称作格式修饰符,它们不但说明了插入数据的位置,同时还说明了将插入什么类型的变量,以及如何对其格式化。在这个例子中,%d表示x是一个证书,%f表示y是一个浮点数(floatdouble)。

5.2 System.out.format()

Java SE5引入的format方法可用于PrintStreamPrintWriter对象,其中也包括System.out对象。format()方法模仿自C的printf()。如果你比较怀旧的话,也可以使用printf()。以下是一个简单的示例:

package com.zjwave.thinkinjava.string;

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

可以看到,format()printf()是等价的,它们只需要一个简单的格式化字符串,加上一串参数即可,每个参数对应一个格式修饰符。

5.3 Formatter类

在Java中,所有新的格式化功能都由java.util.Formatter类处理。可以将Formatter看做一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个Formatter对象的时候,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出:

package com.zjwave.thinkinjava.string;

import java.io.PrintStream;
import java.util.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);
    }
}

所有的tommy都将输出到System.out,而所有的terry则都输出到System.out的一个别名中。Formatter的构造器经过重载可以接受多种输出目的地,不过最常用的还是PrintStreamOutputStreamFile

5.4 格式化说明符

在插入数据时,如果想要控制空格与对齐,你需要更精细复杂的格式修饰符。以下是其抽象的语法:

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

最常见的应用是控制一个域的最小尺寸,这可以通过制定width来实现。Formatter对象通过在必要时添加空格,来确保一个域至少达到某个长度。在默认的情况下,数据是右对齐,不过可以通过使用“-”标志来改变对齐方向。

width相对的是precision,它用来指明最大尺寸。width可以应用于各种类型的数据转换,并且其行为方式都一样。precision则不然,不是所有类型的数据都能使用precision,而且,应用于不同类型的数据转换时,precision的意义也不同。在将precision应用于String时,它表示打印String时输出字符的最大数量。而在将precision应用于浮点数时,它表示小数部分要显示出来的位数(默认是6位小数),如果小数位过多则舍入,太少则在尾部补零。由于整数没有小数部分,所以precision无法应用于整数,如果你对整数应用precision,则会触发异常。

下面的程序应用格式修饰符来打印一个购物收据:

package com.zjwave.thinkinjava.string;

import java.util.Formatter;

public class Receipt {
    private double total = 0;
    private Formatter f = new Formatter(System.out);

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

}

5.5 Formatter转换

下面的表格包含了最常用的类型转换:

下面的程序演示了这些转换是如何工作的:

package com.zjwave.thinkinjava.string;

import java.math.BigInteger;
import java.util.Formatter;

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("500000000000000");
        System.out.println("w = new BigInteger(\"500000000000000\")");
        //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);

    }
}

被注释的代码表示,针对相应类型的变量,这些转换是无效的。如果执行这些转换,则会触发异常。

注意,程序中的每个变量都用到了b转换。虽然它对各种类型都是合法的,但其行为却不一定与你想象的一致。对于boolean基本类型或Boolean对象,其转换结果对应的truefalse。但是,对其他类型的参数,只要该参数不为null,那转换的结果就永远都是true。即使是数字0,转换结果依然为true,而这在其他语言中(包括C),往往转换为false。所以,将b应用于非布尔类型的对象时请格外小心。

还有许多不常用的类型转换与格式修饰符选项,可以在JDK文档中的Formatter类部分找到它们。

5.6 String.format()

Java SE5也参考了C中的printf()方法,以生成格式化的String对象。String.format()是一个static方法,它接受与Formatter.format()方法一样的参数,但返回一个String对象。当你只需使用format()方法一次的时候,String.format()用起来很方便。例如:

package com.zjwave.thinkinjava.string;

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 (DatabaseException e) {
            System.out.println(e);
        }
    }
}

其实在String.format()内部,它也是创建一个Formatter对象,然后将你传入的参数转给Formatter。不过,与其自己做这些事情,不如使用便捷的String.format()方法,这样使代码更清晰易读。

一个十六进制转储(dump)工具

在处理二进制文件时,我们经常希望以十六进制的格式查看其内容。现在我们就将它作为第二个例子。下面的小工具使用了String.format()方法,以可读的十六进制格式将字节数组打印出来:

package com.zjwave.thinkinjava.string;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class Hex {

    public static String format(byte[] data) {
        StringBuilder result = new StringBuilder();
        int n = 0;
        for (byte b : data) {
            if (n % 16 == 0) {
                result.append(String.format("%05X: ", n));
            }
            result.append(String.format("%02X ", b));
            n++;
            if (n % 16 == 0) {
                result.append("\n");
            }
        }
        result.append("\n");
        return result.toString();
    }

    public static void main(String[] args) throws Exception{
        File f;
        if(args.length == 0){
            // TEst by displaying this class file:
            f = new File("F:\\workspace\\apps\\thinkinjava\\out\\production\\thinkinjava\\com\\zjwave\\thinkinjava\\string\\Hex.class");
        }else{
            f = new File(args[0]);
        }
        FileInputStream fi = new FileInputStream(f);
        byte[] buffer = new byte[(int) f.length()];
        int offset = 0;
        int numRead;
        while (offset < buffer.length
                && (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) {
            offset += numRead;
        }
        fi.close();
        System.out.println(format(buffer));
    }
}

6.正则表达式

很久之前,正则表达式就已经整合到标准Unix工具集之中,例如Python和Perl。而在Java中,字符串操作还主要集中于StringStringBufferStringTokenizer类。与正则表达式相比较,它们只能提供相当简单的功能。

正则表达式是一种强大而灵活的文本处理工具。使用正则表达式,我们能够以编程的方式,构造复杂的文本模式,并对输入的字符串进行搜索。一旦找到了匹配这个模式的部门,你就能够随心所欲地对它们进行处理。初学正则表达式时,其语法是一个难点,但它确实是一种简洁、动态的语言。正则表达式提供了一种完全通用的方式,能够解决各种字符串处理相关的问题:匹配、选择、编辑以及验证。

6.1 基础

一般来说,正则表达式就是以某种方式来描述字符串,因此你可以说:“如果一个字符串含有这些东西,那么它就是我正在找的东西。”例如,要找一个数字,它可能有一个负号在最前面,那你就写一个负号加上一个问号,就像这样:

-?

要描述一个整数,你可以说它又一位或多位阿拉伯数字。在正则表达式中,用\d表示一位数字。如果在其他语言中使用过正则表达式,那你立刻就能发现Java对反斜线\的不同处理。在其他语言中,\\表示“我想要在正则表达式中插入一个普通的(字面上的)反斜线,请不要给它任何特殊的意义。”而在Java中,\\的意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。”例如,如果你想表示一位数字,那么正则表达式应该是\\d。如果你想插入一个普通的反斜线,则应该这样\\\\。不过换行和制表符之类的东西只需使用单反斜线:\n\t

要表示“一个或多个之前的表达式”,应该使用+。所以,如果要表示“可能有一个负号,后面跟着一位或多位数字”,可以这样:

-?\\d+

应用正则表达式的最简单的途径,就是利用String类内建的功能。例如,你可以检查一个String是否匹配如上所述的正则表达式:

package com.zjwave.thinkinjava.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+"));
    }
}

前两个字符串满足对应的正则表达式,匹配成功。第三个字符串开头有一个+,它也是一个合法的整数,但与对应的正则表达式却不匹配。因此,我们的正则表达式应该描述为:“可能以一个加号或减号开头”。在正则表达式中,括号有着将表达式分组的效果,而竖直线“|”则表示或操作。也就是:

(-|\\+)?

这个正则表达式表示字符串的起始字符可能是一个-或+,或者二者皆没有(因为后面跟着?修饰符)。因为字符+在正则表达式中有特殊的意义,所以必须使用\\将其转义,使之成为表达式中的一个普通字符。

String类还自带了一个非常有用的正则表达式工具——split()方法,其功能是“将字符串从正则表达式匹配的地方切开。”

package com.zjwave.thinkinjava.string;

import java.util.Arrays;

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
    }
}

首先看第一个语句,注意这里用的是普通的字符作为正则表达式,其中并不包含任何特殊的字符。因此第一个split()只是按空格来划分字符串。

第二个和第三个split()都用到了\\W,它的意思是非单词字符(如果W小写,\w,则表示一个单词字符)。通过第二个例子可以看到,它将标点字符删除了。第三个split()表示“字母n后面跟着一个或多个非单词字符。”可以看到,在原始字符串中,与正则表达式匹配的部分,在最终结果中都不存在了。

String.split()还有一个重载的版本,它允许你限制字符串分割的次数。

String类自带的最后一个正则表达式工具是“替换”。你可以只替换正则表达式第一个匹配的子串,或是替换所有匹配的地方。

package com.zjwave.thinkinjava.string;

public class Replacing {
    static String s = Splitting.knights;

    public static void main(String[] args) {
        System.out.println(s.replaceFirst("f\\w+","located"));
        System.out.println(s.replaceAll("shrubbery|tree|herring","banana"));
    }
}

第一个表达式要匹配的是,以字母f开头,后面跟一个或多个字母(注意这里的w是小写的)。并且只替换掉第一个匹配的部分,所以“found”被替换成“located”。

第二个表达式要匹配的是三个单词中的任意一个,因为它们以竖直线分隔表示“或”并且替换所有匹配的部分。

稍后你会看到,String之外的正则表达式还有更强大的替换工具,例如,可以通过方法调用执行替换。而且,如果正则表达式不是只是用一次的话,非String对象的正则表达式明显具备更佳的性能。

6.2 创建正则表达式

我们首先从正则表达式可能存在的构造集中选取一个很有用的子集,以此开始学习正则表达式。正则表达式的完整构造字列表,请参考JDK文档的java.util.regex包中的Pattern类。

当你学会了使用字符类(character classes)之后,正则表达式的威力才能真正显现出来。以下是一些创建字符类的典型方式,以及一些预定义的类:

这里只列出了部分常用的表达式,你应该将java.util.regex.Pattern那一页加入浏览器书签中,以便在需要的时候方便查询。

作为演示,下面的每一个正则表达式都能成功匹配字符序列“Rudolph”:

package com.zjwave.thinkinjava.string;

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

当然了,我们的目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的、最简单以及最必要的正则表达式。一旦真正开始使用正则表达式了,你就会发现,在编写新的正则表达式之前,你通常会参考代码中已经用到的正则表达式。

6.3 量词

量词描述了一个模式吸收输入文本的方式:

  • 贪婪型:量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组,如果它是贪婪的,那么它就会继续往下匹配。
  • 勉强性:用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也称作懒惰的、最少匹配的、非贪婪的、或不贪婪的。
  • 占有型:目前,这种类型的两次只有在Java语言中才可用(在其他语言中不可用),并且也更高级,因此我们大概不会立刻用到它。当正则表达式被应用于字符串时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有的”两次并不保存这些中间状态,因此它们可以防止回溯。它们常常用于防止正则表达式时空,因此可以使正则表达式执行起来更有效。

应该非常清楚地意识到,表达式X通常必须要用圆括号括起来,以便它能够按照我们期望的效果去执行,例如:

abc+

看起来它似乎应该匹配1个或多个abc序列,如果我们把它应用于输入字符串abcabcabc,则实际上会获得3个匹配。然而,这个表达式实际上表示的是:匹配ab,后面跟随1个或多个c。要表明匹配1个或多个完成的abc字符串,我们必须这样表示

(abc)+

你会发现,在使用正则表达式时很容易混淆。

CharSequence

接口CharSequenceCharBufferStringStringBufferStringBuilder类之中抽象出了字符序列的一般化定义:

因此,这些类都实现了该接口。多数正则表达式操作都接受CharSequence类型的参数。

6.4 Pattern和Matcher

一般来说,比起功能有限的String类,我们更愿意构造功能强大的正则表达式对象。只需导入java.util.regex包,然后用static Pattern.compile()方法来编译你的正则表达式即可。它会根据你的String类型的正则表达式生成一个Pattern对象。接下来,把你想要检索的字符串传入Pattern对象的matcher()方法。matcher()方法会生成一个Matcher对象,它有很多功能可用(可以参考java.util.regex.Matcher的JDK文档)。例如,它的replaceAll()方法能将所有匹配的部分都替换成你传入的参数。

作为第一个示例,下面的类可以用来测试正则表达式,看看它们能否匹配一个输入字符串。第一个控制台参数是将要用来搜索匹配的输入字符串,后面的一个或多个参数都是正则表达式,它们将被用来在输入的第一个字符串中查找匹配。在Unix/Linux上,命令行中的正则表达式必须用引号括起。这个程序在测试正则表达式时很有用,特别是当你想验证它们是否具备你所期待的匹配功能的时候。

package com.zjwave.thinkinjava.string;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TestRegularExpression {

    //args: abcabcabcdefabc abc+ (abc)+ (abc){2,}
    public static void main(String[] args) {
        if (args.length < 2) {
            System.out.println("Usage:\njava TestRegularExpression characterSequence regularExpression+");
            System.exit(0);
        }
        System.out.println("Input \" " + args[0] + "\"");
        for (String arg : args) {
            System.out.println("Regular expression \"" + arg + "\"");
            Pattern p = Pattern.compile(arg);
            Matcher m = p.matcher(args[0]);
            while (m.find()){
                System.out.println("Match \"" + m.group() + "\" at positions " + m.start() + "-" + (m.end() - 1) );
            }
        }
    }
}

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

public static boolean matches(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()则用来判断该字符串(不必是整个字符串)的部分是否能够匹配模式。

find()

Matcher.find()方法可用来在CharSequence中查找多个匹配。例如:

package com.zjwave.thinkinjava.string;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

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()){
            System.out.print(m.group() + " ");
        }
        System.out.println();
        int i = 0;
        while (m.find(i)){
            System.out.print(m.group() + " ");
            i++;
        }
    }
}

模式\\w+将字符串划分为单词。find()像迭代器那样前向遍历输入字符串。而第二个find()能够接收一个整数作为参数,该整数表示字符串中字符的位置,并以其作为搜索的起点。从结果中可以看出,后一个版本的find()方法能根据其参数的值,不断重新设定搜索的起始位置。

组(Groups)

组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个表达式,组号1表示被第一对括号括起的组,以此类推。因此,在下面这个表达式:

A(B(C))D

中有三个组:组0是ABCD,组1是BC,组2是C

Matcher对象提供了一系列方法,用以获取与组相关的信息:

  • public int groupCount(),返回该匹配器的模式中的分组数目,第0组不包括在内。
  • public String groupd(),返回前一次匹配操作(例如find())的第0组(整个匹配)。
  • public String group(int i),返回在前一次匹配操作期间指定的组号,如果匹配成功,但是指定的组没有匹配输入字符串的任何部分,则将会返回null。
  • public int start(int group),返回在前一次匹配操作中寻找到的组的起始索引。
  • public int end(int group),返回在前一次匹配操作中寻找到的组的最后一个字符索引加一的值。

下面是正则表达式组的例子:

package com.zjwave.thinkinjava.string;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Groups {
    public static 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 i = 0; i <= m.groupCount(); i++) {
                System.out.print("[" + m.group(i) + "]");
            }
            System.out.println();
        }
    }
}

可以看到这个正则表达式模式有许多圆括号分组,由任意数目的非空格字符(\S+)及随后的任意数目的空格字符(\s+)所组成。目的是捕获每行的最后3个词,每行最后以$结束。不过,在正常情况下是将$与整个输入序列的末端相匹配。所以我们一定要显示地告知正则表达式注意输入序列中的换行符。这可以由序列开头的模式标记(?m)来完成。

start()与end()

在匹配操作成功之后,start()返回先前匹配的起始位置的索引,而end()返回所匹配的最后字符的索引加一的值。匹配操作失败之后(或先于一个正在进行的匹配操作去尝试)调用start()end()将会产生IllegalStateException。下面的示例还同时展示了matches()lookingAt()的用法:

package com.zjwave.thinkinjava.string;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class StartEnd {
    public static String input =
            "As long as there is injustice, whenever a \n" +
                    "Targathian baby cries out, wherever a distress\n" +
                    "signal sounds among the starts ... We'll be there.\n" +
                    "This fine ship, and this fine crew ...\n" +
                    "Never give up! Never surrender!";

    private static class Display {
        private boolean regexPrinted = false;
        private String regex;

        Display(String regex) {
            this.regex = regex;
        }

        void display(String message){
            if(!regexPrinted){
                System.out.println(regex);
                regexPrinted = true;
            }
            System.out.println(message);
        }
    }

    static void examine(String s ,String regex){
        Display d = new Display(regex);
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(s);
        while(m.find()){
            d.display("find() '" + m.group() + "' start = " + m.start() + " end = " + m.end());
        }
        if(m.lookingAt()){//No reset() necessary
           d.display("lookingAt() start = " + m.start() + " end = " + m.end());
        }
        if(m.matches()){//No reset() necessary
            d.display("matches() start = " + m.start() + " end = " + m.end());
        }
    }

    public static void main(String[] args) {
        for (String in : input.split("\n")) {
            System.out.println("input : " + in);
            for (String regex : new String[]{"\\w*ere\\w*", "\\w*ever", "T\\w+", "Never.*?!"}) {
                examine(in,regex);
            }
        }
    }

}

注意,find()可以在输入的任意位置定位正则表达式,而lookingAt()matches()只有在正则表达式与输入的最开始出就开始匹配时才会成功。matches()只有在整个输入都匹配正则表达式时才会成功,而lookingAt()只要输入的第一部分匹配就会成功。

Pattern标记

Pattern类的compile()方法还有另一个版本,它接受一个表及参数,以调整匹配的行为:

Pattern compile(String regex, int flags) 

其中的flag来自以下Pattern类中的常量:

在这些标记中,Pattern.CASE_INSENSITIVEPattern.MULTILINE以及Pattern.COMMENTS(对声明或文档有用)特别有用。请注意,你可以直接在正则表达式中使用其中的大多数标记,只需要将商标中括号括起的字符插入到正则表达式中你希望它起作用的位置即可。

你还可以通过“或”(|)操作符组合多个标记的功能:

package com.zjwave.thinkinjava.string;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReFlags {
    public static void main(String[] args) {
        Pattern p = Pattern.compile("^java", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
        Matcher m = p.matcher("java has regex\nJava has regex\n" +
                "JAVA has pretty good regular expressions\n" +
                "Regular expressions are is Java");
        while (m.find()){
            System.out.println(m.group());
        }
    }
}

在这个例子中,我们创建了一个模式,它将匹配所有以“java”、“Java”和“JAVA”等开头的行,并且是在设置了多行标记的状态下,对每一个行(从字符序列的第一个字符开始,到每一个行终结符)都进行匹配。注意,group()方法值返回已匹配的部分。

6.5 split()

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

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

这是一个快速而方便的方法,可以按照通用边界断开输入文本:

package com.zjwave.thinkinjava.string;

import java.util.Arrays;
import java.util.regex.Pattern;

public class SplitDemo {
    public static void main(String[] args) {
        String input = "This!!unusual use!!of exclamation!!points";
        System.out.println(Arrays.toString(Pattern.compile("!!").split(input)));
        //Only do the first three:
        System.out.println(Arrays.toString(Pattern.compile("!!").split(input,3)));
    }
}

第二种形式的split()方法可以限制将输入分割成字符串的数量。

6.6 替换操作

正则表达式特别便于替换文本,它提供了许多方法:replaceFirst(String replacement)以参数字符串replacement替换掉第一个匹配成功的部分。

replaceAll(String replacement)以参数字符串replacement替换掉第一个匹配成功的部分。appendReplacement(StringBuffer sb, String replacement)执行渐进式的替换,而不是像replaceFirst()replaceAll()那样只替换第一个匹配或全部匹配。这是一个非常重要的方法。它允许你调用其他方法来生成或处理replacementreplaceFirst()replaceAll()则只能使用一个固定的字符串),是你能够以编程的方式将目标分割成组,从而具备更强大的替换功能。appendTail(StringBuffer sb),在执行了一次或多次appendReplacement()之后,调用此方法可以将输入字符串余下的部分复制到sb中。

下面的程序演示了如何使用这些替换方法。开头部分注释掉的文本,就是正则表达式要处理的输入字符串。

package com.zjwave.thinkinjava.string;

/*! Here's a block of text to use as input to
    the regular expression matcher. Note that we'll
    first extract the block of text by looking for
    the special delimiters, then process the
    extracted block. !*/

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TheReplacements {
    public static void main(String[] args) throws Exception{
        File f = new File("F:\\workspace\\apps\\thinkinjava\\src\\com\\zjwave\\thinkinjava\\string\\TheReplacements.java");
        String s = readFile(f);
        //Match the specially commented block of text above:
        Matcher mInput = Pattern.compile("/\\*!(.*)!\\*/", Pattern.DOTALL).matcher(s);
        if(mInput.find()){
            s = mInput.group(1);// Captured by parentheses
        }
        // Replace two ore more spaces with a single space:
        s = s.replaceAll(" {2,}","");
        // Replace one or more spaces at the beginning of each
        // line with no spaces. Must enable MULTILINE mode:
        s.replaceAll("(?m) ^+","");
        System.out.println(s);
        s = s.replaceFirst("[aeiou]","(VOWEL1)");
        StringBuffer sbuf = new StringBuffer();
        Pattern p = Pattern.compile("[aeiou]");
        Matcher m = p.matcher(s);
        // Process the find information as you
        // perform the replacements:
        while (m.find()){
            m.appendReplacement(sbuf,m.group().toUpperCase());
        }
        // Put in the remainder of the text:
        m.appendTail(sbuf);
        System.out.println(sbuf);
    }

    private static String readFile(File f) throws Exception{
        byte[] strBuffer;
        InputStream in = null;
        try{
            in = new FileInputStream(f);
            int flen = (int)f.length();
            strBuffer = new byte[flen];
            in.read(strBuffer, 0, flen);
        }finally {
            in.close();
        }
        return new String(strBuffer);
    }
}

mInput用以匹配在/*!和!*/之间的所有文字(注意分组的括号)。接下来,将存在两个或两个以上空格的地方,缩减为一个空格,并且删除每行开头部分的所有空格(为了使每一行都达到这个效果,而不仅仅只是删除文本开头部分的空格,这里特意打开了多行状态)。这两个替换操作所使用的replaceAll()String对象自带的方法,在这里,使用此方法更方便。注意,因为这两个替换操作都只是用了一次replaceAll(),所以,与其编译为Pattern,不如直接使用StringreplaceAll()方法,而且开销也更小些。

replaceFirst()只对找到的第一个匹配进行替换。此外,replaceFirst()replaceAll()方法用来替换的只是普通的字符串,所以,如果想对这些字符串执行某些特殊处理,这两个方法是无法胜任的。如果你想要那么做,就应该使用appendReplacement()方法。该方法允许你在执行替换的过程中,操作用来替换的字符串。在这个例子中,先构造了sbuf用来保存最终结果,然后用group()选择一个组,并对其进行处理,将正则表达式找到的元音字母转换成大写字母。一般情况下,你应该遍历执行所有的替换操作,然后再调用appendTail()方法,但是,如果你想模拟replaceFirst()(或替换n次)的行为,那就只需执行一次替换,然后调用appendTail()方法,将剩余未处理的部分存入sbuf即可。

同时,appendReplacement()方法还允许你通过$g直接找到匹配的某个组,这里的g就是组号。然而,它只能应付一些简单的处理,无法实现类似前面这个例子中的功能。

6.7 reset()

通过reset()方法,可以将现有的Matcher对象应用于一个新的字符序列:

package com.zjwave.thinkinjava.string;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Resetting {

    public static void main(String[] args) {
        Matcher m = Pattern.compile("[frb][aiu][gx]").matcher("fix the rug with bags");
        while (m.find()) {
            System.out.print(m.group() + " ");
        }
        System.out.println();
        m.reset("fix the rig with rags");
        while (m.find()) {
            System.out.print(m.group() + " ");
        }
    }

}

使用不带参数的reset()方法,可以将Matcher对象重新设置到当前字符序列的起始位置。

6.8 正则表达式与Java I/O

到目前为止,我们看到的例子都是将正则表达式应用于静态的字符串。下面的例子将向你演示,如何应用正则表达式在一个文件中进行搜索匹配操作。JGrep.java的灵感来自于Unix上的grep。它有两个参数:文件名以及要匹配的正则表达式。输出的是有匹配的部分以及匹配部分在行中的位置。

package com.zjwave.thinkinjava.string;

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// A very simple version of the "grep" program.
// {Args: JGrep.java "\\b[Ssct]\\w+"}
public class JGrep {

    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("Usage: java JGrep file regex");
            System.exit(0);
        }
        BufferedReader in = new BufferedReader(new FileReader(args[0]));
        Pattern p = Pattern.compile(args[1]);
        // Iterate through the lines of the input file:
        int index = 0;
        Matcher m = p.matcher("");
        String line;
        while ((line = in.readLine()) != null){
            m.reset(line);
            while (m.find()){
                System.out.println(index++ + ": " + m.group() + ": " + m.start());
            }
        }
    }
}

上述例子虽然也可以在循环内部创建新的Matcher对象,但是,在for循环外创建一个空的Matcher对象,然后用reset()方法每次为Matcher加载一行输入,这种处理会有一定的性能优化。最后用find()搜索结果。这里读入的测试参数是JGrep.java文件,然后搜索以[Ssct]开头的单词。

正则表达式的特点是:

  1. 灵活性、逻辑性和功能性非常强;
  2. 可以迅速地用极简单的方式达到字符串的复杂控制。
  3. 对于刚接触的人来说,比较晦涩难懂。

由于正则表达式主要应用对象是文本,因此它在各种文本编辑器场合都有应用,小到著名编辑器EditPlus,大到Microsoft Word、Visual Studio等大型编辑器,都可以使用正则表达式来处理文本内容。

如果想要更深入的学习正则表达式,可以从网上找到很多有用的信息。

7. 扫描输入

到目前为止,从文件或标准输入读取数据还是一件相当痛苦的事情。一般的解决之道就是读入一行文本,对其进行粉刺,然后使用Integer、Double等类的各种解析方法来解析数据:

package com.zjwave.thinkinjava.string;

import com.zjwave.thinkinjava.exception.InputFile;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class SimpleRead {
    public static BufferedReader input = new BufferedReader(new StringReader("Sir Robin of Camelot\n22 1.61803"));

    public static void main(String[] args) {
        try {
            System.out.println("What is your name?");
            String name = input.readLine();
            System.out.println(name);
            System.out.println("How old are you? What is your favorite double?");
            System.out.println("(input: <age> <double>)");
            String numbers = input.readLine();
            System.out.println(numbers);
            String[] numArray = numbers.split(" ");
            int age = Integer.parseInt(numArray[0]);
            double favorite = Double.parseDouble(numArray[1]);
            System.out.format("Hi %s.\n",name);
            System.out.format("In 5 years you will be %d.\n",age + 5);
            System.out.format("My favorite double is %f.",favorite / 2);
        }catch (IOException e){
            System.err.println("I/O Exception");
        }
    }
}

input元素使用的类来自java.ioStringReaderString转化为可读的流对象,然后用这个对象来构造BufferedReader对象。

readLine()方法将一行输入转为String对象。如果每一行数据正好对应一个输入值,那这个方法也还可行。但是,如果两个输入值在同一行中,事情就不好办了,我们必须分解这个行,才能分别翻译所需的输入值。在这个例子中,分解的操作发生在创建numArray时。

在Java SE5新增了Scanner类,它可以大大减轻扫描输入的工作负担:

package com.zjwave.thinkinjava.string;

import java.io.IOException;
import java.util.Scanner;

public class BetterReader {
    public static void main(String[] args) {
        Scanner stdin = new Scanner(SimpleRead.input);
        System.out.println("What is your name?");
        String name = stdin.nextLine();
        System.out.println(name);
        System.out.println("How old are you? What is your favorite double?");
        System.out.println("(input: <age> <double>)");
        int age = stdin.nextInt();
        double favorite = stdin.nextDouble();
        System.out.println(age);
        System.out.println(favorite);
        System.out.format("Hi %s.\n", name);
        System.out.format("In 5 years you will be %d.\n", age + 5);
        System.out.format("My favorite double is %f.", favorite / 2);
    }
}

Scanner的构造器可以接受任何类型的输入对象,包括FileInputStreamString或者像此例中的Readable对象。Readable是Java SE5中新加入的一个接口,表示“具有read()方法的某种东西”。前一个例子中的BufferedReader也归于这一类。有了Scanner,所有的输入、分词以及翻译的操作都隐藏在不同类型的next方法中。普通的next()方法返回下一个String。所有的基本类型(除char以外)都有对应的next方法,包括BigDecimalBigInteger。所有的next方法,只有在找到一个完整的分词之后才会返回。Scanner还有相应的hasNext方法,用以判断下一个输入分词是否具有所需的类型。

BetterReader.java没有针对IOException添加try区块。因为,Scanner有一个假设,在输入结束时会抛出IOException,所以Scanner会把IOException吞掉。不过,通过ioException()方法,你可以找到最近发生的异常,因此,你可以在必要时检查它。

7.1 Scanner定界符

在默认的情况下,Scanner根据空白字符对输入进行粉刺,但是你可以用正则表达式指定自己所需的定界符:

package com.zjwave.thinkinjava.string;

import java.util.Scanner;

public class ScannerDelimiter {
    public static void main(String[] args) {
        Scanner scanner = new Scanner("12,42,78,99,42");
        scanner.useDelimiter("\\s*,\\s*");
        while (scanner.hasNextInt()){
            System.out.println(scanner.nextInt());
        }
    }
}

这个例子使用逗号(包括逗号前后任意的空白字符)作为定界符,同样的技术也可以用来读取逗号分隔的文件。我们可以用useDelimiter()来设置定界符,同时,还有一个delimiter()方法,用来返回当前正在作为定界符使用的Pattern对象。

7.2 用正则表达式扫描

除了能够扫描基本类型之外,你还可以使用自定义的正则表达式进行扫描,这在扫描复杂数据的时候非常有用。下面的例子将扫描一个防火墙日志文件中记录的威胁数据:

package com.zjwave.thinkinjava.string;

import java.util.Scanner;
import java.util.regex.MatchResult;

public class ThreatAnalyzer {
    static String threatData =
            "58.27.82.161@02/10/2005\n" +
                    "204.45.234.40@02/11/2005\n" +
                    "58.27.82.161@02/11/2005\n" +
                    "58.27.82.161@02/12/2005\n" +
                    "58.27.82.161@02/12/2005\n" +
                    "[next log section with different data format]";

    public static void main(String[] args) {
        Scanner scanner = new Scanner(threatData);
        String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@" +
                "(\\d{2}/\\d{2}/\\d{4})" ;
        while (scanner.hasNext(pattern)){
            scanner.next(pattern);
            MatchResult match = scanner.match();
            String ip = match.group(1);
            String date = match.group(2);
            System.out.format("Threat on %s from %s\n",date,ip);
        }
    }
}

next()方法配合指定的正则表达式使用时,将找到下一个匹配该模式的输入部分,调用match()方法就可以获得匹配的结果。如上所示,它的工作方式与之前看到的正则表达式匹配相似。在配合正则表达式使用扫描时,有一点需要注意:它仅仅针对下一个输入分词进行匹配,如果你的正则表达式中含有定界符,那永远都不可能匹配成功。

8.StringTokenizer

在java引入正则表达式和Scanner类之前,分割字符串的唯一方法是使用StringTokenizer来分词。不过,现在又了正则表达式和Scanner,我们可以使用更加简单、简洁的方式来完成同样的工作了。下面的例子中,我们将StringTokenizer与另外两种技术做了一个比较:

package com.zjwave.thinkinjava.string;

import java.util.Arrays;
import java.util.Scanner;
import java.util.StringTokenizer;

public class ReplacingStringTokenizer {
    public static void main(String[] args) {
        String input = "But I'm not dead yet! I fell happy!";
        StringTokenizer stoke = new StringTokenizer(input);
        while (stoke.hasMoreElements()){
            System.out.print(stoke.nextToken() + " ");
        }
        System.out.println();
        System.out.println(Arrays.toString(input.split(" ")));
        Scanner scanner = new Scanner(input);
        while (scanner.hasNext()){
            System.out.print(scanner.next() + " ");
        }
    }
}

使用正则表达式或Scanner对象,我们能够以更加复杂的模式来分割一个字符串,而这对于StringTokenizer来说就很困难了。基本上,我们可以放心的说,StringTokenizer已经可以废弃不用了。

9.总结

过去,Java对字符串操作的支持相当不完善。不过随着近几个版本的升级,我们可以看到,Java已经从其他语言中西区了许多成熟的经验。到目前为止,它对字符串操作的支持已经很完善了。不过,有时你还要在细节上注意效率的问题,例如恰当地使用StringBuilder等。

 

所有源码均可在https://gitee.com/zjwave/thinkinjava中下载

关联文章:

Thinking in Java——集合(容器)基础

Thinking in Java——Java异常体系(通过异常处理错误)

Thinking in Java——运行时类型信息(RTTI)以及反射

Thinking in Java——泛型

Thinking in Java——数组

Thinking in Java——集合(容器)深入研究

Thinking in Java——Java I/O系统

Thinking in Java——枚举

Thinking in Java——注解

Thinking in Java——并发

转载请注明原文链接:ZJ-Wave

Top