面试系列之String原理详细讲解

介绍String之前,先来介绍常量池(本文基于JDK1.8)

主要有三种常量池

1、Class文件常量池

在class文件中保存了一份常量池(Constant Pool),主要存储编译时确定的数据,包括代码中的字面量和符号引用。

2、运行时常量池

位于方法区中,全局共享,class文件常量池中的内容会在类加载后存放到方法区的运行时常量池中,除此之外在运行期间可以将新的变量放入运行时常量池中,具备动态性。

3、字符串常量池(String Pool)

位于堆中,全局共享,JVM为了提升性能和减少内存开销。避免字符串的重复创建,维护了一块特殊的内存空间,由String类独自维护。在String Pool池中,字符串内容不重复。JDK1.7之前是存放方法区,用来存储编译器生成的字符串引用。在JDK1.7之后,常量池存放在堆中。这个String Pool是一个固定大小的Hash Table。默认值大小是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表很长,就会导致调用String.intern()性能下降,在JDK6中是固定的,在JDK7可以通过参数- XX:String TableSize=999991指定大小。

三者之间的关系如图所示:

详细说下字符串常量池的结构,在Hotspot JVM中,字符串常量池StringTable的本质是一张HashTable,采用的是数组加链表的结构,链表中的节点是一个个的HashTableEntry,而Hash TableEntry中的value则存储了堆上String对象的引用。

如图:

以String s = “123”的过程为例,我们都知道是将这个字符串放到字符串常量池中了,实际放进去的是字符串的对象本身还是对象的引用呢??

首先在堆上创建String对象,value执向char[]数组对象“123”,然后创建HashTableEntry,使它的value指向堆上的String实例,并把HashTableEntry放入字符串常量池,而不是直接把String对象放入字符串常量池中。意思就是常量池中存放的是String对象的引用。

一、String的几个特性:

我们先看下String源码:

1、String类被final修饰,不能被继承。

2、value值被final和private修饰,我们知道final修饰的成员变量,引用地址不能改变,且因为被private修饰符修饰,外部也无法改变value值。

3、通过 String str = "abc"字面量赋值的方式是存储于字符串常量池中的,一个字符串被创建,就不能被修改了, replace、subString等方式是创建了一个新的字符串,并非修改原来的字符串。

4、JDK1.8及之前value值用char[]数组存储,JDK1.9之后用byte[]数组存储。原因:1.8及之前,用两个字节表示一个字符。实际运用中,大多数字符只使用了拉丁字符,只需要一个字节就能存放,所以改变了内部存储结构,从UTF-16到byte[]数组,并加上一个encoding- flag域,用于判断字符串该用哪种方式存储。

5、字符串常量池不会存储相同内容的字符串,String Pool是一个固定大小的Hashtable,JDK6默认长度是1009,JDK7以后默认60013,如果放进池的String非常多,就会冲突严重而使链表变长,性能大幅下降。长度是固定的,可以提前用 —XX:String TableSize设置大小,在JDK6可以任意设置,JDK8以后最小设置为1009。

6、常量+常量(“hello” + “world”)的拼接结果在常量池中,原理是编译期优化(在生成字节码文件的时候就完成了拼接)。只要其中有一个变量,结果就在堆中,原因是采用的String Builder对象的append进行拼接的,如:String s = “hello” s = s + “world” ;有个特殊的情况,如果变量被声明为final,即此变量中途不会再改变其引用对象,那么就会被视作为常量,不会使用StringBuilder进行拼接。

7、如果一个字符串在堆中,在常量池中不存在,可以调用intern()方法,主动将该字符串放入常量池中,并返回该字符串在堆中的引用地址。

二、创建几个对象

1、我们来看看字面量声明String的方式:

public class StringTest {
  public static void main(String[] args) {

    String str = "hello";
    System.out.println(str); //true
    
  }
}

这种方式创建,只在堆中创建一个String对象,一个char数组,在字符串常量池中存储了它的引用。

2、构造方法创建的字符串方式

String str = new String("xxxx");

通过这种方式创建了两个String对象,一个char数组,都是在堆上创建的,两个String中的value都指向了同一个char数组对象,且通过字面量的方式创建的String对象的引用被存储到了字符串常量池中,可通过IDEA中的debug模式的Membory中看到String对象的创建个数。

三、String的几种创建方式的区别

String在我们平时开发过程中,使用的是最多的,创建String也有几种不同的方式:

1、通过字面量直接赋值

String s1 = "HelloWorld";

2、通过new关键字调用构造方法创建对象

String s2 = new String("HelloWorld");

3、通过 “+ ”运算赋创建对象

String s3 = "Hello" + "World";

String ss = "Hello";

String s4 = ss + "World";

那么这几种方式有什么区别呢?让我们看下代码

public class StrTest {

  public static void main(String[] args) throws InterruptedException {

    //1、通过字面量直接赋值
    String s1 = "HelloWorld";
    //2、通过new关键字调用构造方法创建对象
    String s2 = new String("HelloWorld");
    //3、通过 “+ ”运算赋创建对象
    String s3 =  "Hello" + "World";
    String ss =  "Hello";
    String s4 =  ss + "World";
   
    System.out.println(s1 == s2); //false
    System.out.println(s1 == s3); //true
    System.out.println(s1 == s4); //false
    System.out.println(s3 == s4); //false
  }
}

解释下执行结果:

代码1中 String s1 = "HelloWorld"; 首先在常量池中进行查找是否存在 "HelloWorld",如果不存在,则在常量池中创建 "HelloWorld"对象,并把常量池的引用地址赋值给s1。

代码2中 String s2 = new String("HelloWorld"); 创建的字符串内容"HelloWorld"在常量池中已经存在了,所以不会进行创建。而通过构造方法则会在堆中创建一个新对象,变量s2引用的是这个新对象的地址,所以s1==s2为false;

代码3中 s3 中是两个常量用“+”连接,编译器在编译时可以确定是常量,编译器会进行优化,会直接拼接成一个常量,然后在常量池中进行查找"HelloWorld",查找到了就把字符串的引用地址赋值给s3,因此s1==s3为true

而s4中因为ss是以变量的形式参与+连接的,在编译期不能确定是常量,JVM在字符串遇到“+”的时候,会创建StringBuilder对象进行append操作,然后再调用to String()方法创建一个新的String对象,把这个新的对象赋值给s4,所以s1 == s4 为false,s3== s4 为false,过程就是:new StringBuilder().append(ss).append("World").toString();

四、字符串 “ + ”操作的底层原理

先看段代码:

//代码1  
String sa = "ab";                                          
String sb = "cd";                                       
String sab = sa+sb;                                      
String s = "abcd";  
System.out.println(sab==s); // false  
//代码2  
String sc="ab"+"cd";  
String sd="abcd";  
System.out.println(sc==sd); //true 

代码1中“ab” 和“cd”都是字符串常量,存在堆中的字符串常量池中,同时把“ab”和“cd”所对应的字符串常量池的引用地址分别赋值给sa和sb,当执行sa+sb时,在编译时不能确定是常量,所以JVM首先会在堆中创建一个String Builder类,同时用“ab”完成对象的初始化,接着调用append("cd")执行合并操作,最后调用toString()方法在堆中创建一个新的String对象,将新生成的对象的引用地址赋值给sab。局部变量s存储的是“abcd”常量在常量池中中对应的引用地址,所以sab的引用地址不等于s的引用地址,== 比较的是引用地址。

代码2中,“ab” + “cd” 会在编译完成后就合并成常量“abcd”,原因是在编译期间,应用了编译器优化中一种被称为常量折叠的技术,因此相同字面值常量对应的是同一个引用地址,所以 sc == sd。

编译期优化:

常量折叠会将编译期常量的加减乘除的运算过程在编译过程中折叠。编译器通过语法分析,会将常量表达式计算求值,并用求出的值来替换表达式,而不必等到运行期间再进行运算处理,从而在运行期间节省处理器资源。

编译期常量有哪些:

  • 被声明为final
  • 基本类型或者字符串类型
  • 声明时就已经初始化
  • 使用常量表达式进行初始化

五、创建几个字符串对象??

String s1 = new String("xyz");
 String s2 = new String("xyz"); 
System.out.println(s3==s4);// false

对于String s1 = new String("xyz");由上一个问题可知,创建了2个String对象和一个char[]数组,此时字符串 常量池中已经有“xyz”的地址引用了。当执行String s2 = new String("xyz")时,会调用构造方法重新创建一个String对象。

所以在堆中,存在三个String对象和一个char[]数组,三个String对象的value都指向这个char[]数组。

六、String.intern()方法

String类的intern()方法:当调用 intern方法时,如果字符串常量池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串对象的引用。

否则,将此String对象添加到字符串常量池中,并返回此String对象的引用。

先来看段代码:

public static void main(String[] args) {
    String s1 = new String("Hydra");
    String s2 = s1.intern();
    System.out.println(s1 == s2);
    System.out.println(s1 == "Hydra");
    System.out.println(s2 == "Hydra");
}

执行结果:

false

false

true

用图来描述它们的关系:

  • 在创建s1的时候,其实堆里已经创建了两个字符串对象StringObject1和StringObject2,并且在字符串常量池中驻留了StringObject2
  • 当执行s1.intern()方法时,字符串常量池中已经存在内容等于"Hydra"的字符串StringObject2,直接返回这个引用并赋值给s2
  • s1和s2指向的是两个不同的String对象,因此返回 fasle
  • s2指向的就是驻留在字符串常量池的StringObject2,因此s2=="Hydra"为 true,而s1指向的不是常量池中的对象引用所以返回false

再来看另外一段代码:

public static void main(String[] args) {
    String s1 = new String("Hy") + new String("dra");
    s1.intern();
    String s2 = "Hydra";
    System.out.println(s1 == s2);
}

执行结果:

true

简单分析一下这个过程

第一步会在堆上创建"Hy"和"dra"的字符串对象,并驻留到字符串常量池中。

接下来,完成字符串的拼接操作,前面我们说过,实际上jvm会把拼接优化成StringBuilder的append方法,并最终调用toString方法返回一个String对象。在完成字符串的拼接后,字符串常量池中并没有驻留一个内容等于"Hydra"的字符串。

所以,执行s1.intern()时,会在字符串常量池创建一个引用,指向前面StringBuilder创建的那个字符串,也就是变量s1所指向的字符串对象。

最后,当执行String s2 = "Hydra"时,发现字符串常量池中已经驻留这个字符串,直接返回对象的引用,因此s1和s2指向的是相同的对象。

六、String类为什么要设计成不可变的 ??

我们都知道String类是final修饰的,且String底层是char[]value,value被final修饰。

1、保证线程安全,在并发环境中多个线程同时读写资源时,会引起线程不安全,由于String是不可变的,不会引起线程问题。

2、HashCode,当String被创建的时候,Hash Code的值也会被缓存,Hash Code的值与value有关,如果String改变,那么Hash Code也会改变。针对Map、Set等容器的键值需要保证唯一性和一致性,String不可变保证了这个特性。

3、安全问题,一些参数都是通过字符串的形式进行传递的,如果可以改变的话,可能会被遭到攻击,搞成安全漏洞。

本文部分观点参考引用于 5道面试题,拿捏String底层原理! - 知乎,因为对String常量池存储内容的争论各式各样,因为官方没有给出明确的解释,我只是赞同本博文的观点。另外本文基于JDK1.8,不同版本争论较大。本文只是学习整理,以备以后复习之用。不喜勿喷。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
String底层原理是指String类在内存中的实现方式。根据引用的观点,String类是final修饰的,底层使用char[]数组来存储字符串内容,并且该char[]数组是被final修饰的,即不可变。这意味着一旦字符串被创建,它的内容就不能修改。 当我们使用字面量声明String时,如引用中的示例代码所示,编译器会将字符串直接存储在常量池中,而不是在堆内存中创建新的对象。这样做的好处是可以节省内存空间,并且多个字符串常量可以共享同一份内存。 需要注意的是,由于String是不可变的,如果我们对一个字符串进行修改操作,实际上是创建了一个新的String对象。这也是String不适合频繁修改字符串的原因之一,因为每次修改都会创建新的对象,导致内存开销较大。 总结来说,String底层原理是通过char[]数组来存储字符串内容,并且字符串常量会被存储在常量池中以提高内存利用率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [面试系列String原理详细讲解](https://blog.csdn.net/lgy_2021/article/details/124787916)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值