当前位置 : 主页 > 编程语言 > java >

说说String吧

来源:互联网 收集:自由互联 发布时间:2023-09-06
10道不得不会的Java基础面试题 (qq.com) 如何理解 String 类型值的不可变? - 知乎 (zhihu.com) 为什么 String 被设计为是不可变的? (qq.com) 面试题系列第2篇:new String()创建几个对象?有你不知

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",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。

alt

  • 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”

alt

如果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类重载过的运算符

字符串通过+的字符串拼接方式,本质上是调用StringBuilderappend()方法实现的,拼接完成之后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对象。

上一篇:说说异常(Exception)吧
下一篇:没有了
网友评论