字符串
- 前言
- 字符串的不可变
- + 的重载与 StringBuilder
- 格式化输出
- Formatter 类
- 正则表达式
- 创建正则表达式
- Pattern 和 Matcher
前言
字符串操作毫无疑问是计算机程序设计中最常见的行为之一。
字符串的不可变
String 对象是不可变的。查看 JDK 文档你就会发现,String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象,以包含修改后的字符串内容。而最初的 String 对象则丝毫未动。
public class Str {
public static void main(String[] args) {
String str = "hello";
System.out.println(str);
String str2 = append(str);
System.out.println(str2);
System.out.println(str);
}
private static String append(String s){
return s + "world";
}
}
调用append()
方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指向的对象其实一直待在单一的物理位置上,从未动过。一旦 append()
运行结束,s 就消失了。当然了,append()
的返回值,其实是最终结果的引用。这足以说明,append()
返回的引用已经指向了一个新的对象,而 str 仍然在原来的位置。
+ 的重载与 StringBuilder
String 对象是不可变的,所以指向它的任何引用都不可能修改它的值,因此,也就不会影响到其他引用。不可变性会带来一定的效率问题。为 String 对象重载的 + 操作符就是一个例子。重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义(用于 String 的 + 与 += 是 Java 中仅有的两个重载过的操作符,Java 不允许程序员重载任何其他的操作符 )。
操作符 + 可以用来连接 String:
public class Str {
public static void main(String[] args) {
String s = "hello";
String str = s + " world";
System.out.println(str);
/** Output:
* hello world
*/
}
}
我们可以通过-javap来反编译以上代码,来了解它的底层:
//先编译
javac 类名.java
//反编译
javap -c 类型
如果你有汇编语言的经验,以上代码应该很眼熟。即使你完全不了解汇编语言也无需担心)。需要重点注意的是:编译器自动引入了 java.lang.StringBuilder
类。虽然源代码中并没有使用 StringBuilder 类,但是编译器却自作主张地使用了它,就因为它更高效。编译器创建了一个 StringBuilder 对象,调用了两次append()
方法,最后调用 toString()
生成结果。
或许,你会觉得可以随意使用 String 对象,反正编译器会自动为你做性能优化。下面通过两个例子String对象和StringBuilder对象来展示它们的区别。
public class Str {
public static void main(String[] args) {
String s = "";
for (int i = 0; i < 5; i++) {
s+="";
}
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 5; i++) {
stringBuilder.append("");
}
}
}
通过反编译可以看到,在循环内,使用操作符 + 此时每循环一次就会创建一个新的 StringBuilder 对象。而使用StringBuilder 对象追加元素时,反编译的内容更短,且只创建了一个对象。
StringBuilder 提供了丰富而全面的方法,包括 insert()
、replace()
、substring()
,甚至还有reverse()
,但是最常用的还是 append()
和 toString()
。StringBuilder是 Java SE5 引入的,在这之前用的是 StringBuffer。后者是线程安全的,因此开销也会大些。使用 StringBuilder 进行字符串操作更快一点。
格式化输出
在长久的等待之后,Java SE5 终于推出了 C 语言中 printf()
风格的格式化输出这一功能。这不仅使得控制输出的代码更加简单,同时也给与Java开发者对于输出格式与排列更强大的控制能力。
转义符 | 含义 |
%n | 换行,相当于 \n |
%c | 单个字符 |
%d | 十进制整数 |
%u | 无符号十进制数 |
%f | 十进制浮点数 |
%o | 八进制数 |
%x | 整型(十六进制数) |
%s | 字符串 |
%% | 输出百分号 |
%b | Boolean值 |
C 语言的 printf()
并不像 Java 那样连接字符串,它使用一个简单的格式化字符串,加上要插入其中的值,然后将其格式化输出。 printf()
并不使用重载的 + 操作符(C语言没有重载)来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置。而且它还将插入格式化字符串的参数,以逗号分隔,排成一行。例如:
public class Str {
public static void main(String[] args) {
System.out.printf("我是%s", "张三");
/** Output:
* 我是张三
*/
}
}
Java SE5 引入了 format()
方法,可用于 PrintStream 或者 PrintWriter 对象,其中也包括 System.out 对象。format() 方法模仿了 C 语言的 printf()。如果你比较怀旧的话,也可以使用 printf()。以下是一个简单的示例:
public class Str {
public static void main(String[] args) {
System.out.printf("我是%s%n", "张三");
System.out.format("我是%s%n", "张三");
System.out.println("我是张三");
/** Output:
* 我是张三
* 我是张三
* 我是张三
*/
}
}
可以看到,format()
和 printf()
是等价的,它们只需要一个简单的格式化字符串,加上一串参数即可,每个参数对应一个格式修饰符。
Formatter 类
在 Java 中,所有的格式化功能都是由 java.util.Formatter 类处理的。可以将 Formatter 看做一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个 Formatter 对象时,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出:
public class Str {
public static void main(String[] args) {
Formatter formatter = new Formatter(System.out);
formatter.format("%s%n","张三");
formatter.format("%c%n",'张');
formatter.format("%b%n",'张');
formatter.format("%d%n",1);
/** Output:
* 张三
* 张
* true
* 1
*/
}
}
一定要注意类型匹配,否则会触发异常。用到了 %b 转换,只要该参数不为 null,其转换结果永远都是 true。还有许多不常用的类型转换与格式修饰符选项,你可以在 JDK 文档中的 Formatter 类部分找到它们。
String 类也有一个 static format()
方法,可以格式化字符串,其实在 String.format()
内部,它也是创建了一个 Formatter 对象,然后将你传入的参数转给 Formatter。这样的代码更清晰易读。
public class Str {
public static void main(String[] args) {
String format = String.format("我是%s", "张三");
System.out.println(format);
/** Output:
* 我是张三
*/
}
}
正则表达式
正则表达式是一种强大而灵活的文本处理工具。使用正则表达式,我们能够以编程的方式,构造复杂的文本模式,并对输入 String 进行搜索。正则表达式提供了一种完全通用的方式,能够解决各种 String 处理相关的问题:匹配、选择、编辑以及验证。
例如,要找一个数字,它可能有一个负号在最前面,那你就写一个负号加上一个问号,就像这样:
-?
在正则表达式中,用 \d
表示一位数字。Java中 \\
的意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。例如,如果你想表示一位数字,那么正则表达式应该是 \\d
;换行符和制表符之类的东西只需要使用单反斜线:\n\t
;要表示“一个或多个之前的表达式”,应该使用 +
;用竖线 |
表示或操作。应用正则表达式最简单的途径,就是利用 String 类内建的功能,如下所示:
public class Str {
public static void main(String[] args) {
System.out.println("+183".matches("-?\\d+"));
System.out.println("+183".matches("(-|\\+)?\\d+"));
/** Output:
* false
* true
*/
}
}
除此之外还有:split()
方法,其功能是“将字符串从正则表达式匹配的地方切开。”;replaceAll()
方法和replaceFirst()
方法可以进行匹配替换操作,replaceFirst()
方法只替换第一个匹配元素:
public class Str {
public static void main(String[] args) {
String string = "This is string";
//非单词字符进行切割
System.out.println(Arrays.toString(string.split("\\W+")));
//T开头的字母替换为located
System.out.println(string.replaceAll("T\\w+", "located"));
//第一个T开头的字母替换为located
System.out.println(string.replaceFirst("T\\w+", "located"));
/** Output:
* [This, is, string]
* located is string
* located is string
*/
}
}
创建正则表达式
我们首先从正则表达式可能存在的构造集中选取一个很有用的子集,以此开始学习正则表达式。正则表达式的完整构造子列表,请参考JDK文档 java.util.regex
包中的 Pattern类。
表达式 | 含义 |
B | 指定字符B |
\xhh | 十六进制值为0xhh的字符 |
\uhhhh | 十六进制表现为0xhhhh的Unicode字符 |
\t | 制表符Tab |
\n | 换行符 |
\r | 回车 |
\f | 换页 |
\e | 转义(Escape) |
当你学会了使用字符类(character classes)之后,正则表达式的威力才能真正显现出来。以下是一些创建字符类的典型方式,以及一些预定义的类:
表达式 | 含义 |
. | 任意字符 |
[abc] | 包含a、b或c的任何字符(和 |
[^abc] | 除a、b和c之外的任何字符(否定) |
[a-zA-Z] | 从a到z或从A到Z的任何字符(范围) |
[abc[hij]] | 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-Z_0-9]) |
\W | 非词字符([^\w]) |
下面是不同的边界匹配符:
边界匹配符 | 含义 |
^ | 一行的开始 |
$ | 一行的结束 |
\b | 词的边界 |
\B | 非词的边界 |
\G | 前一个匹配的结束 |
下面展示了,h开头,d结尾,中间任意字符的一个简单的正则表达式:
public class Str {
public static void main(String[] args) {
System.out.println("hello world".matches("^h(.*)d$"));
/** Output:
* true
*/
}
}
我们的目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的、最简单以及最必要的正则表达式。一旦真正开始使用正则表达式了,你就会发现,在编写新的表达式之前,你通常会参考代码中已经用到的正则表达式。
Pattern 和 Matcher
比起功能有限的 String 类,我们更愿意构造功能强大的正则表达式对象。只需导入 java.util.regex
包,然后用 static Pattern.compile()
方法来编译你的正则表达式即可。它会根据你的 String 类型的正则表达式生成一个 Pattern 对象。接下来,把你想要检索的字符串传入 Pattern 对象的 matcher()
方法。matcher()
方法会生成一个 Matcher 对象。使用 Matcher 上的方法,我们将能够判断各种不同类型的匹配是否成功:
boolean matches()
boolean lookingAt()
boolean find()
boolean find(int start)
Matcher.find()
方法可用来在 CharSequence 中查找多个匹配。例如:
public class Str {
public static void main(String[] args) {
// 按指定模式在字符串查找
String line = "2022 One World One Family";
String pattern = "\\w+.*";
// 创建 Pattern 对象
Pattern r = Pattern.compile(pattern);
// 现在创建 matcher 对象
Matcher m = r.matcher(line);
while (m.find()) {
System.out.println("Found value: " + m.group());
} else {
System.out.println("NO MATCH");
}
/** Output:
* Found value: 2022 One World One Family
*/
}
}
在匹配操作成功之后,start()
返回先前匹配的起始位置的索引,而 end()
返回所匹配的最后字符的索引加一的值。匹配操作失败之后(或先于一个正在进行的匹配操作去尝试)调用 start()
或 end()
将会产生 IllegalStateException。
public class Str {
public static void main(String[] args) {
// 按指定模式在字符串查找
String line = "2022 One World One Family";
String pattern = "\\w+.*";
// 创建 Pattern 对象
Pattern r = Pattern.compile(pattern);
// 现在创建 matcher 对象
Matcher m = r.matcher(line);
while (m.find()) {
System.out.println("start:"+ m.start());
System.out.println("end:"+ m.end());
}
/** Output:
* start:0
* end:25
*/
}
}
通过 reset()
方法,可以将现有的 Matcher 对象应用于一个新的字符序列:
public class Str {
public static void main(String[] args) {
// 按指定模式在字符串查找
String line = "2022 One World One Family";
String pattern = "\\w+.*";
// 创建 Pattern 对象
Pattern r = Pattern.compile(pattern);
// 现在创建 matcher 对象
Matcher m = r.matcher(line);
while (m.find()) {
System.out.println("old: "+m.group());
m.reset("hello world");
while(m.find()) {
System.out.println("reset: "+m.group());
}
}
/** Output:
* old: 2022 One World One Family
* reset: hello world
*/
}
}
在 Java 引入正则表达式(J2SE1.4)和 Scanner 类(Java SE5)之前,分割字符串的唯一方法是使用 StringTokenizer 来分词。不过,现在有了正则表达式和 Scanner,我们可以使用更加简单、更加简洁的方式来完成同样的工作了。基本上,我们可以放心地说,StringTokenizer 已经可以废弃不用了。