字符串

第13章 字符串

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

13.1 不可变String

String对象是不可变的。String类中每一个看似会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容:

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

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

可以发现,我们将q传递给upcase()方法,但经过String.toUpperCase()返回的引用和原引用却指向了不同的对象。

13.2 重载"+"与StringBuilder

重载操作符:一个操作符在应用于特定的类时,被赋予了特殊的意义。 例如,操作符"+"可以用来连接String:

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

我们可以使用JDK自带的工具javap来反编译以上代码:

javap -c Concatenation

可以发现:编译器自动引入了java.lang.StringBuilder类。在上例中,编译器创建了一个StringBuilder对象,并为每个字符串调用一次StringBuilder的append()方法,最后调用toString()生成结果。

然而,并非在任何情况下,编译器都能做到优化。下例是在循环中进行字符串操作:

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 eplicit(String[] fields) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < fields.length; i++) 
            result.append(fields[i]);
        return result.toString();
    }
}

第一个方法使用重载"+"进行连接字符串:StringBuilder是在循环之内构造的,即每经过一次循环,就会创建一个新的StringBuilder对象。

第二个方法则在循环外先创建了StringBuilder,并且显式地创建还允许预先为其指定大小。

因此,如果字符串操作比较简单,可以使用重载"+"连接字符串,而如果需要使用循环,则最好在循环外显式地创建StringBuilder:

public class UsingStringBuilder {
    public static Random rand = new Random(66);
    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);
    }
}

StringBuilder还具有以下方法:

  • insert()
  • repleace()
  • subString()
  • reverse()
  • append()
  • toString()
  • delete()

StringBuilder是Java SE5引入的,在此之前使用的是线程安全的StringBuffer。

13.3 无意识的递归

所有的容器类都覆写了toString()方法,使得它能表达容器和容器所包含的对象:

public class ArrayListDisplay {
    public static void main(String[] args) {
        ArrayList<Coffee> coffees = new ArrayList<Coffee>();
        for (Coffee coffee : new CoffeeGenerator(10)) 
            coffees.add(coffee);
        System.out.println(coffees);
    }
}

如果试图使用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);
    }
}

其原因是:使用this则会调该对象的toString()方法,即发生递归调用。正确的做法应该是调用super.toString()方法,即Object.toString()。

13.4 String上的操作

以下是String对象具备的一些基本方法:

  • length():String中字符的个数
  • charAt(Int):获取String中指定索引位置上的字符
  • getBytes():将字符串转换为字节数组
  • toCharArray():将字符串转换为字符数组
  • equals(String):比较字符串内容
  • equalsIgnoreCase(String):比较字符串内容,忽略大小写
  • compareTo():按词典顺序比较String内部,大小写敏感
  • contains(CharSequence):字符串是否包含指定字符序列
  • contentEquals(CharSequence):比较字符串与指定字符序列内容
  • regionMatcher():比较当前字符串在指定偏移量后和指定字符串在偏移量后是否一致。
  • startsWith(String):当前字符串是否以指定字符串开头
  • endWith(String):当前字符串是否以指定字符串结尾
  • indexOf():从前往后搜索当前字符串,如果包含指定参数,返回索引,不包含,返回-1
  • lastIndexOf():从后向前搜索当前字符串,包含,返回索引,不包含,返回-1
  • subString():根据指定索引,截取字符串
  • contact(String):当前字符串连接指定字符串,并返回一个新字符串对象
  • replace(char,char):使用新字符替换旧字符,并返回新字符串对象
  • toLowerCase():将字符串中所有字符变为小写,并返回新字符串对象
  • toUpperCase():将字符串中所有字符变为大写,并返回新字符串对象
  • trim():去除字符串两段空格
  • valueOf():将参数转换为String,并返回
  • intern():为每个唯一的字符序列生成且仅生成一个String引用

13.5 格式化输出

Java SE5推出了格式化输出功能。

13.5.1 printf()

C语言中的printf()通过特殊的占位符来表示数据将来的位置,将插入格式化字符串的参数,以逗号分隔,排成一排:

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

这些占位符称作格式修饰符,它们不但说明了插入数据的位置,同时还说明了将插入什么类型的变量,以及如何对其格式化。

13.5.2 System.out.format()

Java SE5引入的format方法可用于PrintStream或PrintWriter对象。format()方法模仿自printf()方法,下面是一个简单示例:

public class SImpleFormat {
    public static void main(String[] args) {
        int x = 5;
        double y = 5.332542;
        System.out.println("Row 1: [" + x + " " + y + "]");
        System.out.printf("Row 1: [%d %f]\n", x, y);
        System.out.format("Row 1: [%d %f]\n", x, y);
    }
}

13.5.3 Formatter类

在Java中,所有新的格式化功能都是由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(5, 8);
        tommy.move(3, 9);
        terry.move(9, 9);
    }
}

Formatter的构造器经过重载可以接收多种输出的目的地,最常用的有:PrintStream、OutputStream和File。

13.5.4 格式化说明符

以下是格式化修饰符抽象的语法:

%[argyment_index$][flags][width][.precision]conversion
  • width:控制一个域最小尺寸。尺寸不足时,默认使用空格补足。
  • -:默认情况下,数据是右对其。"-"用来改变对齐方向。
  • precision作用于String:表示打印String时输出字符的最大数量。
  • precision作用于浮点数:表示小数部分要显示的位数,默认6位。
  • 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();
    }
}

13.5.5 Formatter转换

一下是最常用的类型转换字符:

  • d:整数型(十进制)
  • c:Unicode字符
  • b:Boolean值
  • s:String
  • f:浮点数(十进制)
  • e:浮点数(科学计数)
  • h:整数(十六进制)
  • %:字符"%"

13.5.6 String.format()

String.format()是一个静态方法,它接收与Formatter.format()方法一样的参数,但返回一个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 (Exception e) {
            System.out.println(e);
        }
    }
}

事实上,在String.format()内部,依然是创建了Formmater对象。

一个十六进制转储工具

下面的例子使用了String.format()方法,以可读的十六进制格式将字节数组打印出来:

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 IOException {
            if(args.length == 0) 
                System.out.println(format(BinaryFile.read("Hex.class")));
            else
                System.out.println(format(BinaryFile.read(args[0])));
    }
}

13.6 正则表达式

正则表达式是一种强大而灵活的文本处理工具。使用正则表达式,我们能够以编程的方式构造复杂的文本模式,并对输入字符串进行搜索。正则表达式提供了一种完全通用的方式,能够解决各种字符串处理相关的问题:匹配、选择、编辑以及验证。

13.6.1 基础

一般来说,正则表达式就是以某种方式来描述字符串。

在Java中,使用\表示正则表达式的反斜线。

  • \d:表示一位数字。
  • +:表示一个或多个之前的表达式。
  • -?\d+:表示可能以负号开头,后面紧跟着一位或多位数字

下面的例子分别演示了String类内建的应用正则表达式的功能。

matches():当前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+"));
    }
}

由于字符+在正则表达式中有特殊意义,所有必须使用\将其转义。

split():将字符串从与指定正则表达式匹配的地方分割。

public class Splitting {
    public static String knights = "Then, when you have found tghe shrubbery, "
            + "you must cust down the  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(" ");
        split("\\W+");
        split("n\\W+");
    }
}

上述代码中的几个正则表达式分别表示:

  • \W:非单词字符
  • \w:单词字符
  • n\W+:字母n后面跟着一个或多个非单词字符

replaceFirst()、replaceAll():替换与正则表达式匹配的子串。

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+:字母f后面跟着一个或多个字母
  • shrubbery|tree|herring:三个单词中的任意一个。竖直线表示或。

13.6.2 创建正则表达式

正则表达式的完整构造子列表,请参考JDK文档java.util.regex包中的Pattern类。以下是一些常用的构造集:

字符:

  • B:指定字符B
  • \xhh:十六进制为oxhh的字符
  • \uhhhh:十六进制表示为oxhhhh的Unicode字符
  • \t:制表符Tab
  • \n:换行符
  • \r:回车
  • \f:换页
  • \e:转义

下面是一些创建字符类的典型方式,以及一些预定义的类:

  • .:任意字符
  • *:0个或多个
  • [abc]:包含a、b和c的任意字符,和a|b|c作用相同
  • [^abc]:除a、b和c之外的任意字符
  • [a-zA-Z]:从a到z或从A到Z的任意字符
  • [abc[hij]]:或
  • [a-z&&[hij]]:交
  • \s:空白符
  • \S:非空白符
  • \d:数字[0-9]
  • \D:非数字
  • \w:字母
  • \W:非字母

逻辑操作符:

  • XY:Y跟在X后面
  • X|Y:X或Y
  • (X):捕获组

边界匹配符:

  • ^:一行的起始
  • $:一行的结束
  • \b:字母边界
  • \B:非字母边界
  • \G:前一个匹配的结束

下面的例子演示了正则表达式的用法:

public class Rudolph {
    public static void main(String[] args) {
        String[] patterns = new String[]{"Rudolph","[rR]udolph",
                "[rR][aeiou][a-z]ol.*","R.*"};
        for (String pattern : patterns) 
            System.out.println("Rudolph".matches(pattern));
    }
}

13.6.3 量词

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

  • 贪婪型
  • 勉强型
  • 占有型

多数正则表达式操作都接收CharSequence类型的参数,该接口是从CharBuffer、String、StringBuffer、StringBuilder类之中抽象处的字符序列的一般定义:

public interface CharSequence {
    char charAt(int index);
    int length();
    CharSequence subSequence(int start, int end);
    public String toString();
}

13.6.4 Pattern和Matcher

String类对正则表达式的支持十分有限,我们一般使用功能强大的Pattern类来处理正则表达式。

下例通过输入控制台参数来测试正则表达式:

// Args:abcabcabcdefabc "abc+" "(abc)+" "(abc){2,}"
public class TestRegularExpression {
    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 matcher = p.matcher(args[0]);
            while(matcher.find()) 
                System.out.println("Match \"" + matcher.group() + "\" at positions " +
                                    matcher.start() + "-" + (matcher.end()-1));
        }
    }
}

Pattern对象表示编译后的正则表达式,该类具有以下方法:

  • static Pattern compile(String regex):编译正则表达式
  • Matcher matcher(CharSequence input):获取Matcher对象
  • static boolean matches(String regex,CharSequence input):检查该字符序列是否具有与指定正则表达式匹配项

通过获取的Matcher对象,我们可以判断各种不同类型的匹配是否成功:

  • boolean matches():判断整个字符串是否匹配正则表达式模式
  • boolean lookingAt():判断该字符串的开始部分是否匹配模式
  • boolean find():用于在CharSequence中查找多个匹配
  • boolean find(int start):从指定位置开始搜索
  • int start():返回前一次匹配的起始位置的索引
  • int end():返回前一次匹配的最好字符的索引加一

下例是通过find()方法进行匹配:

public class Finding {
    public static void main(String[] args) {
        Matcher matcher = Pattern.compile("\\w+").matcher("Evening is full of the linnet's wings");
        while(matcher.find()) 
            System.out.print(matcher.group() + " ");
        System.out.println();
        int i = 0;
        while(matcher.find(i)) {
            System.out.print(matcher.group() + " ");
            i++;
        }
    }
}
组(Group)

组是用括号划分的正则表达式,可以根据组的编号来引用某个组:

  • 组号0表示整个表达式。
  • 组号1表示被第一对括号括起来的组,以此类推。

下面的表达式中:

A(B(C))D

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

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

  • public int groupCount():返回该匹配器的模式中的分组数目
  • public String group():返回前一次匹配操作的第0组(整个匹配)
  • public String group(int):返回前一次匹配操作的指定组号,该组没有匹配返回null
  • public int start(int):返回前一次匹配操作中指定组号的起始索引
  • public int end(int):返回前一次匹配操作中指定组号的最后一个字符索引加1的值

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

public class Groups {
    public static final String POEM = "Twas brilling, and the slithy toves\n"
            + "Dig gyre and gumble in the wabe.\n"
            + "All mimsy were the borogoves,\n"
            + "And the more 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 matcher = Pattern.compile("(?m)(\\S+)\\s+((\\S+)\\s+(\\S+))$").matcher(POEM);
        while(matcher.find()) { 
            for (int i = 0; i <= matcher.groupCount(); i++) 
                System.out.print("[" + matcher.group(i)+ "]") ;
            System.out.println();
        }
    }
}

可以看到这个正则表达式模式有许多圆括号分组,并且在序列开头的模式标记(?m)会使得$匹配一行的结束。

下面的例子综合展示了正则表达式的使用:

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 stars ... 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;
        public 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 pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(s);
        while (matcher.find()) 
            d.display("find() '" + matcher.group() + "' start = " 
                    + matcher.start() + " end = " + matcher.end());
        if(matcher.lookingAt())
            d.display("lookingAt() start = " + matcher.start() + " end = " + matcher.end());
        if(matcher.matches()) 
            d.display("matches() start = " + matcher.start() + " end = " + matcher.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()只有在正则表达式与输入的最开始处就开始匹配时才会成功。

Pattern标记

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

static Pattern compile(String regex, int flag)

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

  • Pattern.CANON_EQ:两个字符当且仅当它们的完全规范分解相匹配时,才认为它们匹配。
  • Pattern.CASE_INSENSIITIVE(?i):该标记允许大小写不敏感。
  • Pattern.COMMENTS(?x):该标记下,忽略空格符和#注释。
  • Pattern.DOTALL(?s):该标记下,表达式"."匹配所有字符。默认不匹配终结符。
  • Pattern.MULTILINE(?):该标记下,表达式^和$分别匹配一行的开始和结束。默认匹配完整字符串的开始和结束。
  • Pattern.UNICODE _ CASE(?u):指定该标记并开启CASE_INSENSITIVE时,大小写不敏感将作用于Unicode。默认只能作用于US-ASCII。
  • Pattern.UNIX_LINES(?d):该模式下,在.、^和$行为中,值识别终结符\n。

我们也可以直接在正则表达式中使用其中的大多数标记,只需将商标中括号括起的字符插入到正则表达式中的相应位置即可。并且可以通过|操作符组合多个标记的功能:

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

13.6.5 split()

split()方法将输出字符串由指定正则表达式分割成字符串对象数组:

  • String[] split(CharSequence input)
  • String[] split(CharSequence input, int limit):限制分割成字符串的数量

下例通过该方法分割输入文本:

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)));
        System.out.println(Arrays.toString(Pattern.compile("!!").split(input,3)));
    }
}

13.6.6 替换操作

正则表达式特别便于替换文本,它提供了许多方法:

  • replaceFirst(String):将参数字符串替换掉第一个匹配项
  • replaceAll(String):将参数字符串替换所有匹配项
  • appendReplacement(StringBuffer,String):执行渐进式的替换,即边查找边替换,并且每次替换后,将所已经查询并替换的字符串复制到StringBuffer中。
  • appendTail(StringBuffer):在执行一次或多次渐进式替换后,调用此方法,可以将输入字符串中余下的部分复制到StringBuffer中。

下面的程序演示了如何使用这些方法:

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

public class TheReplacements {
    public static void main(String[] args) {
        String s = TextFile.read("TheReplacements.java");
        Matcher matcher = Pattern.compile("/\\*!(.*)!\\*/",Pattern.DOTALL).matcher(s); // 使用//*转义*
        if(matcher.find())
            s = matcher.group(1);
        s = s.replaceAll(" {2,}", " ");
        s = s.replaceAll("(?m)^ +", "");
        System.out.println(s);
        s = s.replaceFirst("[aeiou]", "(VOWEL1)");
        StringBuffer sb = new StringBuffer();
        Pattern p = Pattern.compile("[aeiou]");
        Matcher m = p.matcher(s);
        while(m.find()) 
            m.appendReplacement(sb, m.group().toUpperCase());
        m.appendTail(sb);
        System.out.println(sb);
    }
}

上例中使用TextFile类打开并读入文件,该类会在第18章中对其代码进行详细介绍。

13.6.7 reset()

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

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

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

13.6.8 正则表达式与Java I/O

在此之前,我们介绍了将正则表达式应用于静态的字符串。下面,我们将学习如何应用正则表达式在一个文件中进行搜索匹配操作:

// 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);
        }
        Pattern pattern = Pattern.compile(args[1]);
        int index = 0;
        Matcher matcher = pattern.matcher("");
        for (String line : new TextFile(args[0])) {
            matcher.reset(line);
            while(matcher.find()) 
                System.out.println(index++ + ": " + matcher.group() + ": " + matcher.start());
        }
    }
}

上例通过TextFile读取文件信息,遍历文件中的每行文本,进行匹配。

13.7 扫描输入

一般情况下,从文件或标准输入中读取数据都是每次读取一行,再进行操作,例如:

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's your name?");
            String name = input.readLine();
            System.out.println(name);
            System.out.println("How old are you?What's your favorite double?");
            System.out.println("(input: <age> <double>)");
            String numbers = input.readLine();
            System.out.println(numbers);
            String[] numArr = numbers.split(" ");
            int age = Integer.parseInt(numArr[0]);
            double favorite = Double.parseDouble(numArr[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.\n", favorite/2);
        } catch (IOException e) {
            System.err.println("I/O exception");
        }
        
    }
}

StringReader将String转化为可读的流对象,然后使用这个对象来构造BufferedReader对象,通过使用该对象的readLine()方法一次读取一行文本,并且根据需要将其分解。

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

public class BetterRead {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(SimpleRead.input);
            System.out.println("What's your name?");
            String name = scanner.nextLine();
            System.out.println(name);
            System.out.println("How old are you?What's your favorite double?");
            System.out.println("(input: <age> <double>)");
            int age = scanner.nextInt();
            double favorite = scanner.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.\n", favorite/2);
    }
}

Scanner的构造器可以接受任何类型的输入对象,包括File、InputStream、String或本例中的BufferedReader。除char之外的所有基本类型都有对应的next方法,包括BigDecimal和BigInteger,并且可以通过对应hasNext()方法判断是否具有下一个所需类型的分词。

13.7.1 Scanner定界符

默认情况下,Scanner根据空白符对输入进行分词,但我们可以使用Scanner.useDelimiter(String)方法通过正则表达式指定自己所需的定界符:

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

13.7.2 用正则表达式扫描

除了能够扫描基本类型外,还可以使用自定义的正则表达式进行扫描:

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 result = scanner.match();
            String ip = result.group(1);
            String date = result.group(2);
            System.out.format("Threat on %s from %s\n",date,ip);
        }
    }
}

注意,在配合正则表达式使用扫描时,它仅仅针对下一个输入分词进行匹配,如果该正则表达式中含有定界符,将无法匹配成功。

13.8 StringTokenizer

在Java引入正则表达式(J2SE1.4)和Scanner类(Java SE5)之前,分割字符串的唯一方法是使用StringTokenizer来分词。 下面分别用三种技术进行比较:

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

13.9 总结

随着近几个版本的升级,Java对字符串操作的支持已经十分完善。不过,有时我们需要在细节上注意效率问题,例如恰当地使用StringBuilder等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值