Java 字符串

本文最后更新于:2024年3月18日 凌晨

Java 字符串

  • 字符串是字符的序列,在某种程度上类似字符的数组,实际上,在有些语言中(如C语言)就是用字符数组表示字符串,在Java中则是用类的对象来表示。

String

String类主要用于对字符串内容的检索和比较等操作,但要记住操作的结果通常得到一个新字符串,而且不会改变源串的内容。

创建字符串

  • 字符串的构造方法有如下4个:
    • public String():创建一个空的字符串。
    • public String(String s):用已有字符串创建新的String
    • public String(StringBuffer buf):用StringBuffer对象的内容初始化新的String
    • public String(char value[ ]):用已有字符数组初始化新的String
  • 在构造方法中使用最多的是是第2个,用另一个串作为参数创建一个新串对象,例如:
1
String s = new String("ABC");
  • 这里要注意,字符串常量在Java中也是以对象形式存储的,Java编译时将自动对每个字符串常量创建一个对象,因此,当将字符串常量传递给构造方法时,将自动将常量对应的对象传递给方法参数,当然,也可以直接给String变量赋值,例如:
1
String s = "ABC";
  • 字符数组要转换为字符串可以利用第3个构造方法,例如:
1
2
char[]helloArray= {'h','e','l','l','o'};
String helloString = new String(helloArray);
  • 利用字符串对象的length()方法可获得字符串中字符个数,例如字符串"good morning\\你好\n"长度为16
  • 利用字符串对象的toCharArray()方法可得到字符串对应的字符数组。
  • 将GB2312编码的字符串转换为ISO-8859-1编码的字符串。
1
2
String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

字符串的连接

  • 利用"+"运算符可以实现字符串的拼接,而且,可以将字符串与任何一个对象或基本数据类型进行拼接,例如:
1
2
String s = "Hello!";
s = s + "Mary" + 4;//s的结果为Hello! Mary 4
  • 读者也许会想,String对象封装的数据不是不能改变吗?这里怎么能够修改s的值?这里要注意,String类的引用变量只代表对字符串的一个引用,更改s的值实际上只将其指向另外一个字符串对象,字符串拼接后将创建另一个串对象,而变量s指向这个新的串对象。
  • Java还提供另一个方法concat(String str)专用于字符串的连接,以下代码将创建一个新串"4+3=7"赋给s,而内容"4+3="的那个串对象,不再有引用变量指向它,该串对象将自动由垃圾收集程序删除:
1
2
String s = "4+3=";
s = s.concat("7");// 新串为4+3=7

比较两个字符串

字符串的比较如下表所示,其中,compareTo()方法的返回值为一个整数,而其他两个方法的返回值为布尔值。

方法 功能(当前串与参数内容比较)
boolean equals(Object Obj) 如果相等返回true,否则返回false
boolean equalsIgnoreCase(String Str) 忽略字母的大小写判断两串是否相等
int compareTo(String Str) 当前串大,则返回值>0
当前串小,则返回值<0
两串相等,则返回值=0
  • 字符串的比较有一个重要的概念要引起注意,例如:
1
2
3
4
String s1 = "Hello!World";
String s2 = "Hello!World";
boolean b1 = s1.equals(s2);
boolean b2 = (s1==s2);
  • s1.equals(s2)是比较两个字符串的对象值是否相等,显然结果为true,而s1==s2是比较两个字符串对象引用是否相等,这里的结果仍为true,为何?
  • 由于字符串常量是不变量,Java在编译时在对待的字符串常量的存储时有一个优化处理策略,相同字符串常量只存储一份,也就是说s1和s2指向的是同一个字符串。
  • 因此,s1==s2的结果为true,不妨对程序适当修改,其中一个采用构造方法创建,情况又是怎么样呢?
1
2
3
4
String s1 = "Hello!World";
String s2 = new String("Hello!World");
boolean b1 = s1.equals(s2);
boolean b2 = (s1==s2);
  • 这时b1是true,b2却为false,因为new String("Hello!World”)将导致运行时创建一个新字符串对象。
  • 特别地,String类的intern()方法返回字符串的一个等同串,如下两个赋值是等价的:
1
2
String s2 = s1.intern();
String s2 = s1;

[例7-1]:设有中英文单词对照表,输入中文单词,显示相应英文单词,输入英文单词显示相应中文单词,如果没找到,显示"无此单词"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Ex_7 {
public static void main(String[] args) {
String[][] x = {{"good", "好"}, {"bad", "坏"}, {"work", "工作"}};
int k;
String in = args[0];
if ((k = find_e(x, in)) != -1)
System.out.println(x[k][1]);
else if ((k = find_c(x, in)) != -1)
System.out.println(x[k][0]);
else
System.out.println("无此单词");
}

/*根据英文找中文,找到则返回所在行为止,未找到则返回-1*/
static int find_e(String[][] x, String y) {
for (int k = 0; k < x.length; k++)
if (x[k][0].equals(y))
return k;
return -1;
}

/*根据中文找英文,找到则返回所在行位置,未找到则返回-1*/
static int find_c(String[][] x, String y) {
for (int k = 0; k < x.length; k++)
if (x[k][1].equals(y))
return k;
return -1;
}
}
  • 本例用字符串类型的二维数组来存放中英文单词的对应表,对中英文单词的查找分别用两个方法实现,一个是根据英文单词查找中文单词,另一个时根据中文单词查找英文单词,对于任意输入的一个单词只要分别按中文和英文查找一遍即可,如果均为-1,则输出"无此单词"

字符串的提取与修改

  • char charAt(int index):返回指定位置的字符。
  • String substring(int begin,int end):返回从begin位置到end-1结束的子字符串,因此子字符串的长度是end-begin
  • String substring(int begin):返回从begin位置开始到串末尾的字符串。
  • String replaceAll(String regex,String replacement):将字符串中所有与正则式regex匹配的子字符串用新的字符串replacement替换。
  • String trim():将当前字符串去除前部空格和尾部空格后的结果作为返回的字符串。
  • String toUpperCase():结果是将字符串的所有字符全部换大写字母表示。
  • String toLowerCase():结果是将字符串的所有字符全部换小写字母表示。

[例7-2]:从命令行参数获取一个字符串,统计其中有多少数字字符,多少英文字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Ex7_2 {
public static void main(String[] args) {
String a = args[0];
int n = 0, c = 0;//n表示数字字符个数,c表示字母字符个数。
for (int k = 0; k < a.length(); k++) {
char x = a.charAt(k);
if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z'))
c++;
if (x >= '0' && x <= '9')
n++;
}
System.out.println("数字字符" + n + "个" + ",字母字符" + c + "个");
}
}

字符串中字符或子串查找

下表列出的方法用来在字符串中查找某字符或子串的出现位置,如果未找到,则方法返回值为-1,带start参数的方法规定查找的开始位置。

方法 功能(返回参数在字符串中的位置)
int indexOf(int ch) ch的首次出现位置
int indexOf(int ch,int start) ch的首次出现位置>=start
int indexOf(String str) str的首次出现位置
int indexOf(String str,int start) str的首次出现位置>=start
int lastIndexOf(int ch) ch的最后出现位置
int lastIndexOf(int ch,int start) ch的最后出现位置>=start
int lastIndexOf(String str) str的最后出现位置
int lastIndexOf(String str,int start) str的最后出现位置>=start
  • 注意:字符串中第一个字符的位置是0,另外,还有两个方法可用来判断参数从是否为字符串的特殊子串。
    • boolean startsWith(String prefix):判断参数串是否为当前串的前缀。
    • boolean endsWith(String postfix):判断参数串是否为当前串的后缀。

[例7-3]:从一个带有路径的文件名中分离出文件名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Ex7_3 {
/*以下方法获取文件名,文件名是最后一个分隔符\后面的子串*/
public static String pickFile(String fullpath) {
int pos = fullpath.lastIndexOf('\\');
if (pos == -1)
return fullpath;
return fullpath.substring(pos + 1);
}

public static void main(String[] args) {
String filename = pickFile("d:\\java|\\example\\test.java");
System.out.println("filename=" + filename);
}
}

filename=test.java

说明

  • 字符串的查找和子字符串的提取在实际应用中经常遇到,读者要仔细体会查找与提取的配合,查找时经常出现要查找的目标在字符串中出现多次,事实上,本例中字符""就出现了3次,但这里只对离文件名近的出现位置感兴趣,所以选用lastIndexOf()方法进行查找。
  • 一个有趣的问题是找出一个字符串中所有英文和单词的个数,也许读者会认为把空格最为单词分隔符,统计空格数即可,显然这种方法是不准确的,首先,其他符号也可作为单词的分隔符,另外,两个单词之间也可能不止一个空格,从单词的定义出发来查找是可行的办法,单词是以字母开头后面跟若干字母的字符串,遇到一个非字母字符即为一个单词的结束。
  • 在Java中提供了一个类StringTokenizer专门分析一个字符串中的单词,以下程序演示了该类的用法:
1
2
3
4
5
6
7
8
9
10
11
12
class WordAnalyse {
public static void main(String[] args) {
StringTokenizer st = new StringTokenizer("hello every body");
while (st.hasMoreTokens()) { // 判断是否有后续单词。
System.out.println(st.nextToken());// 取下一个单词。
}
}
}

hello
every
body
  • 需要提醒读者注意的是,创建StringTokenizer对象时,如果未使用带分隔符的构造方法,则默认以空格作为单词间的分隔符。
  • 在String类中也提供了一个方法split()用来根据指定的分隔符分离字符串,这个方法非常有用,它的返回结果是一个字符串数组,数组的每个元素就是分离好的子字符串。
  • 格式:public String[ ] split(String regex)
  • 例如,对于字符串str="boo:and:foo”,split(":”)的结果为{"boo”,”and”,"foo"},而split("o”)的结果为{"b”,””,”:and:f”}

getChars()

getChars()方法将字符从字符串复制到目标字符数组。

1
public void getChars(int srcBegin, int srcEnd, char[] dst,  int dstBegin)
  • srcBegin – 字符串中要复制的第一个字符的索引。
  • srcEnd – 字符串中要复制的最后一个字符之后的索引。
  • dst – 目标数组。
  • dstBegin – 目标数组中的起始偏移量。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public static void main(String args[]) {
String Str1 = new String("www.test.com");
char[] Str2 = new char[4];

try {
Str1.getChars(4, 7, Str2, 0);
System.out.print("拷贝的字符串为:" );
System.out.println(Str2 );
} catch( Exception ex) {
System.out.println("触发异常...");
}
}
}
  • 以上程序执行结果为:
1
拷贝的字符串为:test

StringBuffer类

前面介绍的String类不能改变串对象中的内容,只能通过建立一个新串来实现串的变化,而创建对象过多不仅浪费内存,而且效率也低,要动态改变字符串,通常用StringBuffer类,StringBuffer类可实现字符串内容的添加,修改和删除。

创建StringBuffer对象

StringBuffer类的构造方法如下。

  • public StringBuffer():创建一个空的StringBuffer对象。
  • public StringBuffer(int length):创建一个长度为length的StringBuffer对象。
  • public StringBuffer(String str):用字符串String初始化新建的StringBuffer对象。

StringBuffer类的主要方法

方法 功能
StringBuffer append(Object obj) 将某个对象的串描述添加到StringBuffer尾部
StringBuffer insert(int position,Object obj) 将某个对象的串描述插入到StringBuffer中的某个位置
StringBuffer insert(int index,char[ ]str,int offset,int len) 将字符数组str中从offset位置开始的len个字符插入到串的index位置
StringBuffer setCharAt(int position,char ch) 用新字符替换指定位置字符
StringBuffer deleteCharAt(int position) 删除指定位置的字符
StringBuffer replace(int start,int end,String str) 将参数指定范围的一个子字符串用新的字符串替换
StringBuffer substring(int start,int end) 获取所指定范围的子字符串
int length() StringBuffer中串的长度(字符数)

例如,思考以下代码段对应的运行结果:

1
2
3
4
5
6
StringBuffer str1 = new StringBuffer();
str1.append("Hello,mary!");
str1.insert(6,30);
System.out.println(str1.toString());

Hello,30mary!
  • insert(6,30)将30添加到StringBuffer中并不是匹配insert(int position,Object obj)方法,而是执行了如下方法:
1
StringBuffer insert(int offset,int i)
  • StringBuffer类为各类基本类型提供了相应的方法将其数据添加到StringBuffer对象中,只是限于篇幅在上表中未将这些方法列出,StringBuffer类没有直接定义equals()方法,所以它将继承Object类的equals()方法。

[例7-4]:将一个字符串反转。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Ex7_4 {
public static void main(String[] args) {
String s = "Dot saw I was Tod";
int len = s.length();
StringBuffer dest = new StringBuffer(len);
for (int i = (len - 1); i >= 0; i--) {// 从后往前处理。
dest.append(s.charAt(i));
}
System.out.println(dest.toString());
}
}

doT saw I was toD
  • 第6行循环变量i的取值从串的末尾往前进行处理,第7行取串的第i个位置的字符添加到StringBuffer中,循环结束后,StringBuffer中的内容就是原来字符串的反转,实际上,在StringBuffer中有一个reverse()方法实现字符串的反转,本例只是演示字符串处理的一些方法的应用。
  • 仅用String类也可以对字符串的反转,代码如下:
1
2
3
4
5
String s = "Dot saw I was Tod";
String res = "";
for(int k=s.length();k >= 0;k--)
res = res + s.charAt(k);
System.out.println(res);
  • 循环中将res变量所指对象的值与获取的字符拼接产生新的字符串,将新的字符串对象赋给res,这里每个创建字符串对象均要占用内存空间,为Java的垃圾回收也带来负担,从效率上来讲比用StringBuffer类的拼接方法要差。

String Builder

  • StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)
  • 由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类,然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。

创建StringBuilder对象

构造方法 描述
StringBuilder() 创建一个容量为16的StringBuilder对象(16个空元素)
StringBuilder(CharSequence cs) 创建一个包含cs的StringBuilder对象,末尾附加16个空元素
StringBuilder(int initCapacity) 创建一个容量为initCapacity的StringBuilder对象
StringBuilder(String s) 创建一个包含s的StringBuilder对象,末尾附加16个空元素

StringBuilder类的主要方法

方法 功能
StringBuilder append(Object obj) 将某个对象的串描述添加到StringBuilder尾部
StringBuilder insert(int position,Object obj) 将某个对象的串描述插入到StringBuilder中的某个位置
StringBuilder insert(int index,char[ ]str,int offset,int len) 将字符数组str中从offset位置开始的len个字符插入到串的index位置
StringBuilder setCharAt(int position,char ch) 用新字符替换指定位置字符
StringBuilder deleteCharAt(int position) 删除指定位置的字符
StringBuilder replace(int start,int end,String str) 将参数指定范围的一个子字符串用新的字符串替换
StringBuilder substring(int start,int end) 获取所指定范围的子字符串
int length() StringBuilder中串的长度(字符数)
int capacity() StringBuilder中串的容量
int setLength() 将新长度作为参数,如果新长度大于旧长度,则额外位置(多过的部分)用空字符填充(空字符为\u0000)

注意:StringBuilder类有两个属性:lengthcapacity,它的长度是指其内容的长度,而其容量是指它可以容纳而不分配新的内存的最大字符数,length()capacity()方法分别返回其长度和容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Main {
public static void main(String[] args) {
// Create an empty StringBuffer
StringBuilder sb = new StringBuilder();
printDetails(sb);

// Append "good"
sb.append("good");
printDetails(sb);

// Insert "Hi " in the beginning
sb.insert(0, "Hi ");
printDetails(sb);

// Delete the first o
sb.deleteCharAt(1);
printDetails(sb);

// Append " be with you"
sb.append(" be with you");
printDetails(sb);

// Set the length to 3
sb.setLength(3);
printDetails(sb);

// Reverse the content
sb.reverse();
printDetails(sb);
}

public static void printDetails(StringBuilder sb) {
System.out.println("Content: \"" + sb + "\"");
System.out.println("Length: " + sb.length());
System.out.println("Capacity: " + sb.capacity());

// Print an empty line to separate results
System.out.println();
}
}


Java

上面的代码生成以下结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Content: ""
Length: 0
Capacity: 16

Content: "good"
Length: 4
Capacity: 16

Content: "Hi good"
Length: 7
Capacity: 16

Content: "H good"
Length: 6
Capacity: 16

Content: "H good be with you"
Length: 20
Capacity: 34

Content: "H g"
Length: 3
Capacity: 34

Content: "g H"
Length: 3
Capacity: 34

StringBuilder基本实现原理

StringBuilder类

  • 内部组成和构造方法与String类似,StringBuilder类也封装了一个字符数组,定义如下:
1
char[] value;
  • 与String不同,它不是final的,可以修改,另外,与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
1
int count;
  • StringBuilder继承自AbstractStringBuilder,它的默认构造方法是:
1
2
3
public StringBuilder() {
super(16);
}
  • 调用父类的构造方法,父类对应的构造方法是:
1
2
3
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
  • 也就是说,new StringBuilder()这句代码,内部会创建一个长度为16的字符数组,count的默认值为0

append方法

  • append方法的代码实现:
1
2
3
4
5
6
7
8
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
  • append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count体现,具体来说ensureCapacityInternal(count+len)会确保数组的长度足以容纳新添加的字符,str.getChars会拷贝新添加的字符到字符数组中,count+=len会增加实际使用的长度。
  • ensureCapacityInternal的代码如下:
1
2
3
4
5
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
  • 如果字符数组的长度小于需要的长度,则调用expandCapacity进行扩展,expandCapacity的代码是:
1
2
3
4
5
6
7
8
9
10
11
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
  • 扩展的逻辑是,分配一个足够长度的新数组,然后将原内容拷贝到这个新数组中,最后让内部的字符数组指向这个新数组,这个逻辑主要靠下面这句代码实现:
1
value = Arrays.copyOf(value, newCapacity);
  • 那newCapacity是怎么算出来的?
  • 参数minimumCapacity表示需要的最小长度,需要多少分配多少不就行了吗?不行,因为那就跟String一样了,每append一次,都会进行一次内存分配,效率低下,这里的扩展策略,是跟当前长度相关的,当前长度乘以2,再加上2,如果这个长度不够最小需要的长度,才用minimumCapacity
  • 比如说,默认长度为16,长度不够时,会先扩展到16*2+2即34,然后扩展到34*2+2即70,然后是70*2+2即142,这是一种指数扩展策略,为什么要加2?大概是因为在原长度为0时也可以一样工作吧。
  • 为什么要这么扩展呢?这是一种折中策略,一方面要减少内存分配的次数,另一方面也要避免空间浪费,在不知道最终需要多长的情况下,指数扩展是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中。
  • 那如果预先就知道大概需要多长呢?可以调用StringBuilder的另外一个构造方法:
1
public StringBuilder(int capacity)