【Java原理探索】带你攻克String类创建的难点分析 | Java开发实战

简介: 【Java原理探索】带你攻克String类创建的难点分析 | Java开发实战

字符串常量池引入


String是一个引用类型,这意味着String类型的实例化与其它对象一样,相较于基本数据类型,时间和空间的消耗都是较大的,但是由于String的使用频率非常高,JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化,引入了字符串常量池。



字符串创建过程


  • 每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用


  • 如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串


  • .class文件中的常量池将包含String字面量,在jvm进行类装载过程中,class文件中的常量池将被载入内存,此时便形成了所谓的字符串常量池。




整体来说String对象的初始化分为两种


初始化方式将会影响对象内存分配的方式,


字面量初始化的形式创建字符串


public class ImmutableStrings{
    public static void main(String[] args)
    {
        String one = "someString"; // 1
        String two = "someString"; // 2
        System.out.println(one.equals(two));  // String 对象是否相同内容
        System.out.println(one == two);  // String 对象是否相同的引用
    }
}
// Output
true
true
复制代码

执行完上面的第一句代码之后,会在堆上创建一个String 对象,并把String 对象的引用存放到字符串常量池中,并把引用返回给 one,那当第二句代码执行时,字符串常量池已经有对应内容的引用了,直接返回对象引用给 two。one.equals(two) / one == two 都为true。 图形化如下所示:

image.png



字符串拼接的初始化场景


String s = "a"+"a";
String s2 = "aa";
System.out.println(s == s2); // true
复制代码

应该思考为什么会输出true,通过反编译可知jvm直接将上面的"a"+"a"在编译阶段直接变成了"aa"。

image.png


String s = "a";
String s1 = s + "a";
String s2 = "aa";
System.out.println(s1 == s2); // false
复制代码

上面这一段输出false,同样通过反编译


image.png


可以看出汇编指令明显比上面的长了许多,然后我们逐个分析s1的产生过程


  1. 首先jvm会先生成一个StringBuilder对象

image.png

2.然后会添加s和"a",这里我们可以看出第一次添加的时候需要通过ldc出栈解析了字符串s的值,然后添加到StringBuilder对象中。image.png

3.最后调用StringBuilder对象的toString方法返回一个新的字符串对象。image.png

4.StringBuilder的toString方法如下,所以上面s1==s2为false。image.png




通过上面的分析


  • 我们可以知道当String s1 = "a" + "a"时在编译阶段由于可以直接确定s1的值,所以在编译阶段直接将s1的值赋值为aa
  • String s1 = s+ "a"在编译阶段中由于不知道s的内容(在编译阶段jvm不会知道一个对象的内容),所以需要运行期间来解析s并且通过StringBuilder进行优化来将它们相加。  


所以我们在平时写代码的时候对于字符串拼接用StringBuilder来拼接,因为String类型相加底层用的StringBuilder,而每一次String相加都会生成一个对象,使用StringBuilder可以节约内存,避免内存溢出



new创建字符串


众所周知String s = new String("a")将会在生成一个String对象,字符串a会不会加入到常量池中呢?我们对String s = new String("a")也进行反编译如下:


image.png


  • 通过对上面进行反编译可以看到使用new创建对象的时候执行了ldc这个指令,ldc指令的意思是操作字符串常量池,如果有直接拉取下来,如果没有就创建一个对象在常量池中
  • 通过反编译我们可以看出使用new String()创建对象的时候我们访问了字符串常量池的,那么是不是创建s对象的时候在常量池也创建了一个"a"呢


可以理解为


String variable = "a"; // variable为匿名变量
String s = new String(variable);
复制代码

也就是说在第二种初始化中是包含了第一种初始化的,首先进行的是以字面量的形式创建匿名变量,具体流程与第一种方式初始化一致,然后new操作会在堆上创建s指向的String对象,也就是说第二种方式初始化实际上会创建两个String对象,一个存放在字符串常量池,一个存放在堆中;


实际案例分析


public class ImmutableStrings
{
    public static void main(String[] args)
    {
        String one = "someString";
        String two = new String("someString");
        System.out.println(one.equals(two));
        System.out.println(one == two);
    }
}
// Output
true
false


image.png

带着这个疑问我们看看String 的构造函数

image.png



  • 这个构造函数只是将"a"的value和hash赋值给了新创建的对象,而value是char[]类型的数组,hash也是int类型,这两个都不可能对字符串常量池访问,所以真正的原因只可能是传入的"a"是从字符串常量池中获取的,所以我们在new String()的时候有可能会生成两个对象。
  • 所以对于new String("a")可能会生成两个对象,一个是字符串类型对象存放在堆中,另外一个就是字符串常量池对象,当然如果a 以前在字符串常量池中存在那么将不会创建字符串常量池对象。



注意:new String()本身不会在字符串常量池中创建相应的对象,new String("a")会生成两个对象的原因是因为"a"在加载new String("a");这行代码所存在的类的时候将其放入字符串常量池中的。比如"int"就是java在Integer类的初始化阶段时候在解析 public static final Class TYPE = (Class) Class.getPrimitiveClass("int");这行代码的时候放入到字符串常量池中的,具体可以根据工具进行查看 =》工具可以使用GDB


String s = new String(new char[]{'a','b','c'});

image.png


可以看出当使用char[]数组创建对象的时候并没有访问常量池,通过上面我们可以得出只要在代码中出现"a","ab"这种直接告诉我们这是什么的字符串才会在常量池中创建相应的对象


可以得出以下结论:


字面量形式的String对象初始化都会被加入字符串常量池;此时当内容一致时,多个引用会同时指向同一对象,这也是为什么String会被设计成immutability(不变性),防止当一个引用更改对象的内容时,其它引用被迫更改。


使用new操作创建的String对象,一定会在堆上创建对象,但是如果涉及到字面量初始化,则会创建两个对象,分别存放在字符串常量池与堆中。


  1. 在直接使用双引号"" 声明字符串的时候,java 都会去常量池通过equal找有没有相同的字符串,如果有,则将常量池的引用返回给变量,如果没有 回在常量池中创建一个对象,然后返回这个对象的引用
  2. 使用new 关键字创建,例如 String a = new String("a"),这里首先会去常量池对比有没有"a",没有则会创建,其次 new 一定会在堆里面创建一个新对象 并返回该对象的引用
  3. 使用+ 运算符,此处有大致有三种情况,


String str = "ab"+"cd"; 在常量池上创建常量ab, cd ,abcd 返回 abcd【着重了解abcd在常量池上】

String str = new String("ab") + new String("cd"); 在堆上创建对象ab、cd和 abcd,在常量池上创建常量ab和cd ,常量池上不会创建 abcd【着重了解abcd 不会在常量池上】


还有混合使用的就不在一一说明 如 String str = "ab" + new String("cd"); 或者 String strAb = "ab"; String str = strAb + new String("cd") 等情况


验证运算符
String s2 = new String("a") +"b"; //s2 指向堆 里面ab 的地址,并且常量池不存在 ab
String s3 = s2.intern(); // 由于常量池并没有ab 因此会把 s2 的堆地址引用 放到常量池
System.out.println(s2 == s3);// 同时指向 ab 堆里面的地址 固为true
复制代码



验证运算符与字符串混用


区别仅仅就是多定义了一个String s1 ="ab",

String s1 = "ab"; //s1指向 ab常量池地址
String s2 = new String("a") +"b"; //s2 指向 堆 里面ab 的地址,常量池已存在 ab
String s3 = s2.intern(); // 由于常量池存在 ab 因为会把 ab 常亮池的引用返回
System.out.println(s2 == s3);// s2 指向堆 s3地址字符常量池 固为false
复制代码



来一个特殊字符串


与代码3 一样仅仅是字符串替换成了"java" 返回结果为false 其实此处就是因为jvm虚拟机在其他类【Version.class】先定义了放入了常量池,其实原理就和代码4一样 先把ab 放入了常量池 了解了原理就可以举一反三

String s2 = new String("ja") +"va"; //s2 指向 堆 里面java 的地址,常量池已存在 java【原因查看Version.class】
String s3 = s2.intern();// 由于常量池存在 java 因为会把 java 常亮池的引用返回
System.out.println(s2 == s3);// s2 指向堆 s3地址字符常量池 固为false


image.png




运行时常量的包装类


8种基本数据类型都有自己的包装类,在包装类对象创建的实话就会消耗资源,因此 java 对 其中5种(Byte,Short,Integer,Long,Character,Boolean)包装类实现了常量池技术,默认创建了数值(-128 ,127)的相应类型的缓存数据,但是超出了此范围依然会去创建新的对象。两种浮点数类型的包装类 (Float,Double) 并没有实现常量池技术










相关文章
|
13天前
|
安全 Java 程序员
《从头开始学java,一天一个知识点》之:控制流程:if-else条件语句实战
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问"`a==b`和`equals()`的区别",大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列为你打造Java「速效救心丸」!每天1分钟,地铁通勤、午休间隙即可完成学习。直击高频考点和实际开发中的「坑位」,拒绝冗长概念,每篇都有可运行的代码示例。明日预告:《for与while循环的使用场景》。 ---
52 19
|
6天前
|
存储 缓存 人工智能
【原理】【Java并发】【synchronized】适合中学者体质的synchronized原理
本文深入解析了Java中`synchronized`关键字的底层原理,从代码块与方法修饰的区别到锁升级机制,内容详尽。通过`monitorenter`和`monitorexit`指令,阐述了`synchronized`实现原子性、有序性和可见性的原理。同时,详细分析了锁升级流程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,结合对象头`MarkWord`的变化,揭示JVM优化锁性能的策略。此外,还探讨了Monitor的内部结构及线程竞争锁的过程,并介绍了锁消除与锁粗化等优化手段。最后,结合实际案例,帮助读者全面理解`synchronized`在并发编程中的作用与细节。
30 8
【原理】【Java并发】【synchronized】适合中学者体质的synchronized原理
|
14天前
|
存储 缓存 安全
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
82 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
|
7天前
|
消息中间件 Java 应用服务中间件
JVM实战—1.Java代码的运行原理
本文介绍了Java代码的运行机制、JVM类加载机制、JVM内存区域及其作用、垃圾回收机制,并汇总了一些常见问题。
JVM实战—1.Java代码的运行原理
|
9天前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
38 11
|
15天前
|
Java
课时14:Java数据类型划分(初见String类)
课时14介绍Java数据类型,重点初见String类。通过三个范例讲解:观察String型变量、"+"操作符的使用问题及转义字符的应用。String不是基本数据类型而是引用类型,但使用方式类似基本类型。课程涵盖字符串连接、数学运算与字符串混合使用时的注意事项以及常用转义字符的用法。
|
14天前
|
存储 Java 编译器
课时11:综合实战:简单Java类
本次分享的主题是综合实战:简单 Java 类。主要分为两个部分: 1.简单 Java 类的含义 2.简单 Java 类的开发
|
14天前
|
Oracle Java 关系型数据库
课时37:综合实战:数据表与简单Java类映射转换
今天我分享的是数据表与简单 Java 类映射转换,主要分为以下四部分。 1. 映射关系基础 2. 映射步骤方法 3. 项目对象配置 4. 数据获取与调试
|
14天前
|
存储 JavaScript Java
课时44:String类对象两种实例化方式比较
本次课程的主要讨论了两种处理模式在Java程序中的应用,直接赋值和构造方法实例化。此外,还讨论了字符串池的概念,指出在Java程序的底层,DOM提供了专门的字符串池,用于存储和查找字符串。 1.直接赋值的对象化模式 2.字符串池的概念 3.构造方法实例化
|
4月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
140 2

热门文章

最新文章