本文原文链接
45岁老架构 尼恩说在前面
在45岁老架构师 尼恩的读者交流群(100+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的相关面试题:
String 为什么 不可变?
String .intern() 的原理是什么?
最近有小伙伴面试美团,都问到了这个面试题。 小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
String为什么是不可变的
在Java中,String
类型被设计为不可变的(immutable),这意味着一旦一个 String
对象被创建,它的内容就不能被改变。
不可变的(immutable)String 定义
在 Java 中,String
类被设计为不可变的(immutable)。
这意味着一旦一个String
对象被创建,它的值就不能被改变。
例如:
String str = "技术自由圈";
str = str + " 真的都是顶级高手";
在这个例子 ,第二句 是改变了str
的值,
实际上,当执行str + " 真的都是顶级高手"
时,一个新的String
对象被创建,而原来的"技术自由圈"
字符串对象并没有被修改,而是创建了一个新的 String对象。
所以实际上,上面的代码产生了三个 String 对象:
第一个 String 对象: "技术自由圈"
第二个 String 对象: " 真的都是顶级高手"
第三个 String 对象:"技术自由圈 真的都是顶级高手"
String 定义的存储方式
JDK 8 及之前的存储方式
在 JDK 8 及之前,String
类内部是通过一个char[]
数组来存储字符串的内容。
在String
类的源码中可以看到如下定义:private final char value[];
。
JDK 8 及之前的 immutable 不可变 原理
第一,字符数组使用 final修饰:在
String
类的内部,字符串的值是通过一个char
数组来存储的。这个char
数组被声明为private final
,如在 Java 的String
类源码中可以看到private final char value[];
。final
关键字保证了这个数组的引用不能被重新赋值。一旦String
对象被创建,这个char
数组的引用就固定了,不能指向其他的字符数组。第二,没有提供修改方法:
String
类没有提供可以修改这个字符数组内容的公共方法。很多的方法,看起来像是修改String
的操作(如substring
、concat
等), 实际上都是返回一个新的String
对象。
在JDK 8中,String
底层数据结构是一个字符数组(char[]
),元素char类型,可以 直接映射了Java中对Unicode标准的支持。
每个char
类型的变量占据2个字节的空间,能够表示从U+0000到U+FFFF的Unicode码点。
char
类型 可以 涵盖 Unicode 的 语言编码, 涵盖了基本多文种平面(BMP),这种设计确保了String
对象能够无缝地处理包括中文、日文、韩文在内的多种语言文本。
例如,当创建一个字符串String str ="技术自由圈";
时,
String str = "技术自由圈";
str = str + " 真的都是顶级高手";
字符串的字符'技'
、'术'
、'自'
、'由'
、'圈'
就存储在这个char[]
数组中。
这个char[]
数组它被private
修饰,是private
(私有)的,外界无法直接访问,保证了字符串数据的封装性。
这个char[]
数组它被final
修饰 ,又是不可变的, 也就是说, 这个char[]
数组 的引用一旦被初始化,就不能再指向其他的数组,但数组中的元素内容可以改变。 而且,String
类没有向外暴露修改char[]
数组 元素内容的方法 。
字符串的缓存机制:字符串常量池(String Constant Pool)
回到 前面的在这个例子中,
String str = "技术自由圈";
str = str + " 真的都是顶级高手";
前面分析了,上面的代码产生了三个 String 对象:
第一个 String 对象: "技术自由圈"
第二个 String 对象: " 真的都是顶级高手"
第三个 String 对象:"技术自由圈 真的都是顶级高手"
看起来, 由于String 里边的 char[]
数组 不是复用的, 如果字符串很多的话, 会导致 内存的浪费。
尤其是, 如何重复的 字符串很多 , 就更加如此。
假设文章中有大量重复的句子片段,如 反复用到 “技术自由圈 ” , 比如下面的代码:
String sentence1 = "技术自由圈";
String sentence2 = "技术自由圈" + ", 真的都是顶级高手";
String sentence3 = "技术自由圈" + ", 是一个 技术发烧友 的 圈子";
String sentence4 = "技术自由圈" + ", 是一个 P7、P8、P9 顶级技术专家 圈子";
String sentence5 = "技术自由圈" + ", 是一个 快速帮助大家 转架构 圈子";
String sentence6 = "技术自由圈" + ", 是一个 快速帮助大家 急速上岸 圈子";
String sentence7 = "技术自由圈" + ", 是一个 快速帮助大家 职业升级 圈子";
// 还有更多类似以 sentence1 为基础构建的字符串
那么,一个问题来了, 上面的代码中的 反复用到 “技术自由圈 ” , 是一个 string对象, 而是多个 string对象 呢?
其实, 上面代码中的 “技术自由圈 ” ,并不是 纯种的 String对象, 而是叫做 字符串字面量 , 有 jdk 的native 代码创建。
什么是 字符串字面量 ?
字符串字面量是在 Java 代码中直接用双引号(""
)包围的字符序列。
例如,"技术自由圈"
、"123"
、", 是一个 P7、P8、P9 顶级技术专家 圈子"
这些,都是字符串字面量。
它是一种在程序中直接表示字符串值的方式,编译器可以直接识别这种表示形式。
字符串字面量的 存储位置与特性
在 Java 中,字符串字面量存储在 字符串常量池中。字符串常量池(String Constant Pool)是Java中用于存储 字符串字面量 的内存区域,它的作用是节省内存和提高性能。
当创建相同的字符串字面量时,它们会引用常量池中的同一个对象,从而降低程序内存的开销。
字符串常量池 又 存储在哪儿呢?
字符串常量池(String Constant Pool) 在JDK 7之前,是存储在永久代(PermGen)中的一块特殊区域。
字符串常量池(String Constant Pool) 在JDK7之后,是存储在堆 中的一块特殊区域。
当程序中出现一个字符串字面量时,JVM 会首先检查字符串常量池。
如果池中已经存在相同内容的字符串,就直接返回该字符串的引用;
如果不存在,就会在池中创建一个新的字符串对象并放入池中,然后返回该对象的引用。
字符串常量池 和 运行时常量池的区别
字符串常量池(String Pool)和运行时常量池(Runtime Constant Pool)是Java中两个不同的概念,它们在Java程序的运行时内存中扮演着不同的角色。下面分别解释这两个概念:
字符串常量池(String Pool)
字符串常量池是Java堆内存中的一个特殊存储区域,用于存储字符串常量。它的主要目的是优化字符串的存储,避免相同字符串的重复创建,从而节省内存空间。字符串常量池中存储的字符串是不可变的,并且可以通过字符串字面量直接访问。
- 字符串字面量:在代码中直接书写的字符串,如
"hello"
,会被自动放入字符串常量池中。 - intern()方法:可以通过调用
String.intern()
方法将非字面量字符串放入字符串常量池中。
从Java 7开始,字符串常量池从永久代(PermGen)移动到了Java堆中。
运行时常量池(Runtime Constant Pool)
运行时常量池是Java虚拟机(JVM)方法区(Method Area)的一部分,它存储了编译期生成的各种字面量和符号引用。运行时常量池是每个类或接口的常量池的运行时表示,它包含了以下内容:
- 字面量:如文本字符串、声明为
final
的常量值等。 - 符号引用:包括类和接口的全限定名、字段名称和类型、方法名称和签名等。
当Java程序运行时,JVM会将这些编译期的常量和符号引用转化为实际的内存地址或直接内存引用。
字符串常量池 和 运行时常量池的区别
- 存储位置:字符串常量池存储在Java堆中,而运行时常量池存储在方法区。
- 存储内容:字符串常量池主要存储字符串类型的常量,运行时常量池存储的是编译期生成的各种字面量和符号引用。
- 作用范围:字符串常量池是全局的,作用于整个Java虚拟机;运行时常量池是局部的,作用于每个类或接口。
- 生命周期:字符串常量池的生命周期与Java虚拟机的生命周期相同;运行时常量池的生命周期与类或接口的生命周期相同。
字符串常量池 和 运行时常量池的联系
Jdk1.6
及之前 是包含关系, 运行时常量池包含字符串常量池- 它们都是为了提高Java程序的性能和效率而设计的。字符串常量池通过避免重复的字符串对象来节省内存,运行时常量池则通过存储编译期的常量和符号引用来加速程序的运行。两者都是Java内存管理的重要组成部分。
字符串常量池 和 运行时常量池的 位置迁移
Jdk1.6
及之前 , 字符串常量池 是运行时常量池中的一小部分,字符串常量池的位置在jdk不同版本下,有一定区别!
Jdk1.6
及之前 有永久代, 运行时常量池包含字符串常量池
Jdk1.7
:有永久代 但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离, 字符串常量池 迁移到堆里了
Jdk1.8及之后 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里
java8内存结构图
字符串常量池与String对象的关系如下:
1.字符串字面量与常量池:
当我们使用双引号直接声明字符串时,如String s = "技术自由圈";
,JVM会在字符串常量池中查找是否存在该字符串对象。
如果存在,则直接返回该对象的引用地址, 是一个 String 类型的指针;
如果不存在,则在字符串常量池中创建该字符串对象,并返回引用地址, 是一个 String 类型的指针;
2. 使用new关键字创建的String对象
如果我们使用new
关键字创建字符串对象,如String s = new String("技术自由圈");
,
JVM会在堆内存中创建一个新的String对象,这个string 对象里边的 char[]
数组 在堆中, 不在字符串常量池(String Constant Pool)。
尼恩在这里强调一下:使用 new 关键字创建的 new String("技术自由圈");
String对象,不是 字符串字面量 , 不在 字符串常量池(String Constant Pool), 而是在 JVM 的堆中。
所以,使用new 创建的String对象,不管内容是否相同,都会指向不同的 堆内存区域, 例如:
String sentence1 = new String("技术自由圈");
String sentence2 = new String("技术自由圈");
上面的 sentence1 和 sentence1 ,都是new 创建,指向不同的地址,在 JVM 的堆中。
而下面的例子,,使用 字符串字面量 ,如果内容是否相同,都会指向统一的内存区域, 例如:
String sentence1 = "技术自由圈" ;
String sentence2 = "技术自由圈" ;
上面的 sentence1 和 sentence1 ,都会指向统一的内存区域,在 字符串常量池(String Constant Pool)
不同的 jdk版本,字符串常量池(String Constant Pool)的位置不同;
- 在 JDK 8 及之前位于方法区
- 而 JDK 9 之后,String Constant Pool 位于堆中
3:字符串常量池(String Constant Pool)的作用?
字符串常量池的主要目的: 是减少内存使用和提高性能,通过重用字符串实例来实现。
尼恩从架构师视角给大家分析的话: 字符串常量池 ,就是 字符串的池化机制。
但是 上面大家看到了: new 创建的 String ,并没有 用到 字符串常量池。
如果 通过 字符串常量池,进行 new 创建的 String 的复用呢?
4. String.intern()方法建立 二者关联 :
String.intern()
方法允许我们将通过new
关键字创建的String对象,放入字符串常量池中。
当intern()
方法被调用时,它会在字符串常量池中查找是否存在该字符串,
- 如果不存在,如果字符串常量池中不存在,就在常量池中创建一个指向该对象堆中实例的引用,并返回这个引用地址。
- 如果存在,也就是字符串常量池中已经存在这个字符串对象了,就返回常量池中该字符串对象的地址;
通过String.intern()
方法, 进行 new 创建的 String 的 池化和复用。
方法intern()
的作用就是将String池化,这个池是字符串常量池。当然。不同版本的JDK有不同的实现。
java 8 中,String.intern()
方法是一个 native 方法,其 Java 层面的声明如下:
public native String intern();
这意味着 intern()
方法的具体实现是在 JVM 层面,而不是在 Java 代码中。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// ....
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
}
翻译过来就是,当intern()方法被调用的时候,如果字符串常量池中已经存在这个字符串对象了,就返回常量池中该字符串对象的地址;如果字符串常量池中不存在,就在常量池中创建一个指向该对象堆中实例的引用,并返回这个引用地址。
这个方法调用了 JVM 内部的 JVM_InternString
方法,该方法负责实际的字符串池管理和字符串的 intern
操作。尼恩在这里, 不去抠 底层的 c++ 源码了。
在java代码中,当使用字符串字面量(如"技术自由圈"
)创建字符串时,JVM 会首先检查字符串常量池。
- 如果池中已经存在相同内容的字符串,就直接返回该字符串的引用;
- 如果不存在,就会在池中创建一个新的字符串对象并放入池中,然后返回该对象的引用。
例如:
String s1 ="技术自由圈";
String s2 = "技术自由圈";
System.out.println(s1 == s2);
这里输出结果为true
,因为s1
和s2
都指向字符串常量池中的同一个"技术自由圈"字符串对象。
String str1 ="技术自由圈";
System.out.println(str1.intern() == str1);
这里输出结果为true
,与上个例子对比,将常量池的地址赋值给了str1变量,所以相等。
5.使用new 创建的String对象 和 字符串字面量 的区别
使用 字符串字面量创建的String对象会指向字符串常量池中的相同对象,而使用
new
创建的String对象会指向堆内存中的不同对象。使用
==
比较两个字符串字面量引用时会返回true
,而比较一个字符串字面量和一个通过new
创建的String对象时会返回false
。而String对象可以存储在堆内存中,也可以通过
intern()
方法被放入字符串常量池中,这取决于它们的创建方式。
eg: 比较一个字符串字面量和一个通过new
创建的String对象时会返回false
。
String s3 = new String("技术自由圈");
String s4 = "技术自由圈";
System.out.println(s3 == s4);
这里输出为false
,因为s3
是通过new
关键字在堆中创建的新对象,而s4
是直接从字符串常量池中获取的对象引用,它们指向不同的内存区域。
而String对象可以存储在堆内存中,也可以通过intern()
方法被放入字符串常量池中,可以举个例子
以下是一个关于String
对象通过intern()
方法被放入字符串常量池的例子:
public class StringInternExample {
public static void main(String[] args) {
// 创建一个String对象(不在字符串常量池中)
String str1 = new String("Java");
// 使用intern()方法将str1放入字符串常量池,并获取常量池中的引用
String str2 = str1.intern();
// 直接通过字符串字面量创建一个字符串,该字符串会在常量池中创建(如果不存在的话)
String str3 = "Java";
// 比较str2和str3,它们都指向字符串常量池中的同一个对象
System.out.println(str2 == str3); // 输出 true
// 比较str1和str2,str1是在堆中创建的对象,str2是常量池中的对象,所以不相等
System.out.println(str1 == str2); // 输出 false
}
}
在上述代码中:
- 首先通过
new String("Java")
创建了str1
,此时在堆内存中创建了一个String
对象,其内容为"Java"
,但这个对象最初不在字符串常量池中。 - 然后调用
str1.intern()
方法,该方法会检查字符串常量池,如果常量池中不存在"Java"
这个字符串,就会把str1
所代表的字符串放入常量池,并返回常量池中的引用,这里将返回的引用赋值给str2
。 - 接着通过
"Java"
这个字符串字面量创建了str3
,由于之前str1.intern()
已经把"Java"
放入了常量池,所以str3
直接获取到了常量池中的那个"Java"
对象的引用。 - 最后进行比较,
str2 == str3
为true
,因为它们都指向字符串常量池中的同一个对象;而str2 == str1
为false
,因为str1
是在堆中创建的对象,str2
是从常量池中获取的引用,它们指向不同的内存区域。
总结来说,字符串常量池是Java中用于优化字符串存储和管理的一种机制,它通过重用相同的字符串字面量来减少内存消耗,并提高程序性能。
字符串常量池与String对象的关系 总结
- 当一个字符串通过
new
关键字创建时,它会在堆内存中占用空间。由于String
类是不可变的,每次使用new
创建字符串时,都会在堆内存中分配新的空间。 - 字符串字面量 也是 String类型,不过是通过native 方法创建。 换一个说法也是可以的,字符串字面量总是隐式调用
intern()
方法 创建的String 对象。 为了节省内存和提高性能,Java 提供了字符串常量池(String Constant Pool),用于缓存和复用 字符串字面量。 - 可以显式调用
String.intern()
在将字符串放入字符串常量池(String Constant Pool),放入之前,JVM 会检查池中是否已经存在该字符串。如果存在,则返回池中字符串的引用;如果不存在,则在池中创建一个新的字符串,并返回新创建字符串的引用。
如何 修改 一个字符串?
String
是不可变的(immutable)。一旦一个String
对象被创建,它的值就不能被改变。
例如,当执行拼接操作时,如
String str = "技术自由圈";
str = str + " 一个 骨灰级 技术 顶级高手的圈子"
实际上是创建了一个新的String
对象,而原来的"技术自由圈"
字符串对象并没有被修改。
这是因为String
内部存储字符的char[]
数组被声明为private final
,并且没有提供修改这个数组内容的公共方法。
String
中的对象是不可变的,也就可以理解为常量,线程安全。
问题是: 如果在需要修改的场景,需要 深入 修改内部的 char[]
数组 里边的内容, 怎么办呢?
可以使用 另外两个 类型: StringBuilder
与 StringBuffer
。
StringBuffer:是可变的(mutable)字符串 缓冲。可以在原有对象上直接进行修改,如追加、插入、删除、替换等操作。例如,
StringBuffer sb = new StringBuffer("技术自由圈"); sb.append(" 一个 骨灰级 技术 顶级高手的圈子");
这里是在
sb
这个StringBuffer
对象的基础上直接添加了新的内容,char[] 数据的内部发生了改变,而不是创建新的对象。StringBuilder:是可变的(mutable)字符串 缓冲,但不是线程安全的。在单线程 场景中,它的性能比
StringBuffer
更好,因为它没有同步机制的额外开销。例如,在单线程的字符串拼接、修改等操作中,StringBuilder
可以更快地完成任务。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组 char[] value 保存字符串的内容,不过, char[] value成员 没有使用 final
和 private
关键字修饰,
AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,最关键的是,这个 类还提供了很多修改字符串的方法,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。
比如 AbstractStringBuilder
的append
方法,大致如下:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
以下是一个展示StringBuffer
基本用法的示例代码:
public class StringBufferDemo {
public static void main(String[] args) {
// 创建一个StringBuffer对象,初始值可以为空字符串
StringBuffer stringBuffer = new StringBuffer();
// 追加字符串
stringBuffer.append("Hello");
System.out.println("追加 'Hello' 后:" + stringBuffer);
// 继续追加字符串
stringBuffer.append(" World");
System.out.println("再追加 ' World' 后:" + stringBuffer);
// 在指定位置插入字符串
stringBuffer.insert(5, " Beautiful");
System.out.println("在索引5处插入 ' Beautiful' 后:" + stringBuffer);
// 删除指定范围内的字符
stringBuffer.delete(5, 14);
System.out.println("删除索引5到13的字符后:" + stringBuffer);
// 替换指定范围内的字符
stringBuffer.replace(0, 5, "Hi");
System.out.println("替换索引0到4的字符后:" + stringBuffer);
// 获取字符串长度
int length = stringBuffer.length();
System.out.println("当前StringBuffer的长度为:" + length);
// 反转字符串
stringBuffer.reverse();
System.out.println("反转后的字符串:" + stringBuffer);
// 将StringBuffer转换为String
String finalString = stringBuffer.toString();
System.out.println("转换为String后的结果:" + finalString);
}
}
在上述代码中:
- 首先创建了一个
StringBuffer
对象,初始时可以为空字符串。 - 然后通过
append
方法不断追加字符串内容,展示了如何逐步构建一个较长的字符串。 - 接着使用
insert
方法在指定位置插入新的字符串,delete
方法删除指定范围内的字符,replace
方法替换指定范围内的字符,reverse
方法反转整个字符串。 - 最后通过
length
方法获取当前字符串的长度,并使用toString
方法将StringBuffer
对象转换为普通的String
对象,以便在需要时进行其他操作(比如输出等)。
总之,StringBuffer
是可变的字符序列,与 String 相比,它在需要频繁修改字符串内容的场景下能更高效地利用内存,因为它不需要像String
那样每次修改都创建新的对象。
而 StringBuilder
的用法和 StringBuffer
是差不的, 不同的是一个线程安全,一个不安全:
StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String对象的 不可变的(immutable)几个表面 原因:
回到咱们的面试题,String对象 为啥是不可变的?
来看一下,大家 表面的答案, 无非是下面的5个小点:
- 传参的 安全性:不可变性使得
String
对象可以安全传参,因为它们不会被改变。这对于防止数据被意外或恶意修改非常重要。 - 线程的安全:由于
String
对象不可变,它们自然就是线程安全的。这意味着在多线程环境中,不需要额外的同步机制就可以安全地使用String
对象。 缓存优化:不可变性,允许
String
对象被缓存。例如,Java中的字符串字面量会被缓存,这可以减少内存使用,并提高性能。简化编程模型:不可变性简化了编程模型,因为开发者不需要担心对象状态的变化,这可以减少错误和提高代码的可读性。
- 对象重用:由于字符串是不可变的,它们可以被重用而不需要创建新的实例,这有助于减少垃圾收集的压力。
总的来说,String
的不可变性是Java语言设计中的一个核心特性,它提供了许多好处,包括安全性、性能优化和简化的编程模型。
表面原因之一:不可变 保障了 传参的 安全性
当String
作为参数传递给方法时,由于它是不可变的,方法内部不能修改它的值,这就保证了调用者传入的String
对象的安全性。
例如:
public class StringSafety {
public static void main(String[] args) {
String str = "Secret";
printString(str);
System.out.println(str);
}
public static void printString(String s) {
// 在这里,不能修改s的值
System.out.println(s);
}
}
在这个例子中,printString
方法无法修改str
的值,因为String
是不可变的,这就防止了意外修改数据的情况。
表面原因之二:不可变 保障了 线程安全
在多线程环境下,不可变对象是天然的线程安全对象。多个线程可以同时访问String
对象而不需要额外的同步机制。例如:
class StringThreadSafety {
public static void main(String[] args) {
final String str = "Shared String";
Thread thread1 = new Thread(() -> {
System.out.println(str);
});
Thread thread2 = new Thread(() -> {
System.out.println(str);
});
thread1.start();
thread2.start();
}
}
在这个例子中,str
是一个String
对象,多个线程可以安全地访问它,因为它不会被修改,不会出现数据不一致的情况。
表面原因之三:不可变 保障了 缓存优化
因为String
是不可变的,所以可以在内存中缓存String
对象。
例如,在 Java 的字符串常量池机制中,对于相同的字符串字面量,只会在常量池中创建一个String
对象。
像String s1 = "Hello";
和String s2 = "Hello";
这两个引用实际上可能指向同一个String
对象,这可以节省内存空间并且提高性能。
还有两个表面原因:
表面原因之4:简化编程模型:不可变性简化了编程模型,因为开发者不需要担心对象状态的变化,这可以减少错误和提高代码的可读性。
表面原因之5:方便对象重用 :由于字符串是不可变的,它们可以被重用而不需要创建新的实例,这有助于减少垃圾收集的压力。
尼恩就不做 举例了。
高端面试:必须来点 非常见的、 高大上的答案
尼恩这里想说的是, 要拿到 高薪offer, 要进大厂,光是前面 5个原因不够,
必须来点 非常见的、 高大上的答案, 整点技术狠活儿。
String对象的 不可变的(immutable)非常见 原因 、 更加深层次的原因。
这个原因, 90% 的人 不知道 , 或者说不上来。
String对象的 不可变的(immutable)深层次 原因:
Java 中的 String 类型, 既可以在 堆中存储, 又可以 在 字符串常量池(String Constant Pool) 中存储 。
如果 String对象的 可变的( mutable), 那么 字符串字面量(immutable) 就不能 赋值给 String 引用了。
如果 字符串字面量(immutable) 不能使用 String 引用 , 那么 字符串字面量(immutable) 应该是什么类型呢?
既然 字符串字面量(immutable) 是 String 类型, 注定了 String 类型是不可变的。
所以, 从 语义规则上来说, String类型, 只能是 不可变的(immutable)。
45岁老架构师尼恩认为 :
字符串字面量(immutable) 是 String 类型 , 才是 String对象的 不可变的(immutable)深层次 原因,
前面的 线程安全 等5个原因, 仅仅是 String对象的 不可变的(immutable)浅层次的、表面 原因。
前面小伙伴, 如果能讲清楚这个 关系, 美团面试官一定口水直流, 美团offer 就到手啦。
可惜了! 他之前没有看到尼恩的文章。
尼恩架构团队的塔尖 sql 面试题
- sql查询语句的执行流程:
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
- 索引
阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?
滴滴面试:单表可以存200亿数据吗?单表真的只能存2000W,为什么?
- 索引下推 ?
- 索引失效
美团面试:mysql 索引失效?怎么解决?(重点知识,建议收藏,读10遍+)
- MVCC
- binlog、redolog、undo log
美团面试:binlog、redolog、undo log底层原理是啥?分别实现ACID哪个特性?(尼恩图解,史上最全)
- mysql 事务
京东面试:RR隔离mysql如何实现?什么情况RR不能解决幻读?
- 分布式事务
分布式事务圣经:从入门到精通,架构师尼恩最新、最全详解 (50+图文4万字全面总结 )
- mysql 调优
说在最后:有问题找45岁老架构取经
只要按照上面的 尼恩团队梳理的 方案去作答, 你的答案不是 100分,而是 120分。 面试官一定是 心满意足, 五体投地。
按照尼恩的梳理,进行 深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会, 可以找尼恩来改简历、做帮扶。前段时间,空窗2年 成为 架构师, 32岁小伙逆天改命, 同学都惊呆了。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
尼恩技术圣经系列PDF
- 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
- 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
- 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
- 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
- 《大数据HBase学习圣经:一本书实现HBase学习自由》
- 《大数据Flink学习圣经:一本书实现大数据Flink自由》
- 《响应式圣经:10W字,实现Spring响应式编程自由》
- 《Go学习圣经:Go语言实现高并发CRUD业务开发》
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓