10道不得不会的Java基础面试题 (qq.com)
如何理解 String 类型值的不可变? - 知乎 (zhihu.com)
为什么 String 被设计为是不可变的? (qq.com)
面试题系列第2篇:new String()创建几个对象?有你不知道的-腾讯云开发者社区-腾讯云 (tencent.com)
String、StringBuffer、StringBuilder 的区别
String是不可变类的典型实现,被声明为final class,除了hash这个属性其他属性都声明为final。由于他是不可变的,拼接字符串时会产生很多无用的中间对象,频繁操作会对性能有影响;
StringBuffer是线程安全的可修改的字符序列,把所有修改数据的方法都加上了synchronized;
StringBuilder是线程不安全的可修改的字符序列,但是比用 StringBuffer
能获得 10%~15% 左右的性能提升;
追问:String的不可变体现在哪?
String不可变很简单,如下图,给一个已有字符串"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
-
1:首先String类是用final关键字修饰,这说明String不可继承,进而避免了子类破坏 String 不可变。
-
2:String类的主力成员字段value是个char[ ]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** String本质是个char数组. 而且用final关键字修饰.*/
private final char value[];
...
...
}
真的不可变吗?
value用final修饰,编译器不允许我把value指向堆区另一个地址。
但如果我直接对数组元素动手,分分钟搞定。
final int[] value={1,2,3}; value[2]=100; //这时候数组里已经是{1,2,100}
追问:设计成不可变有什么好处?
一、String 不可变的第一个好处是可以使用字符串常量池。
String s1 = "lagou";
String s2 = "lagou";
s1 = "LAGOU";
System.out.println(s2); //输出lagou
s1 和 s2 背后指向的都是常量池中的同一个“lagou”
如果String 对象是可变的,那么把 s1 指向的对象从小写的“lagou”修改为大写的“LAGOU”之后,s2 理应跟着变化,那么此时打印出来的 s2 也会是大写的。这就和我们预期不符了,同样也就没办法实现字符串常量池的功能了,因为对象内容可能会不停变化,没办法再实现复用了。
二、用作 HashMap 的 key
String 不可变的第二个好处就是它可以很方便地用作 HashMap (或者 HashSet) 的 key。通常建议把不可变对象作为 HashMap的 key,比如 String 就很合适作为 HashMap 的 key。
三、缓存 HashCode
String 不可变的第三个好处就是缓存 HashCode。
在 Java 中经常会用到字符串的 HashCode,在 String 类中有一个 hash 属性,代码如下:
/** Cache the hash code for the String */
private int hash;
这是一个成员变量,保存的是 String 对象的 HashCode。因为 String 是不可变的,所以对象一旦被创建之后,HashCode 的值也就不可能变化了,我们就可以把 HashCode 缓存起来。这样的话,以后每次想要用到 HashCode 的时候,不需要重新计算,直接返回缓存过的 hash 的值就可以了,因为它不会变,这样可以提高效率,所以这就使得字符串非常适合用作 HashMap 的 key。
而对于其他的不具备不变性的普通类的对象而言,如果想要去获取它的 HashCode ,就必须每次都重新算一遍,相比之下,效率就低了。
四、线程安全
因为具备不可变的对象一定是线程安全的,我们不需要对其采取任何额外的措施,就可以天然保证线程安全。
由于String是不可变的,所以他就可以非常安全的被多个线程共享,这对多线程编程非常重要,避免了很多不必要的同步操作。
String字符串拼接 (+) 时发生了什么?
Java本身不支持运算符重载,+和+=是专门为String类重载过的运算符
字符串通过+的字符串拼接方式,本质上是调用StringBuilder
的append()
方法实现的,拼接完成之后toString()
得到一个新的String对象返回。
那为什么比StringBuilder
性能低呢?
在多次对字符串的操作中,StringBuilder对象并不会复用,而是每一次循环创建一个StringBuilder对象。
String#intern 方法有什么作用?
String.intern()
是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
下面的语句执行时创建了几个字符串对象?
String str1 = "abc"; // 在常量池中
创建0个或1个对象。
1、如果常量池中已经存在了“abc”,那么不会再创建对象,直接将引用赋值给str1;
2、如果常量池中没有“abc”,那么创建一个对象,并将引用赋值给str1。
String s1 = new String("abc");// 在堆上
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
以下实例我们暂且不考虑常量池中是否已经存在对应字符串的问题,假设都不存在对应的字符串。
String str = "abc" + "def";
上面的问题涉及到字符串常量重载“+”的问题,当一个字符串由多个字符串常量拼接成一个字符串时,它自己也肯定是字符串常量。字符串常量的“+”号连接Java虚拟机会在程序编译期将其优化为连接后的值。
就上面的示例而言,在编译时已经被合并成“abcdef”字符串,因此,只会创建1个对象。并没有创建临时字符串对象abc和def,这样减轻了垃圾收集器的压力。
String str = "abc" + new String("def");
上述的代码Java虚拟机在编译的时候同样会优化,会创建一个StringBuilder来进行字符串的拼接,实际效果类似:
String s = new String("def");
new StringBuilder().append("abc").append(s).toString();
很显然,多出了一个StringBuilder对象,那就应该是5个对象。
有同学可能会想了,StringBuilder最后toString()之后的“abcdef”难道不在常量池存一份吗?
这个还真没有存,我们来看一下这段代码:
@Test
public void testString3() {
String s1 = "abc";
String s2 = new String("def");
String s3 = s1 + s2;
String s4 = "abcdef";
// 如果s1+s2的结果在常量池中存了一份,那么s3中的value引用应该和s4中value的引用是一样的才对。
System.out.println(s3==s4); // false
}
s4很明确是存在于常量池中,那么s3对应的值存储在哪里呢?很显然是在堆对象中。
我们来看一下StringBuilder的toString()方法是如何将拼接的结果转化为字符串的:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
很显然,在toString方法中又新创建了一个String对象,而该String对象传递数组的构造方法来创建的:
public String(char value[], int offset, int count)
也就是说,String对象的value值直接指向了一个已经存在的数组,而并没有指向常量池中的字符串。
因此,上面的准确回答应该是创建了4个字符串对象和1个StringBuilder对象。