【Java编程思想】13.字符串

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


13.1 不可变 String

String 对象是不可变的。String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象去包含修改后的字符串内容;而最初的 String 对象则没有改变。
每当吧 Stirng 对象作为方法的参数时,都会复制一份引用,而该引用所指的对象一直待在单一的物理位置上,从未动过。


13.2 重载 “+” 与 StringBuilder

操作符的重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义。
用于 String 的 “+” 与 “+=” 是 Java 中仅有的两个重载过的操作符,而 Java 不允许程序员重载任何其他操作符。

使用 “+” 可以连接 String,但是原理上,大致是使用类似 append() 方法,生成新的 String 对象,以包含连接后的字符串。这种工作方式,期间涉及大量的中间对象生成与回收,会带来一定的性能问题。
但其实通过反编译后的字节码,我们可以知道在使用 “+” 的时候,编译器会自动引入 java.lang.StringBuilder 类,并使用 append() 方法,最后 toString() 转换拼接好的字符串。
可以看出编译器是会自动优化性能的,但是使用多个 Stirng 对象和操作符拼接和使用 Stringbuilder 有什么不同呢:
假设在循环内拼接字符串,编译后的字节码会显示,使用操作符的方式每次在循环体内部都会新建一个 Stringbuilder 对象,因此显而易见,使用操作符 “+” 进行重载的方式,对性能的消耗是比较大的。


13.3 无意识的递归

对于 ArrayList.toString(),他会遍历 ArrayList 中包含的所有对象,调用每个元素上的 toString() 方法。这就是一种无意识的递归

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

对于上述代码,在其他类型的对象与字符串用 “+” 相连接的时候,会发生自动类型转换,这个时候会调用该对象的 toString() 方法,产生了有害的递归调用。这种情况下,如果真想打印对象的内存地址,应该调用 Object.toString() 方法,因此不应该使用 this,而是应该调用 super.toString() 方法。


13.4 String 上的操作

String 对象的一些基本方法:

方法 参数、重载版本 应用
构造器 重载版本、默认版本、String、StringBuilder、StringBuffer、char 数组、byte 数组 创建 String 对象
length() String 中字符的个数
charAt() Int 索引 取得 String 中该索引位置上的 char
getChars()/getBytes() 要复制部分的七点和终点的索引,复制的目标数组,目标数组的其实索引 复制 char 或 byte 到一个目标数组中
toCharArray() 生成一个 char[],包含 String 的所有字符
equals()/equalsIgnoreCase() 与之进行比较的 String 比较两个 String 的内容是否相同
compareTo() 与之进行比较的 String 按词典顺序比较 String 的内容,比较结果为负数、零或正数。注意,大小写并不等价
contains() 要搜索的 CharSequence 如果该 String 对象包含参数的内容,返回 true
contentEquals() 与之进行比较的 CharSequence 或 StringBuffer 如果该 String 与参数的内容完全一致,则返回 true
equalsIgnoreCase() 与之进行比较的 String 忽略大小写,如果两个 String 的内容相同,则返回 true
regionMatcher() 该 String 的索引偏移量,另一个 String 及其索引偏移量,要比较的长度。重载版本增加忽略大小写功能 返回 boolean 结果,以表明所比较区域是否相等
startsWith() 可能的起始 String,重载版本在参数中增加了偏移量 返回 boolean 结果,以表明该 String 是否以此参数起始
endsWith() 该 String 可能的后缀 String 返回 boolean 结果,以表明该 String 是否是该字符串的后缀
indexOf()/lastIndexOf() 重载版本包括:char;char 与起始索引;String;Stirng 与起始索引 如果该 String 并不包含此参数,就返回-1,否则返回此参数在 String 中的起始索引。lastIndexOf()是从后向前搜索
subString()(subSequence()) 重载版本:起始索引;起始索引+终点坐标 返回一个新的 Stirng,以包含参数指定的子字符串
concat() 要连接的 Stirng 返回一个新的 String 对象,内容为原始 String 连接上参数 String
replace() 要替换掉的字符,用来进行替换的新字符。也可以用一个 CharSequence 来替换另一个 CharSequence 返回替换字符后的新 Stirng 对象,如果没有替换发生,则返回原始的 String 对象
toLowerCase()/toUpperCase() 将字符的大小写改变后,返回一个新 String 对象。如果没有改变发生,则返回原始的 String 对象
trim() 将 String 两端的空白字符删除后,返回一个新的 String 对象。如果没有改变发生,则返回原始的 String 对象
valueOf() 重载版本:Object;char[];char[],偏移量,与字符个数;boolean;char;int;long;float;double 返回一个表示参数内容的 Stirng
intern() 为每个唯一的字符序列生成一个且仅生成一个 String 引用

总体上来说,在要改变字符串内容时,String 类的方法都会返回一个新的 Stirng 对象;如果内容没有改变,String 的方法只是返回指向原对象的引用。


13.5 格式化输出

Java 中的 printf() 可以使用格式修饰符来连接字符串。

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

Java 中还提供了与 printf() 等价的 format() 方法。该方法可用于 PrintStreamPrintWriter 对象。

Java 中所有新的格式化功能都由 java.util.Formatter 处理,当创建一个 Formatter 对象的时候,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出。如下

Formatter f = new Formatter(System.out);

再插入数据时,如果想要更精确的控制格式,那么需要更复杂的格式修饰符。以下是其抽象的语法:

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

其中
width 控制一个域的最小尺寸,width 可以用于各种类型的数据转换,并且其行为方式都一样。默认情况下数据右对齐,可以通过使用“-”标志来改变对齐方向。
precision 用来指明最大尺寸,并不是所有类型的数据都能使用 precision,而且应用于不同类型的数据转换时,precision 的意义也不同:对于 String 表示打印时输出字符的最大数量;对于浮点数表示小数部分要显示出来的位数(默认6位小数)位数过多舍入,过少则补零;而 precision 没法应用于整数。


常用的类型转换字符:

转换字符 描述
d 整数型(十进制)
c Unicode 字符
b Boolean 值
s String
f 浮点数(十进制)
e 浮点数(科学计数)
x 整数(十六进制)
h 散列码(十六进制)
% 字符“%”

String.format() 是一个 static 方法,他接受与 Formatter.format() 方法一样的参数,但是返回一个 String 对象。

String.format("%05X: ", str);

使用上面的方法,可以以可读的十六进制格式将字节数组打印出来。


13.6 正则表达式

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

String 中提供了正则表达式工具
split() 将字符串从正则表达式匹配的地方切开
replace() 只替换正则表达式第一个匹配对象
replaceAll() 替换正则表达式全部的匹配对象


? 可以用来描述一个要查找的字符串
+ 一个或多个之前的表达式
\\ 在正则表达式中插入一个普通的反斜线,因此\\d可以表示一个数字,\\w表示一个非单词小写字符,\\W表示一个非单词大写字符
| 或操作
例:
-?\\d+,表示“可能有一个负号,后面跟着一位或者多位的数字”。
(-|\\+)? 表示“可能以一个正号或者负号开头的字符串”


创建正则表达式

字符
B 指定字符 B
\xhh 十六进制值为 oxhh 的字符
\uhhhh 十六进制表示为 oxhhhh 的 Unicode 字符
\t 制表符 Tab
\n 换行符
\r 回车
\f 换页
\e 转义(Escape)
字符类
. 任意字符
[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) 捕获组(capturing group)。可以在表达式中用 \i 引用第 i 个捕获组
边界匹配符
^ 一行的起始
$ 一行的结束
\b 词的边界
\B 非词的边界
\G 前一个匹配的结束

量词

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

  • 贪婪型:量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。
  • 勉强型:用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也可以视作“懒惰的、最少匹配的、非贪婪的、不贪婪的”。
  • 占有型:该量词只在 Java 中可用。正常当正则表达式被应用于字符串时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有型”量词并不保存这些中间状态,因此他们可以用来防止回溯,这个特性常用于防止正则表达式失控,因此可以使正则表达式执行起来更有效。
贪婪型 勉强型 占有型 如何匹配
X? X?? X?+ 一个或零个 X
X* X*? X*+ 零个或多个 X
X+ X+? X++ 一个或多个 X
X{n} X{n}? X{n}+ 恰好 n 次 X
X{n,} X{n,}? X{n,}+ 至少 n 次 x
X{n,m} X{n,m}? X{n,m}+ X 至少 n 次,且不超过 m 次

表达式 X 通常必须使用圆括号括起来以免造成不必要的歧义。
接口 CharSequenceCharBufferStringStringBufferStringBuilder 类之中抽象出了字符序列的一般化定义。多数正则表达式操作都接受 CharSequence 类型的参数。


Pattern 和 Matcher

使用 static Pattern.compile() 方法来编译正则表达式,它会根据 String 类型的正则表达式生成一个 Pattern 对象。
接下来可以把想要检索的字符串传入 Pattern 对象的 matcher() 方法。该方法会生成一个 Matcher 对象,有很多种用法。
示例如下:

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

可以看到,Pattern 对象表示编译后的正则表达式,利用该对象上的 matcher() 方法加上一个输入字符串,即可构造出 Matcher 对象,用来进行相应的匹配或其他操作。

Pattern 类还提供:

  • matches() 该方法完整为 static boolean matches(String regex, CharSequence input),用以检查 regex 是否匹配整个 CharSequence 类型的 input 参数。
  • split() 该方法从匹配 regex 的地方分隔输入字符串,返回分割后的子字符串 String 数组。

Matcher 类提供:

  • boolean matches() 判断整个输入字符串是否匹配正则表达式模式。
  • boolean lookingAt() 用来判断该字符串(不必是整个字符串)的始部分是否能匹配模式。
  • boolean find() 用来在 CharSequence 中查找多个匹配。
  • boolean find(int start)

(Group)是用括号划分的正则表达式。可以根据组的编号来引用整个组。组号为0表达整个表达式;组号为1表示被第一对括号括起的组,以此类推。
Matcher 类提供一系列方法用于获取与组相关的信息:

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

Pattern 标记Pattern 类的 compile() 方法还有另外一个版本,它接受一个标记参数,以调整匹配的行为。完整方法表达为:Pattern Pattern.compile(String regex, int flag)
其中 flag 来自以下 Pattern 类中的常量:

编译标记 效果
Pattern.CANON_EQ 两个字符当且仅当他们完全规范分解相匹配时,就认为他们是匹配的。在默认情况下匹配不考虑规范的等价性
Pattern.CASE_INSENSITIVE(?i) 默认情况下,大小写不敏感的匹配假定只有 US-ASCII 字符集中的字符才能进行。这个标记允许模式匹配不必考虑大小写。通过指定 UNICODE_CASE 标记以及结合此标记,就可以开启基于 Unicode 的大小写不敏感匹配
Pattern.COMMENTS(?x) 在这种模式下空格符会被忽略,并且以#开始直到行末的注释也会被忽略。通过嵌入的标记表达式也可以开启 Unix 的行模式
Pattern.DOTALL(?s) 在 dotall 模式中,表达式 "." 匹配所有字符,包括行终结符。默认情况下 "."不匹配行终结符
Pattern.MULTILINE(?m) 在多行模式下,表达式^和\(分别匹配一行的开始和结束。^还匹配输入字符串的开始,\)还匹配输入字符串的结尾。默认情况下,这些表达式只匹配输入的完整字符串的开始和结束
Pattern.UNICODE_CASE(?a) 当指定这个标记,并且 开启 CASE_INSENSITIVE 时,大小写不敏感的匹配将按照与 Unicode 标准相一致的方式进行。默认情况下,大小写不敏感的匹配假定只有 US-ASCII 字符集中的字符才能进行。
Pattern.UNIX_LINES(?d) 这种模式下,在 ./^/$ 的行为中,只识别行终结符 \n

示例:

Pattern p =  Pattern.compile("^java",
      Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

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

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

替换操作

  • replaceFirst(String replacement) 以参数字符串 replacement 替换掉第一个匹配成功的部分。
  • replaceAll(String replacement) 以参数字符串 replacement 替换所有匹配成功的部分。
  • appendReplacement(StringBuffer sbuf, String replacement) 执行渐进式替换,它允许你调用其他方法来生成或处理 replacement,使你能够以编程的方式将目标分割成组。
  • appendTail(StringBuffer sbuf) 在执行一次或多次 appendReplacement() 之后,调用此方法可以将输入字符串余下的部分复制到 sbuf 中。

通过 reset() 方法可以将现有的 Matcher 对象应用于一个新的字符序列。使用不带参数的 reset() 方法可以将 Matcher 对象重新设置到当前字符序列的起始位置。


13.7 扫描输入

Java SE5中新增了 Scanner 类,可以用于扫描输入工作。
Scanner 的构造器可以接受任何类型的输入对象,包括 FileInputStreamStringReadable 等。Readable 接口表示”具有 read() 方法的某种东西“。
对于 Scanner,所有的输入、分词以及翻译操作都隐藏在不同类型的 next() 方法中,所有基本类型(除 char 之外)都有对应的 next() 方法。对于所有的 next() 方法,只有找到一个完整的分词之后才会返回。Scanner 也有相应的 hasNext() 方法,用来判断下一个输入分词是否为所需类型。

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


13.8 StringTokenier

在 Java 引入正则表达式(J2SE1.4)和 Scanner 类(Java SE5)之前,使用 StringTokenier 来进行分词。
下面是两者的比较:

public class ReplacingStringTokenizer {
    public static void main(String[] args) {
        String input = "But I'm not dead yet! I feel 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() + " ");
    }
}

输出:

But I'm not dead yet! I feel happy! 
[But, I'm, not, dead, yet!, I, feel, happy!]
But I'm not dead yet! I feel happy! 

StringTokenier 已经基本废弃了。

猜你喜欢

转载自www.cnblogs.com/chentnt/p/9791914.html
今日推荐