美团面试:String 为什么 不可变 ?(90%答错了,尼恩来一个绝世答案)

简介: 45岁老架构师尼恩分享Java面试心得,涵盖String不可变性、字符串常量池、面试技巧等内容。尼恩强调,掌握深层技术原理,如String不可变性的真正原因,可在面试中脱颖而出,赢得高薪Offer。此外,尼恩还提供了大量技术资源和面试指导,帮助求职者提升技术水平,顺利通过大厂面试。

本文原文链接

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[];

image.png

JDK 8 及之前的 immutable 不可变 原理

  • 第一,字符数组使用 final修饰:在String类的内部,字符串的值是通过一个char数组来存储的。这个char数组被声明为private final,如在 Java 的String类源码中可以看到private final char value[];

    final关键字保证了这个数组的引用不能被重新赋值。一旦String对象被创建,这个char数组的引用就固定了,不能指向其他的字符数组。

  • 第二,没有提供修改方法String类没有提供可以修改这个字符数组内容的公共方法。很多的方法,看起来像是修改String的操作(如substringconcat等), 实际上都是返回一个新的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及之前 有永久代, 运行时常量池包含字符串常量池

image.png

Jdk1.7:有永久代 但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离, 字符串常量池 迁移到堆里了

image.png

Jdk1.8及之后 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里

image.png

java8内存结构图

image.png

字符串常量池与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&trade; 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,因为s1s2都指向字符串常量池中的同一个"技术自由圈"字符串对象。

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 == str3true,因为它们都指向字符串常量池中的同一个对象;而str2 == str1false,因为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[]数组 里边的内容, 怎么办呢?

可以使用 另外两个 类型: StringBuilderStringBuffer

  • StringBuffer:是可变的(mutable)字符串 缓冲。可以在原有对象上直接进行修改,如追加、插入、删除、替换等操作。例如,

    StringBuffer sb = new StringBuffer("技术自由圈"); 
    sb.append(" 一个  骨灰级 技术 顶级高手的圈子");
    

    这里是在sb这个StringBuffer对象的基础上直接添加了新的内容,char[] 数据的内部发生了改变,而不是创建新的对象。

  • StringBuilder:是可变的(mutable)字符串 缓冲,但不是线程安全的。在单线程 场景中,它的性能比StringBuffer更好,因为它没有同步机制的额外开销。例如,在单线程的字符串拼接、修改等操作中,StringBuilder可以更快地完成任务。

StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组 char[] value 保存字符串的内容,不过, char[] value成员 没有使用 finalprivate 关键字修饰,

AbstractStringBuilderStringBuilderStringBuffer 的公共父类,最关键的是,这个 类还提供了很多修改字符串的方法,定义了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。

比如 AbstractStringBuilderappend 方法,大致如下:

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 并没有对方法进行加同步锁,所以是非线程安全的。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

String对象的 不可变的(immutable)几个表面 原因:

回到咱们的面试题,String对象 为啥是不可变的?

来看一下,大家 表面的答案, 无非是下面的5个小点:

  1. 传参的 安全性:不可变性使得 String 对象可以安全传参,因为它们不会被改变。这对于防止数据被意外或恶意修改非常重要。
  2. 线程的安全:由于 String 对象不可变,它们自然就是线程安全的。这意味着在多线程环境中,不需要额外的同步机制就可以安全地使用 String 对象。
  3. 缓存优化:不可变性,允许 String 对象被缓存。例如,Java中的字符串字面量会被缓存,这可以减少内存使用,并提高性能。

  4. 简化编程模型:不可变性简化了编程模型,因为开发者不需要担心对象状态的变化,这可以减少错误和提高代码的可读性。

  5. 对象重用:由于字符串是不可变的,它们可以被重用而不需要创建新的实例,这有助于减少垃圾收集的压力。

总的来说,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一条SQL语句的执行过程?

美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?

  • 索引

阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?

滴滴面试:单表可以存200亿数据吗?单表真的只能存2000W,为什么?

  • 索引下推 ?

贝壳面试:什么是回表?什么是 索引下推 ?

  • 索引失效

美团面试:mysql 索引失效?怎么解决?(重点知识,建议收藏,读10遍+)

  • MVCC

MVCC学习圣经:一文穿透MySQL MVCC,吊打面试官

  • binlog、redolog、undo log

美团面试:binlog、redolog、undo log底层原理是啥?分别实现ACID哪个特性?(尼恩图解,史上最全)

  • mysql 事务

阿里面试:事务ACID,底层是如何实现的?

京东面试:RR隔离mysql如何实现?什么情况RR不能解决幻读?

  • 分布式事务

分布式事务圣经:从入门到精通,架构师尼恩最新、最全详解 (50+图文4万字全面总结 )

阿里面试:秒杀的分布式事务, 是如何设计的?

  • mysql 调优

如何做mysql调优?绝命7招,让慢SQL调优100倍

美团面试:Mysql如何选择最优 执行计划,为什么?

说在最后:有问题找45岁老架构取经‍

只要按照上面的 尼恩团队梳理的 方案去作答, 你的答案不是 100分,而是 120分。 面试官一定是 心满意足, 五体投地。

按照尼恩的梳理,进行 深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。

很多小伙伴刷完后, 吊打面试官, 大厂横着走。

在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会, 可以找尼恩来改简历、做帮扶。前段时间,空窗2年 成为 架构师, 32岁小伙逆天改命, 同学都惊呆了

狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。

尼恩技术圣经系列PDF

……完整版尼恩技术圣经PDF集群,请找尼恩领取

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

相关文章
|
12天前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
15天前
|
存储 人工智能 调度
阿里云吴结生:高性能计算持续创新,响应数据+AI时代的多元化负载需求
在数字化转型的大潮中,每家公司都在积极探索如何利用数据驱动业务增长,而AI技术的快速发展更是加速了这一进程。
|
7天前
|
并行计算 前端开发 物联网
全网首发!真·从0到1!万字长文带你入门Qwen2.5-Coder——介绍、体验、本地部署及简单微调
2024年11月12日,阿里云通义大模型团队正式开源通义千问代码模型全系列,包括6款Qwen2.5-Coder模型,每个规模包含Base和Instruct两个版本。其中32B尺寸的旗舰代码模型在多项基准评测中取得开源最佳成绩,成为全球最强开源代码模型,多项关键能力超越GPT-4o。Qwen2.5-Coder具备强大、多样和实用等优点,通过持续训练,结合源代码、文本代码混合数据及合成数据,显著提升了代码生成、推理和修复等核心任务的性能。此外,该模型还支持多种编程语言,并在人类偏好对齐方面表现出色。本文为周周的奇妙编程原创,阿里云社区首发,未经同意不得转载。
|
12天前
|
人工智能 运维 双11
2024阿里云双十一云资源购买指南(纯客观,无广)
2024年双十一,阿里云推出多项重磅优惠,特别针对新迁入云的企业和初创公司提供丰厚补贴。其中,36元一年的轻量应用服务器、1.95元/小时的16核60GB A10卡以及1元购域名等产品尤为值得关注。这些产品不仅价格亲民,还提供了丰富的功能和服务,非常适合个人开发者、学生及中小企业快速上手和部署应用。
|
2天前
|
云安全 存储 弹性计算
|
4天前
|
云安全 人工智能 自然语言处理
|
7天前
|
人工智能 自然语言处理 前端开发
用通义灵码,从 0 开始打造一个完整APP,无需编程经验就可以完成
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。本教程完全免费,而且为大家准备了 100 个降噪蓝牙耳机,送给前 100 个完成的粉丝。获奖的方式非常简单,只要你跟着教程完成第一课的内容就能获得。
|
23天前
|
自然语言处理 数据可视化 前端开发
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
合合信息的智能文档处理“百宝箱”涵盖文档解析、向量化模型、测评工具等,解决了复杂文档解析、大模型问答幻觉、文档解析效果评估、知识库搭建、多语言文档翻译等问题。通过可视化解析工具 TextIn ParseX、向量化模型 acge-embedding 和文档解析测评工具 markdown_tester,百宝箱提升了文档处理的效率和精确度,适用于多种文档格式和语言环境,助力企业实现高效的信息管理和业务支持。
3968 5
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
|
12天前
|
算法 安全 网络安全
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
2024阿里云11.11金秋云创季活动火热进行中,活动月期间(2024年11月01日至11月30日)通过折扣、叠加优惠券等多种方式,阿里云WoSign SSL证书实现优惠价格新低,DV SSL证书220元/年起,助力中小企业轻松实现HTTPS加密,保障数据传输安全。
535 3
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
|
11天前
|
数据采集 人工智能 API
Qwen2.5-Coder深夜开源炸场,Prompt编程的时代来了!
通义千问团队开源「强大」、「多样」、「实用」的 Qwen2.5-Coder 全系列,致力于持续推动 Open Code LLMs 的发展。