揭秘Java中的String

揭秘Java中的String

最近有人问我一个问题:String是什么?

是什么?脑子一片混乱。不可变?不是基本数据类型但又像基本数据类型?顿时很多模糊的理解涌入脑海,怎么都理不清,究其原因,还是对String在JVM里的实现原理不甚理解。为了彻底搞清究竟什么是String,今天特意看了很多相关的资料,终于有所感悟,感悟之余,记录于此,与大家共享。(部分内容抄摘自网上)

一、创建String

创建一个String对象,主要有两种方式:

  1. String s=”Hello world!”;
  2. String s=new String(“Hello world”);

两种方式虽然都实现了创建一个String对象的功能,但实现的原理却大不相同。在讨论这两种方法的不同之前,我们先来了解一下JVM里的常量池概念,对接下来的理解很有帮助。

相信大家都知道,Java程序在运行之前,编译器首先要把源代码编译成字节码文件(.class文件),然后JVM再解释执行.class文件。其中,在.class文件中有一个非常重要的项—常量池,我们上面代码中的”Hello world”字符串被编译之后,就被存放在class常量池中的字符串常量表中。改用网上一段话:

在Java源代码中的每一个字面值字符串,都会在编译成class文件阶段,形成标志号 为8(CONSTANT_String_info)的常量表 。 当JVM加载 class文件的时候,会为对应的常量池建立一个内存数据结构,并存放在方法区中。同时JVM会自动为CONSTANT_String_info常量表中 的字符串常量字面值 在堆中创建 新的String对象(intern字符串对象)。然后把CONSTANT_String_info常量表的入口地址转变成这个堆中String对象的直接地址(常量池解析)。

源代码中所有相同字面值的字符串常量只可能建立唯一一个intern字符串对象。另外,我们也可以调用String的intern()方法来使得一个常规字符串对象成为intern字符串对象。

好了,下面我们来讨论一下第一种方法,也是大家非常常用的方法:

String s=”Hello world!”

首先在编译期,也就是在运行这段指令之前,JVM就已经为”Hello world”在堆中创建了一个intern字符串,局部变量s存储的是早已创建好的intern字符串的堆地址。也就是说,不管有几条String s1=”Hello world”,堆中都只有1个值为”Hello world”的字符串。

那么第二种方法呢?

String s=new String(“Hello world”)

同样在编译期,JVM也为”Hello world”在堆中创建了一个intern字符串,然后用这个intern字符串的值来初始化new出来的新String对象,局部变量s实际上存储的是new出来的堆对象地址。 此时在JVM管理的堆中,有两个相同字符串值的String对象:一个是intern字符串对象,一个是new新建的字符串对象。

最后,来段代码做个更形象的比较:

01 //代码1
02 String sa = "ab";
03 String sb = "cd";
04 String sab=sa+sb;
05 String s="abcd";
06 System.out.println(sab==s); // false
07 //代码2
08 String sc="ab"+"cd";
09 String sd="abcd";
10 System.out.println(sc==sd); //true

代码1中局部变量sa,sb存储的是堆中两个intern字符串对象的地址。而当执行sa+sb时,JVM首先会在堆中创建一个StringBuilder类,同时用sa指向的intern字符串对象完成初始化,然后调用append方法完成对sb所指向的intern字符串的合并操作,接着调用StringBuilder的toString()方法在堆中创建一个String对象,最后将刚生成的String对象的堆地址存放在局部变量sab中。而局部变量s存储的是常量池中”abcd”所对应的intern字符串对象的地址。 sab与s地址当然不一样了。这里要注意了,代码1的堆中实际上有五个字符串对象:三个intern字符串对象、一个String对象和一个StringBuilder对象。
代码2中”ab”+”cd”会直接在编译期就合并成常量”abcd”, 因此相同字面值常量”abcd”所对应的是同一个拘留字符串对象,自然地址也就相同。

二、String,StringBuffer和StringBuilder

  1. 通过查看源码可以发现,String中的value[]是常量(final)数组,只能被赋值一次;而StringBuffer中的value[]就是一个很普通的数组,而且可以通过append()方法将新字符串加入value[]末尾。
  2. StringBuffer和StringBuilder的区别是StringBuffer是线程安全的,而后者不是。这是因为在源代码中StringBuffer的很多方法都被关键字synchronized 修饰了,而StringBuilder没有。另外,由于String是不可变的,也就是只读,自然也是安全的了。
  3. 因为String对象中的value[]是不能改变的,每一次合并后字符串值都需要创建一个新的String对象来存放。循环1000次自然需要创建1000个String对象和1000个StringBuilder对象,效率低就可想而知了;而StringBuffer/StringBuilder,只需要将自己的value[]数组不停的扩大来存放即可,循环过程中无需在堆中创建任何新的对象,效率自然就高了。

三、总结:

  1. 不停的创建对象是程序低效的一个重要原因。那么相同的字符串值能否在堆中只创建一个String对象?可以!除了程序中的字符串常量会被JVM自动创建拘留字符串之外,调用String的intern()方法也能做到这一点。当调用intern()时,如果常量池中已经有了当前String的值,那么返回这个常量指向intern对象的地址。如果没有,则将String值加入常量池中,并创建一个新的intern字符串对象。
  2. String的种种行为都来源于它的immutable性. 因为它是不变的,没有线程安全问题,可以无限共享,池化当然最节省时间空间; 也因为它是不变的,用”+”导致N多的新对象生成,才生的效率问题.

四、知识补充:

equals 和 == 的区别

  1. equals 方法(是String类从它的超类Object中继承的)被用来检测两个对象是否相等,即两个对象的内容是否相等。
  2. ==用于比较引用和比较基本数据类型时具有不同的功能:比较基本数据类型,如果两个值相同,则结果为true ; 在比较引用时,如果引用指向内存中的同一对象,结果为true

堆、栈与常量池:

  1. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中。)
  2. 堆:存放所有new出来的对象。
  3. 静态域:存放静态成员(static定义的)
  4. 常量池:存放字符串常量和基本类型常量(public static final)。

栈和常量池中的对象可以共享,堆中的对象不可以共享。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

原创文章,转载请注明: 转载自Ryan's note

本文链接地址: 揭秘Java中的String

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值