目录

Java String及其相关的类

1. String 类

String 在 Java 中是 Object 类的子类,本身被声明为 final 类,所以 不能被继承,用于处理字符串。

1.1 定义字符串

1.1.1 创建字符串常量:

1
2
String s1 = "MicroLOONG";
String s2 = s1;

s1String 的对象,“MicroLOONG” 这个字符串被存放在 常量池 中。而 s2s1 具有 相同的引用,所以他们代表同一个对象。

1.1.2 创建字符串对象:

1
String s = new String("MicroLOONG");

s 是通过 String 类的构造方法创建的对象,“MicroLOONG” 这个字符串被存放在 中。

1.2 字符串比较

表达式 ==equals() 方法是常用的字符串内容比较方式。

对于赋值了相同内容的字符串常量,如上文的 s1s2 ,使用两种方式效果一致,结果都是 true,因为它们具有相同的引用。

但是有时只能使用 equals()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
String s1 = "microloong";
String s2 = "MicroLOONG".toLowerCase();
String s3 = new String("microloong");
String s4 = new String("microloong");

System.out.println(s1 == s2);        // false,不同引用地址
System.out.println(s1.equals(s2));   // true,相同内容

System.out.println(s1 == s3);        // false,不同引用地址(常量池和堆)
System.out.println(s1.equals(s3));   // true,相同内容

System.out.println(s3 == s4);        // false,堆中不同引用地址
System.out.println(s3.equals(s4));   // true,相同内容

结果为:

1
2
3
4
5
6
false
true
false
true
false
true

可从上面的结果归纳出:

  • == 比较的是引用地址String 类覆写了 equals() 方法,比较的是字符串内容

  • 比较两个字符串内容优先使用 equals()

  • 要忽略大小写比较,可使用 equalsIgnoreCase() 方法。

1.3 连接字符串

前文提到 String 被声明为 final 类,那如何完成字符串拼接呢?

1.3.1 使用 + 拼接:

1
2
3
String s1 = "Micro";
String s2 = "LOONG";
String s = s1 + s2;

实际上,Java中的 + 对字符串拼接的原理是将 String 转成了 StringBuilder,然后使用 StringBuilder.append() 方法 进行处理的。

相当于:

1
String s = (new StringBuilder()).append(s1).append(s2).toString();

1.3.2 使用 StringBuffer 或 StringBuilder 拼接:

StringBufferStringBuilder 类与 String 类不同,它们并不是 final 的,所以可以直接修改。

StringBufferStringBuilder 类的 append() 方法可以将其他类型的数据转换为字符串后,追加到对象的末尾。

1
2
3
4
5
6
7
8
9
// StringBuffer
StringBuffer s1 = new StringBuffer("Micro");
String s2 = "LOONG";
StringBuffer s = s1.append(s2);

// StringBuilder
StringBuilder s1 = new StringBuilder("Micro");
String s2 = "LOONG";
StringBuilder s = s1.append(s2);

1.3.3 使用 concat 拼接:

String 类提供了 concat() 方法,效果跟 StringBufferStringBuilder 类的 append() 方法相同。

1
2
3
String s1 = "Micro";
String s2 = "LOONG";
StringBuffer s = s1.concat(s2);

因为String 类是 final 的,所以 concat() 方法实际是将两个字符串的值 复制到新的字符数组 中,然后创建一个 新的 String 对象

1.3.4 使用 join 拼接:

在 JDK8 中 String 类的静态方法 join(),它用指定的字符串来连接字符串数组/集合。

对于以下集合,输出字符串 A - B - C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

StringBuilder s = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
    if (i != 0) {
        s.append(" - ");
    }
    s.append(list.get(i));
}

而使用 join() 方法省去了循环添加字符串的代码,实际上也是使用 StringBuilder 实现的。

1
String s = String.join(" - ", list);

总结:

技巧

效率比较:StringBuilder > StringBuffer > concat > + > String.join

  • + 拼接字符串的原理也是 StringBuilder,但是在 循环 中使用 + 会导致频繁新建 StringBuilder 对象,耗费时间,浪费资源。非循环体内可以直接使用 +

  • StringBuilder 效率最高,但是 StringBuilder 不是线程安全的StringBuffer 中的 append() 方法声明了 synchronized,是 线程安全 的。所以在并发场景中尽量使用 StringBuffer

  • String.join 更适合处理 字符串数组或者集合 的拼接。

1.4 类型转换

1.4.1 任意类型转换为字符串

使用静态方法 valueOf(),根据不同类型的参数进行重载:

1
2
3
4
5
6
String.valueOf(17);     // "int 17"
String.valueOf(12.34);  // "float/double 12.34"
String.valueOf(true);   // "boolean true"
String.valueOf('A');    // "char A"
char[] arr = {'M', 'i', 'c', 'r', 'o', 'L', 'O', 'O', 'N', 'G'};
String.valueOf(arr);    // "char[] MicroLOONG"

1.4.2 字符串转换为其他类型

字符串转换为基本数据类型,如果类型转换结果不匹配,则抛出 NumberFormatException 异常。

1
2
3
4
int n1 = Integer.parseInt("1234");         // 1234
int n2 = Integer.parseInt("ff", 16);       // 255 十六进制转换
double n3 = Double.parseDouble("12.34");   // 12.34
boolean n4 = Boolean.parseBoolean("true"); // true

字符串转换为字符数组:

1
char[] arr = "MicroLOONG".toCharArray();

1.5 格式化字符串

使用 String 类的静态方法 format() 格式化字符串。

1
System.out.println(String.format("name: %s\nscore: %.2f", "John", 99.5));

也可使用类似 C 语言的 printf() 方法。

1
System.out.printf("name: %s\nheight: %.2f", "John", 174.5);

结果都是:

1
2
name: John
height: 174.50

1.6 正则表达式

正则表达式定义了字符串的模式,可以用来搜索、编辑或处理文本,它不局限于 Java 语言。常见的元字符如下所示:

字符 说明
\ 将下一字符标记为 特殊字符、文本、反向引用或八进制转义符。例如,n 匹配字符 n\n 匹配换行符。序列 \\\\ 匹配 \\\\( 匹配 (
^ 匹配输入字符串 开始 的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n\r 之后的位置匹配。
$ 匹配输入字符串 结尾 的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n \r 之前的位置匹配。
* 零次或多次 匹配前面的字符或子表达式。例如,zo* 匹配 zzoo。* 等效于 {0,}
+ 一次或多次 匹配前面的字符或子表达式。例如,zo+zozoo 匹配,但与 z 不匹配。+ 等效于 {1,}
? 零次或一次 匹配前面的字符或子表达式。例如,do(es)? 匹配 dodoes 中的 do。? 等效于 {0,1}
{n} n 是非负整数,正好匹配 n。例如,o{2}Bob 中的 o 不匹配,但与 food 中的两个 o 匹配。
{n,} n 是非负整数,至少匹配 n。例如,o{2,} 不匹配 Bob 中的 o,而匹配 foooood 中的所有 oo{1,} 等效于 o+o{0,} 等效于 o*
{n,m} mn 是非负整数,其中 n <= m。匹配 至少 n 次,至多 m。例如,o{1,3} 匹配 fooooood 中的头三个 o。‘o{0,1}’ 等效于 o?。注意:您不能将空格插入逗号和数字之间。
? 当此字符紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,匹配模式是 非贪心的非贪心的 模式匹配搜索到的、尽可能短的字符串,而默认的 贪心的 模式匹配搜索到的、尽可能长的字符串。例如,在字符串 oooo 中,o+? 只匹配单个 o,而 o+ 匹配所有 o
. 匹配除 \r\n 之外的 任何单个字符。若要匹配包括 \r\n 在内的任意字符,请使用诸如 [\s\S] 之类的模式。
(pattern) 匹配 pattern 并捕获该匹配的子表达式。可以使用 $0…$9 属性从结果匹配集合中检索捕获的匹配。若要匹配括号字符 ( ),请使用 \( 或者 \)
(?:pattern) 匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用 or 字符 (\|) 组合模式部件的情况很有用。例如,industr(?:y|ies) 是比 industry|industries 更经济的表达式。
(?=pattern) 执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern 的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,Windows (?=95\|98\|NT\|2000) 匹配 Windows 2000 中的 Windows,但不匹配 Windows 3.1 中的 Windows。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
(?!pattern) 执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,Windows (?!95|98|NT|2000) 匹配 Windows 3.1 中的 Windows,但不匹配 Windows 2000 中的 Windows。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
x|y 匹配 x或y。例如,z\|food 能匹配 zfood(z\|f)ood 则匹配 zoodfood
[xyz] 字符集。匹配 包含的任一字符。例如,[abc]匹配plain中的a
[^xyz] 反向字符集。匹配 未包含的任何字符。例如,[^abc]匹配plainplin
[a-z] 字符范围。匹配 指定范围内的任何字符。例如,[a-z]匹配az范围内的任何小写字母。
[^a-z] 反向范围字符。匹配 不在指定的范围内的任何字符。例如,[^a-z]匹配任何不在 az 范围内的任何字符。
\b 匹配 一个字边界,即字与空格间的位置。例如,er\b 匹配 never 中的 er,但不匹配 verb 中的 er
\B 匹配 非字边界er\B 匹配 verb 中的 er,但不匹配 never中的er
\cx 匹配 x 指示的 控制字符。例如,\cM 匹配 Control-M 或回车符。x 的值必须在 A-Za-z 之间。如果不是这样,则假定 c 就是 c 字符本身。
\d 匹配 数字字符。等效于 [0-9]
\D 匹配 非数字字符。等效于 [^0-9]
\f 匹配 换页符。等效于 \x0c\cL
\n 匹配 换行符。等效于 \x0a\cJ
\r 匹配 一个回车符。等效于 \x0d\cM
\s 匹配 任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效。
\S 匹配 任何非空白字符。与 [^ \f\n\r\t\v] 等效。
\t 匹配 制表符。与 \x09\cI 等效。
\v 匹配 垂直制表符。与 \x0b\cK 等效。
\w 匹配 任何字类字符,包括下划线。与 [A-Za-z0-9_] 等效。
\W 匹配 任何非单词字符。与 [^A-Za-z0-9_] 等效。
\xn 匹配 n,此处的 n 是一个十六进制转义码。十六进制转义码必须正好是两位数长。例如,\x41匹配A\x041\x04&1等效。允许在正则表达式中使用 ASCII 代码。
\num 匹配 num,此处的 num 是一个正整数。到捕获匹配的反向引用。例如,(.)\1匹配两个连续的相同字符。
\n 标识一个八进制转义码或反向引用。如果 \n 前面至少有 n 个捕获子表达式,那么 n 是反向引用。否则,如果 n 是八进制数 (0-7),那么 n 是八进制转义码。
\nm 标识一个八进制转义码或反向引用。如果 \nm 前面至少有 nm 个捕获子表达式,那么 nm 是反向引用。如果 \nm 前面至少有 n 个捕获,则 n 是反向引用,后面跟有字符 m。如果两种前面的情况都不存在,则 \nm 匹配八进制值 nm,其中 nm 是八进制数字 0-7
\nml n 是八进制数 0-3ml 是八进制数 0-7 时,匹配八进制转义码 nml
\un 匹配n,其中 n 是以四位十六进制数表示的 Unicode 字符。例如,\u00A9 匹配版权符号 ©
警告
Java 中使用正则表达式需要使用 \\ 来表示 \。如:表示一位数字的正则表达式为 \\d

例1,剔除字符串 .A1/b2?C3,d4|E5 中的非数字字符:

1
2
3
String s = ".A1/b2?C3,d4|E5";
String regex = "\\D";
s = s.replaceAll(regex, ""); // 12345

字符串 regex 包含正则表达式,表示非数字字符。 使用 replaceAll() 方法将非数字字符的字符替换为 ""

也可通过 java.util.regex 包下的 PatternMatcher 类可以利用正则表达式处理字符串。

1
2
3
String regex = "\\D";
Matcher matcher = Pattern.compile(regex).matcher(".A1/b2?C3,d4|E5");
String s = matcher.replaceAll(""); // 12345

Pattern 类通过静态方法 complie() 创建一个正则表达式,并使用 matcher() 方法生成一个 Matcher 对象。

例2,分割字符串,提取单词:

1
2
3
String s = "Java: Hello World!";
String regex = "\\W+";
String[] str = s.split(regex); // Java Hello World

字符串 regex 包含正则表达式,表示匹配一次或多次非单词字符(如空格)。使用 split() 方法利用正则表达式作为标记进行分割。

例3,实现一个类似 Thymeleaf 的模板引擎:

现有以下一段字符串,其中 ${key} 表示变量,也是将要被替换的部分。

Hello, ${name}! You are learning ${lang}!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
String s = "Hello, ${name}! You are learning ${lang}!";
Map<String, String> map = new HashMap<>();
map.put("name", "MicroLOONG");
map.put("lang", "Java");

Matcher matcher = Pattern.compile("\\$\\{(\\w+)\\}").matcher(s);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
    matcher.appendReplacement(sb, map.get(matcher.group(1)));
}
// 添加剩余的内容
matcher.appendTail(sb);

使用 Matcher 类的 find() 方法在字符串中查找匹配到正则表达式的子串。appendReplacement() 方法是将匹配的内容替换为第二个参数的内容,然后通过 StringBuilderStringBuffer 连接字符串。map.get(matcher.group(1)) 表示获取 map 中键 matcher.group(1) 对应的值,即正则表达式中的 \\w+。由于匹配对象后面可能还有字符,比如字符串 s 中的 !,所以最后通过 appendTail() 方法追加。

替换后结果为 Hello, MicroLOONG! You are learning Java!



更多内容在 String 类的 Java API 文档

2. StringBuffer 和 StringBuilder 类

StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,与 被 final 修饰的 String 不同,创建的对象可以修改。

在上文拼接字符串时已提到了这三种类,使用场景:

  • 操作少量的数据: 适用 String

  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder

  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

3. StringJoiner 类

在上文拼接字符串数组使用的是 String.join() 方法,另外还可以使用 StringJoiner 类的构造方法实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// 最终结果:A - B - C
StringJoiner s = new StringJoiner(" - "); 
for (String temp : list) {
    s.add(temp);
}

代码中,StringJoiner 类的 add() 方法依赖 StringBuilder 进行字符串拼接。

还可以通过 StringJoiner(delimiter, prefix, suffix) 加入前缀 prefix 和后缀 suffix

4. StringTokenizer 类

在上文分割字符串使用的是 正则表达式,另外还可以使用 StringTokenizer 类的构造方法实现。

1
2
3
4
5
String s = "Java: Hello World!";
StringTokenizer st = new StringTokenizer(s, ": !");
while (st.hasMoreTokens()) {
    System.out.println(st.nextToken());
} // Java Hello World

StringTokenizer(String str, String delim, boolean returnDelims) 构造方法的第二个参数可以指定分隔符,如果不指定则默认以空格("")、制表符(\t)、换行符(\n)、回车符(\r)作为分隔符。如果定义了第三个参数,可以指定是否返回分隔符。

StringTokenizer 类的 hasMoreTokens() 方法返回是否还有分隔符。如果还有分隔符,可以利用 nextToken() 方法返回从当前位置到下一个分隔符的字符串。

资料