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










相关文章
|
20天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
39 5
|
24天前
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第14天】Java零基础教学篇,手把手实践教学!
110 65
|
9天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
9天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
11天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
17天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
34 2
|
20天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
17天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
31 1
|
24天前
|
存储 安全 Java
深入理解Java中的FutureTask:用法和原理
【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。
|
1月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
55 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性

热门文章

最新文章