【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) 并没有实现常量池技术










相关文章
|
8月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
246 4
|
6月前
|
安全 Java 开发者
告别NullPointerException:Java Optional实战指南
告别NullPointerException:Java Optional实战指南
329 119
|
8月前
|
缓存 前端开发 Java
基于最新 Java 技术栈的在线任务管理系统开发实战详解
本项目基于最新Java技术栈开发在线任务管理系统,涵盖任务创建、分配、跟踪、统计等功能。采用Spring Boot 3.2.x、React 18、PostgreSQL 16等主流技术,详解项目架构设计、核心功能实现及部署流程,助力掌握现代Java全栈开发技能。
449 6
|
8月前
|
Java 关系型数据库 数据库
Java 项目实战教程从基础到进阶实战案例分析详解
本文介绍了多个Java项目实战案例,涵盖企业级管理系统、电商平台、在线书店及新手小项目,结合Spring Boot、Spring Cloud、MyBatis等主流技术,通过实际应用场景帮助开发者掌握Java项目开发的核心技能,适合从基础到进阶的学习与实践。
1131 3
|
8月前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
1531 1
|
7月前
|
存储 前端开发 Java
【JAVA】Java 项目实战之 Java Web 在线商城项目开发实战指南
本文介绍基于Java Web的在线商城技术方案与实现,涵盖三层架构设计、MySQL数据库建模及核心功能开发。通过Spring MVC + MyBatis + Thymeleaf实现商品展示、购物车等模块,提供完整代码示例,助力掌握Java Web项目实战技能。(238字)
790 0
|
7月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
559 100
|
8月前
|
数据采集 JSON Java
Java爬虫获取1688店铺所有商品接口数据实战指南
本文介绍如何使用Java爬虫技术高效获取1688店铺商品信息,涵盖环境搭建、API调用、签名生成及数据抓取全流程,并附完整代码示例,助力市场分析与选品决策。
|
8月前
|
消息中间件 Java 数据库
Java 基于 DDD 分层架构实战从基础到精通最新实操全流程指南
本文详解基于Java的领域驱动设计(DDD)分层架构实战,结合Spring Boot 3.x、Spring Data JPA 3.x等最新技术栈,通过电商订单系统案例展示如何构建清晰、可维护的微服务架构。内容涵盖项目结构设计、各层实现细节及关键技术点,助力开发者掌握DDD在复杂业务系统中的应用。
1477 0
|
6月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
308 2