上一篇博客:Java String详解(一)
写在前面:大家好!我是
晴空๓
。如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正,感谢大家的不吝赐教。我的唯一博客更新地址是:https://ac-fun.blog.csdn.net/。非常感谢大家的支持。一起加油,冲鸭!
用知识改变命运,用知识成就未来!加油 (ง •̀o•́)ง (ง •̀o•́)ง
文章目录
- 前言
- substring()截取字符串的子串
- 连接字符串
- 使用 + 进行连接
前言
在上一篇博客Java String详解(一)中主要讲了String类的声明、字符串存储的位置以及String的不可变性。本篇主要写一下关于String的常用方法以及一些注意点。
substring()截取字符串的子串
String类的 substring 方法可以从一个较大的字符串中提取出一个子串。通过查看String.java类可以发现一共有两种截取子串的方法:
public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)
对于这个方法主要注意的是 substring() 是从 0 开始的;截取的子字符串包含 beginIndex 坐标的字符,但是不包含 endIndex 的字符(前闭后开式[))。通过substring()的源码就可以看到 substring() 会返回一个新的字符串对象,而不是修改了原有的字符串。这也是String不可变的一个体现:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
连接字符串
使用 + 进行连接
Java 语言允许使用 + 号拼接两个字符串,应用于 String 的 + 和 += 是 Java 中仅有的被重载的操作符,Java 不允许程序员重载其他操作符。我们可以使用 JDK 自带的 javap 工具反编译一下以下使用 + 号进行连接字符串时发生了什么:
public class StringTest {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "world";
String str = str1 + str2;
System.out.println(str);
}
}
使用 javac .\StringTest.java 编译代码之后再使用 javap -c .\StringTest.class 反编译.class文件,使用 -c 选项对代码进行反汇编,显示 Java 字节码的指令。可以看到如下字节码及相应的作用:
Compiled from "StringTest.java"
public class StringTest {
public StringTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String world
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_3
29: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
32: return
}
ldc 命令是从常量池中加载各种类型的常量,包括字符串常量、整数常量、浮点数常量,甚至类引用等。对于字符串常量,ldc 指令的行为如下:
1. 从常量池加载字符串:ldc 首先检查字符串常量池中是否已经有内容相同的字符串对象。
2. 复用已有字符串对象:如果字符串常量池中已经存在内容相同的字符串对象,ldc 会将该对象的引用加载到操作数栈上。
3. 没有则创建新对象并加入常量池:如果字符串常量池中没有相同内容的字符串对象,JVM 会在常量池中创建一个新的字符串对象,并将其引用加载到操作数栈中。
new 命令是创建一个新的对象,指令后面紧跟着指向常量池中的索引值,该索引值指向一个类符号引用,即类的全限定名。
invokevirtual 命令是 JVM 字节码用于调用对象实例方法的指令
从以上字节码我们可以看出 java 中使用 + 拼接字符串本质上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。这里需要注意的一个点就是当我们在循环内使用 “+” 对字符串进行拼接的话会有一个明显的缺陷,那就是会造成编译器创建多个 StringBuilder 对象。每进行一次循环就会创建一个 StringBuilder 对象。如下:
public class StringTest {
public static void main(String[] args) {
String[] arr = {"he", "llo", "wor" + "ld"};
String str = "";
for (int i = 0; i < arr.length; i++) {
str += arr[i];
}
System.out.println(str);
}
}
使用 javac 反编译之后可以看到字节码如下:
Compiled from "StringTest.java"
public class StringTest {
public StringTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_3
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String he
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String llo
13: aastore
14: dup
15: iconst_2
16: ldc #5 // String world
18: aastore
19: astore_1
20: ldc #6 // String
22: astore_2
23: iconst_0
24: istore_3
25: iload_3
26: aload_1
27: arraylength
28: if_icmpge 58
31: new #7 // class java/lang/StringBuilder
34: dup
48: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
51: astore_2
52: iinc 3, 1
55: goto 25
58: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
61: aload_2
62: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
65: return
}
我们只需要关注 20~55 即可,相关的解释如下:
20: ldc #6 // String:加载空字符串。
22: astore_2:将其存储在局部变量 StringBuilder 引用。
23: iconst_0:将 0 压入栈(循环索引)。
24: istore_3:存储循环索引。
25: iload_3:加载循环索引。
26: aload_1:加载字符串数组引用。
27: arraylength:获取数组长度。
28: if_icmpge 58:如果循环索引大于等于数组长度,跳转到标签 58(结束循环)。
31: new #7 // class java/lang/StringBuilder:创建一个新的 StringBuilder 对象。
34: dup:复制 StringBuilder 对象引用。
48: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;:调用 StringBuilder 的 toString 方法。
51: astore_2:将结果字符串存储在局部变量 StringBuilder 引用。
52: iinc 3, 1:循环索引加 1。
55: goto 25:跳转回循环开始。
所以当我们需要在循环内进行字符串拼接时最好直接使用 StringBuilder 对象进行字符串拼接,这样就不会造成以上的问题。如果你使用的是 IDEA 的话,IDEA 自带的代码检查也会提示你修改代码
public class StringTest {
public static void main(String[] args) {
String[] arr = {"he", "llo", "wor" + "ld"};
StringBuilder str = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
str.append(arr[i]);
}
System.out.println(str);
}
}
未完待续……
- 《大话Java性能优化》
- 字符串拼接用 + 还是 stringbuilder
- 还在无脑用 StringBuilder?来重温一下字符串拼接吧