Java基础面试题必须有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不可变非常简单,如下图所示,给出一个现有的字符串"abcd"第二次赋值成"abcedl",重新指向新对象、新地址,而不是在原始内存地址上修改数据。
1:首先,String类是用final关键词修改的,这说明String是不可继承的,从而避免了子类的破坏 String 不可变。
2:value是String类的主要成员[ ]数组,用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“字符串,因此,只能创建一个对象。临时字符串对象abc和def没有创建,这减轻了垃圾收集器的压力。
String str = "abc" + new String("def");
上述代码Java虚拟机在编译时也会进行优化,创建StringBuilder拼接字符串,实际效果相似:
String s = new String("def");new StringBuilder().append("abc").append(s).toString();
显然,多了一个Stringbuilder对象,那应该是五个对象。
有些学生可能会想,在Stringbuilder最终tostring()之后,“abcdef难道不是常量池存一份吗?
我们来看看这个代码:
@Testpublic 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()方法是如何将拼接结果转化为字符串的:
@Overridepublic 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值直接指向现有数组,而不指向常量池中的字符串。
因此,上述准确答案应该是创建四个字符串对象和一个Stringbuilder对象。