String
String是JAVA中最常用的对象,就是这样一个最常用最普通的对象,当你深入研究时却发现我们并不是真的很了解它,那么让我们一起来学习它吧!
因为String不可变的性质,因此Java内部实现了常量池。当一个String被创建时,会先去常量池查看有没有值相同的示例,有的话直接返回。节省了内存,加快了字符串的加载速度。不可变的对象也可以保证在并发中保持线程安全
特性
- 字符串常量,实际上也是String对象
- 所有不是通过new创建的String都是放在常量池中
- String类型的对象是不可变的
- String实现了CharSequence接口
String对象创建方式
String str1 = "abcd";
String str2 = new String("abcd");
这两种不同的创建方法是有差别的,第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象。
只要使用new方法,便需要创建新的对象
连接表达式+(加号)
- 只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。
- 对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false
String str5 = "string";
System.out.println(str3 == str5);//true
1、 Sting s; //定义了一个变量s,没有创建对象;
2、 = // 赋值,将某个对象的引用(句柄)赋给s ,没有创建对象;
3、 “abc” //创建一个对象;
4、 new String(); // 创建一个对象。
常用方法
length 返回字符串长度
isEmpty 判断字符串是否为空
charAt 根据索引位置获取char
getChars 复制对应位置范围的char到数组中
equals, equalsIgnoreCase 对比顺序依次为引用地址,char数组长度,char数组内容
compareTo 对比字符串大小
startsWith, endsWith 判断前后缀
hashCode 计算hash值, 公式为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
indexOf 查找首次出现的位置
lastIndexOf 查找最后出现的位置
substring 返回子串(旧版本是返回一个引用在父串的一个新串,节省重新分配内存。但实际如果子串引用了一个占用极大的父串,会因为子串一直被使用导致父串没法被垃圾回收,新版本substring每次重新复制char数组)
concat 拼接字符串(拼接char数组,重新创建字符串)
replace 用新字符替换所有的旧字符(会先遍历一次char数组,寻找时候存在,再去替换,避免每次都要分配char数组)
matches 判断是否符合正则 (复用Pattern.matches()方法)
contains 判断是否包含子串(复用indexOf()方法)
replaceFirst 只替换一次
replaceAll 替换所有正则符合的地方
split 按照正则分割字符串
toLowerCase 返回小写
toUpperCase 返回大写
trim 去除前后空格
toCharArray 重新复制char数组返回
-
join(CharSequence delimiter, CharSequence... elements)
String.join(",", "you", "bao", "luo"); //out: you,bao,luo
equals(Object anObject)
String.equals()代码逻辑:
判断传入的对象与当前对象是否为同一个对象,如果是就直接返回true;
判断传入的对象是否为String,若不是则返回false(如果为null也不成立);
判断传入的String与当前String长度是否一致,若不一致则返回false;
循环对比两个字符串的char[]数组,逐个对比字符是否一致,若不一致则直接返回false;
循环结束没有找到不匹配的则返回true;
JDK8源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- intern():naive方法,直接返回常量池中的引用
当调用intern()方法时,JVM会在常量池中通过equals()方法查找是否存在等值的String,如果存在则直接返回常量池中这个String对象的地址;如果不存在则会创建等值的字符串(即等值的char[]数组字符串,但是char[]是新开辟的一份拷贝空间),然后再返回这个新创建空间的地址;
在常量池查找等值String时,通常不止一个字符串而是多个字符串因此效率会比较低,另外为保证唯一性,需要有锁的介入;
String str1 = "ab";
String str2 = new String("ab");
System.out.println(str1== str2);//false
System.out.println(str2.intern() == str1);//true
System.out.println(str1== str2);//false
str2 = str2.intern();
System.out.println(str1== str2);//true
知识点
- 在调用x.toString()的地方可以用""+x替代;
- 字符串的+拼接操作
public static void main(String[] args) throws InterruptedException {
String s = "a";
String st = s + "b" + "c";
}
javap out====>
Code:
stack=3, locals=3, args_size=1
0: ldc #19 // String a
2: astore_1
3: new #21 // class java/lang/StringBuilder
6: dup
7: aload_1
8: invokestatic #23 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
11: invokespecial #29 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
14: ldc #32 // String b
16: invokevirtual #34 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #38 // String c
21: invokevirtual #34 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #40 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_2
28: return
- StringBuffer是线程安全操作
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
- StringBuilder非线程安全
public StringBuilder append(String str) { super.append(str); return this; }
System.err.println("hello,world"); ##hello,world实际是String对象
printf格式化输出
FAQ
String str1 = "abc"; System.out.println(str1 == "abc");
步骤:
a> 栈中开辟一块空间存放引用str1;
b> String池中开辟一块空间,存放String常量"abc";
c> 引用str1指向池中String常量"abc";
d> str1所指代的地址即常量"abc"所在地址,输出为true;String str2 = new String("abc"); System.out.println(str2 == "abc");
步骤:
a> 栈中开辟一块空间存放引用str2;
b> 堆中开辟一块空间存放一个新建的String对象"abc";
c> 引用str2指向堆中的新建的String对象"abc";
d> str2所指代的对象地址为堆中地址,而常量"abc"地址在池中,输出为false;
注意:对于通过new产生的对象,会先去常量池检查有没有 “abc”,如果没有,先在常量池创建一个 “abc” 对象,然后在堆中创建一个常量池中此 “abc” 对象的拷贝对象;String s2 = new String(“Hello”); 产生几个对象?
首先,在jvm的工作过程中,会创建一片的内存空间专门存入string对象。我们把这片内存空间叫做string池;
String s2 = new String(“Hello”);jvm首先在string池内里面看找不找到字符串"Hello",如果找到不做任何事情;否则创建新的string对象,放到string池里面。由于遇到了new,还会在内存Heap上(不是string池里面)创建string对象存储"Hello",并将内存上的(不是string池内的)string对象返回给s2。
Re: 如果常量池中原来没有“Hello”, 则创建两个对象。如果原来的常量池中存在“Hello”时,就是一个对象;其它
String str1 = "a";
String str2 = "b";
String str3 = str1 + "b";
//str1 和 str2 是字符串常量,所以在编译期就确定了。
//str3 中有个 str1 是引用,所以不会在编译期确定。
//又因为String是 final 类型的,所以在 str1 + "b" 的时候实际上是创建了一个新的对象,在把新对象的引用传给str3
final String str1 = "a";
String str2 = "b";
String str3 = str1 + "b";
//这里和\(3\)的不同就是给 str1 加上了一个final,这样str1就变成了一个常量。
//这样 str3 就可以在编译期中就确定了
编译期优化
编译器在编译期会针对字符串常量叠加得到固定值,字符串常量包括"hello"或用fianl修饰的变量,编译器认为这些常量是不可变的
编译器优化String常量连接
示例一
String str = "hello" + "java" + 1;
// 编译期编译器会直接编译为"hellojava1"
#2 = String #21 // hellojava1
#21 = Utf8 hellojava1
示例二
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
output ==> s等于t,它们是同一个对象
说明:A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s="ab"+"cd";
示例三
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
output ==> s不等于t,它们不是同一个对象
A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了
循环内String加操作
- 性能较低的代码:
public void implicitUseStringBuilder(String[] values) {
String result = "";
for (int i = 0 ; i < values.length; i ++) {
result += values[i];
}
System.out.println(result);
}
编译后的字节码:
public void implicitUseStringBuilder(java.lang.String[]);
Code:
0: ldc #11 // String
2: astore_2
3: iconst_0
4: istore_3
5: iload_3
6: aload_1
7: arraylength
8: if_icmpge 38
11: new #5 // class java/lang/StringBuilder
14: dup
15: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
18: aload_2
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_1
23: iload_3
24: aaload
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_2
32: iinc 3, 1
35: goto 5
38: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
41: aload_2
42: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: return
其中8: if_icmpge 38 和35: goto 5构成了一个循环;
8: if_icmpge 38的意思是如果(i < values.length的相反结果)成立,则跳到第38行(System.out)。
35: goto 5则表示直接跳到第5行。
但是这里面有一个很重要的就是StringBuilder对象创建发生在循环之间,也就是意味着有多少次循环会创建多少个StringBuilder对象,这样明显性能较低
- 性能较高的代码
public void explicitUseStringBuider(String[] values) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < values.length; i ++) {
result.append(values[i]);
}
}
public void explicitUseStringBuider(java.lang.String[]);
Code:
0: new #5 // class java/lang/StringBuilder
3: dup
4: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
7: astore_2
8: iconst_0
9: istore_3
10: iload_3
11: aload_1
12: arraylength
13: if_icmpge 30
16: aload_2
17: aload_1
18: iload_3
19: aaload
20: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: iinc 3, 1
27: goto 10
30: return
从上面可以看出,13: if_icmpge 30
和27: goto 10
构成了一个loop循环,而0: new #5
位于循环之外,所以不会多次创建StringBuilder.
注意:循环体中需要尽量避免隐式或者显式创建StringBuilder
不可变的String
String对象是不可变的。 String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容
String str1 = "java";
String str2 = "java";
System.out.println\("str1=str2 " + \(str1 == str2\)\);
在代码中,可以创建同一个String对象的多个别名,而它们所指的对象是相同的,一直待在一个单一的物理位置上
重载“+”
在Java中,唯一被重载的运算符就是用于String的“+”与“+=”。除此之外,Java不允许程序员重载其他的运算符
public class StringTest {
String a = "abc";
String b = "mongo";
String info = a + b + 47;
}
String对象是不可变的,所以在上述的代码过程中可能会是这样工作的:
- "abc" + "mongo"创建新的String对象abcmongo;
- "abcmongo" + "47"创建新的String对象abcmongo47;
- 引用info 指向最终生成的String;
但是这种方式会生成一大堆需要垃圾回收的中间对象,性能相当糟糕
编译器的优化处理
Compiled from "StringTest.java"
public class StringTest {
java.lang.String a;
java.lang.String b;
java.lang.String info;
public StringTest();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":
()V
4: aload_0
5: ldc #14 // String abc
7: putfield #16 // Field a:Ljava/lang/String;
10: aload_0
11: ldc #18 // String mongo
13: putfield #20 // Field b:Ljava/lang/String;
16: aload_0
17: new #22 // class java/lang/StringBuilder
20: dup
21: aload_0
22: getfield #16 // Field a:Ljava/lang/String;
25: invokestatic #24 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
28: invokespecial #30 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
31: aload_0
32: getfield #20 // Field b:Ljava/lang/String;
35: invokevirtual #33 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
38: bipush 47
40: invokevirtual #37 // Method java/lang/StringBuilder.ap
pend:(I)Ljava/lang/StringBuilder;
43: invokevirtual #40 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
46: putfield #44 // Field info:Ljava/lang/String;
49: return
}
反编译以上代码会发现,编译器自动引入了StringBuilder类。
编译器创建了一个StringBuilder对象,并调用StringBuilder.append()方法,最后调用toString()生成结果,从而避免中间对象的性能损耗
字符串常量池的设计思想
- 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
- JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟一个字符串常量池,类似于缓存区
- 创建字符串常量时,首先坚持字符串常量池是否存在该字符串
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
- 实现的基础
- 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享
- 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收
- 常量池的好处:常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中;
- 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间;
- 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等
字符串常量池存储位置 {#articleHeader1}
字符串常量池则存在于方法区
String str1 = “abc”;
String str2 = “abc”;
String str3 = “abc”;
String str4 = new String(“abc”);
String str5 = new String(“abc”);