1 String
是不可变的
String
对象一旦被创建就固定不变了,对它的任何改变都不会影响到原对象,相关的操作都会生成新的对象。 以下是String
的源码:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
}
由上可知String
是final
类型的,其中的成员变量大部分也是final
类型的。
Java
与C++
不同的点是,在Java
中不可能直接操作对象本身,必须通过引用才能访问对象, 包括获取成员变量的值,改变成员变量的值,调用对象的方法等。
String s = "abc";
System.out.println("s = " + s); // s = abc
s = "123";
System.out.println("s = " + s); // s = 123
这里的s
只是String
类型的引用,而不是对象本身。s = "123";
之后又创建了一个新的对象"123"
,而s
又指向了这个新的对象,而原来的"abc"
这个对象并没有改变。
无论是sub
操作、concat
操作还是replace
操作都不是在原有的字符串上进行的,而是重新生成一个新的字符串对象,也就是说,进行这些操作之后,最原始的字符串并没有发生改变。 以下是源码:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); // 1
}
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true); // 2
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true); // 3
}
}
return this;
}
}
String
为什么要设计成不可变的?
- 字符串常量池的需要:如果创建一个
String
对象时,相关字符串已经存在于常量池中,就不会创建一个新的字符串,而是引用已经存在的字符串。如果允许改变,将导致各种逻辑错误,比如改变一个对象将会影响另一个独立对象,严格来说,这种常量池的思想是一种优化手段; - 允许
String
对象缓存HashCode
:Java
中String
对象的哈希码会被频繁的使用,比如在HashMap
中。字符串不变保证了hash
码的唯一性,因此可以放心的进行缓存。这也是一种优化手段,意味着不必每次都计算新的哈希码。在String
类中有private int hash
来缓存hashcode
; - 安全性:
String
被许多的类来当做参数,如网络url
,文件路径path
等等,如果String
不是固定的,将会引起各种安全隐患;
2 字符串的创建
在Java
语言中,有8
种基本类型和一种比较特殊的类型String
。为了使它们在运行过程中速度更快,更节省内存,在Java
虚拟机的方法区中提供了一种常量池的概念,常量池可以理解成Java
虚拟机提供的缓存。
常量池实际上分为两种状态:静态常量池和运行时常量池。
- 静态常量池:即
.class
文件中的常量池,.class
文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法等信息 - 运行时常量池:
JVM
在完成类装载操作后,将.class
文件中的常量池载入内存中,并保存在方法区中,我们常说的常量池时指方法区中的运行时常量池。
字符串的创建方式:
- 直接使用双引号声明的
String
对象会直接存储在字符串常量池中。 这种方式不会产生垃圾空间 - 采用
new
关键字创建的String
对象,首先会从字符串常量池中查询当前字符串是否存储,如果存在,直接在堆区创建对象;如果不存在会将当前字符串放入常量池中,并在堆出创建相应的String
对象 - 使用
+
连接,生成新的字符串。- 如果是只有字面量的字符串连接,生成的字符串放在字符串常量池中
- 如果是包含字符串引用的拼接,首先会在常量池中检查是否有字符串,如果没有,在常量池中创建,并在堆中创建对象,如果有,直接在堆中创建对象
3 String
的堆栈问题
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true
}
采用字面量的方式创建一个字符串时,JVM
首先会去常量池中查找是否存在abc
这个字符串对象,如果不存在,则在字符串常量池中创建"abc"
这个对象,然后将abc
这个对象的引用地址返回给"abc"
对象的引用s1
,这样s1
就会指向常量池中"abc"
这个字符串对象;如果存在,则不创建任何对象,直接将"abc"
的地址返回给s1
。 由此可知,s1
和s2
指向的同一个对象。
public static void main(String[] args) {
String s3 = new String("xyz");
String s4 = new String("xyz");
System.out.println(s3 == s4); // false
System.out.println(s3.equals(s4)); // true
}
采用new
关键字新建一个字符串对象的时候,JVM
首先会在常量池中查找有没有xyz
这个字符串对象,如果没有,则首先在常量池中创建一个xyz
字符串对象,然后再在堆中创建一个xyz
字符串对象,将堆中的xyz
字符串对象的地址返回给s3
;如果有,则不在池中创建xyz
这个对象了,直接在堆中创建xyz
字符串对象,然后将堆中的xyz
这个对象的地址返回给s3
。 由此可知,s3
和s4
指向不同的对象。
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" + "world";
String s5 = "hello" + "world";
System.out.println("================");
System.out.println(s3 == s4); // true
System.out.println(s4 == s5); // true
String s6 = new String("helloworld");
String s7 = new String("helloworld");
String s8 = s7;
System.out.println("================");
System.out.println(s3 == s6); // false
System.out.println(s6 == s7); // false
System.out.println(s8 == s7); // true
String s9 = "hello" + new String("world");
System.out.println("================");
System.out.println(s9 == s3); // false
System.out.println(s9 == s6); // false
String s10 = s1 + s2;
System.out.println("================");
System.out.println(s10 == s3); // false
System.out.println(s10 == s6); // false
System.out.println(s10 == s9); // false
final String s = "a"; // 用final修饰,相当于一个常量
String str5 = s + "b";
System.out.println(str5 == "ab"); // true
}
s4
是字面量连接,放在常量池中,所以s3
、s4
指向常量池中的同一个对象;s5
、s6
都是字符串引用拼接,创建的对象都在堆中。
问:在String str = new String("abc");
创建多少个对象?
- 在常量池中查找是否有
abc
,如果没有,在常量池中创建对象"abc"
- 在堆中创建一个
String
类型,值为"abc"
的对象, - 将对象地址赋值给
str
,创建一个引用
因此,如果常量池中没有"abc"
,则创建两个对象,如果有则创建一个。
问:String str = new String("A" + "B");
会创建多少个对象?
字符串常量池:"A"
、"B"
、"AB"
;堆:new String("AB")
;str
引用
问:String str = new String("ABC") + "ABC"
会创建多少个对象?
字符串常量池:ABC
;堆:new String("ABC")
;str
引用
问:String a = "abc"; String b = new String("abc");
,a
和b
是否都能放入HashSet
中?
因为a.equals(b)
是true
,所以HashSet
中只能放一个。
问:String str1 = "a" + "b" + "c"; String str2 = "abc";
会创建几个对象?
字符串常量池:"a"
、"b"
、"c"
、"abc"
;
4 ==
和String.equals
两种比较String
的方式:==
比较的是两个对象的地址,equals
比较的是两个对象的内容。
以下是String.equals
的源码:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
public boolean equals(Object anObject) {
// 1.
if (this == anObject) {
return true;
}
// 2.
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = length();
if (n == anotherString.length()) {
int i = 0;
while (n-- != 0) {
if (charAt(i) != anotherString.charAt(i))
return false;
i++;
}
return true;
}
}
return false;
}
}
由此可知,String.equals
方法判断的是值是否相等。
5 String
、StringBuffer
、StringBuilder
的区别
- 可变与不可变:
String
是不可变的字符串对象,StringBuilder
和StringBuffer
是可变字符串对象(其内部的字符串数组长度可变) - 是否多线程安全:
String
中的对象是不可变的,也就可以理解为常量,线程安全。StringBuffer
中的方法大多采用了synchronized
修饰,是线程安全的,而StringBuilder
没有这个修饰,可以认为是非线程安全的。(StringBuffer
与StringBuilder
中的方法和功能是等价的) - 三者的执行效率:
StringBuilder
>StringBuffer
>String
(但这也是相对的,不一定所有的情况都是这样),在字符串相加操作或者改动较少的情况下,建议使用String str = "hello"
这种形式;当字符串相加操作较多的情况下,建议使用StringBuilder
,多线程的情况下,采用StringBuffer
- 对象是否可变:
String
是不可变的对象,每次对String
类型进行改变的等同于生成了一个新的String
对象,经常改变内容的字符串不建议使用String
;对StringBuffer
类改变,每次结果都会对StringBuffer
对象本身进行操作,而不是生成新的对象,再改变对象引用,经常改变内容的字符串建议使用StringBuffer
;
参考
https://blog.csdn.net/weixin_40304387/article/details/81071816
https://segmentfault.com/a/1190000009888357
https://www.cnblogs.com/rgever/p/8902210.html
https://zhuanlan.zhihu.com/p/81869136
https://www.cnblogs.com/kuillldan/p/5570675.html
https://www.cnblogs.com/aspirant/p/9193112.html
https://www.cnblogs.com/Alan-Jones/p/9089816.html
https://www.cnblogs.com/xiaoxi/p/6036701.html
https://www.cnblogs.com/Alan-Jones/p/9089816.html
https://blog.csdn.net/werewolf2017/article/details/90043632
https://blog.csdn.net/claram/article/details/53770830
https://blog.csdn.net/zhangjg_blog/article/details/18319521