做者:每次上网冲杯Java时,都能看到关于String无休无止的争论。仍是以为有必要让这个讨厌又很可爱的String美眉,赤裸裸的站在咱们这些Java色狼面前了。嘿嘿....
众所周知,String是由字符组成的串,在程序中使用频率很高。Java中的String是一个类,而并不是基本数据类型。 不过她却不是普通的类哦!!!java
【镜头1】 String对象的建立
一、关于类对象的建立,很普通的一种方式就是利用构造器,String类也不例外:String s=new String("Hello world"); 问题是参数"Hello world"是什么东西,也是字符串对象吗?莫非用字符串对象建立一个字符串对象?
二、固然,String类对象还有一种你们都很喜欢的建立方式:String s="Hello world"; 可是有点怪呀,怎么与基本数据类型的赋值操做(int i=1)很像呀?程序员
在开始解释这些问题以前,咱们先引入一些必要的知识:
★ Java class文件结构 和常量池
咱们都知道,Java程序要运行,首先须要编译器将源代码文件编译成字节码文件(也就是.class文件)。而后在由JVM解释执行。
class文件是8位字节的二进制流 。这些二进制流的涵义由一些紧凑的有意义的项 组成。好比class字节流中最开始的4个字节组成的项叫作魔数 (magic),其意义在于分辨class文件(值为0xCAFEBABE)与非class文件。class字节流大体结构以下图左侧。算法
其中,在class文件中有一个很是重要的项——常量池 。这个常量池专门放置源代码中的符号信息(而且不一样的符号信息放置在不一样标志的常量表中)。如上图右侧是HelloWorld代码中的常量表(HelloWorld代码以下),其中有四个不一样类型的常量表(四个不一样的常量池入口)。关于常量池的具体细节,请参照个人博客《Class文件内容及常量池 》编程
经过上图可见,代码中的"Hello world"字符串字面值被编译以后,能够清楚的看到存放在了class常量池中的字符串常量表中(上图右侧红框区域)。数组
★ JVM运行class文件安全
源代码编译成class文件以后,JVM就要运行这个class文件。它首先会用类装载器加载进class文件。而后须要建立许多内存数据结构来存放class文件中的字节数据。好比class文件对应的类信息数据、常量池结构、方法中的二进制指令序列、类方法与字段的描述信息等等。固然,在运行的时候,还须要为方法建立栈帧等。这么多的内存结构固然须要管理,JVM会把这些东西都组织到几个“运行时数据区 ”中。这里面就有咱们常常说的“方法区 ”、“堆 ”、“Java栈 ”等。详细请参见个人博客《Java 虚拟机体系结构 》 。数据结构
上面咱们提到了,在Java源代码中的每个字面值字符串,都会在编译成class文件阶段,造成标志号 为8(CONSTANT_String_info)的常量表 。 当JVM加载 class文件的时候,会为对应的常量池创建一个内存数据结构,并存放在方法区中。同时JVM会自动为CONSTANT_String_info常量表中 的字符串常量字面值 在堆中 建立 新的String对象(intern字符串 对象,又叫拘留字符串对象)。而后把CONSTANT_String_info常量表的入口地址转变成这个堆中String对象的直接地址(常量池解 析)。 多线程
这里很关键的就是这个拘留字符串对象 。源代码中全部相同字面值的字符串常量只可能创建惟一一个拘留字符串对象。 实际上JVM是经过一个记录了拘留字符串引用的内部数据结构来维持这一特性的。在Java程序中,能够调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象。咱们会在后面介绍这个方法的。app
★ 操做码助忆符指令
有了上面阐述的两个知识前提,下面咱们将根据二进制指令来区别两种字符串对象的建立方式: eclipse
(1) String s=new String("Hello world");编译成class文件后的指令(在myeclipse中查看):
注意:
【这里有个dup指令。其做用就是复制以前分配的Java.lang.String空间的引用并压入栈顶。那么这里为何须要这样么作呢?由于invokespecial指令经过[15]这个常量池入口寻找到了java.lang.String()构造方法,构造方法虽然找到了。可是必须还得知道是谁的构造方法,因此要将以前分配的空间的应用压入栈顶让invokespecial命令应用才知道原来这个构造方法是刚才建立的那个引用的,调用完成以后将栈顶的值弹出。以后调用astore_1将此时的栈顶值弹出存入局部变量中去。】
事实上,在运行这段指令以前,JVM就已经为"Hello world"在堆中建立了一个拘留字符串( 值得注意的是:若是源程序中还有一个"Hello world"字符串常量,那么他们都对应了同一个堆中的拘留字符串)。而后用这个拘留字符串的值来初始化堆中用new指令建立出来的新的String对象,局部变量s实际上存储的是new出来的堆对象地址。 你们注意了,此时在JVM管理的堆中,有两个相同字符串值的String对象:一个是拘留字符串对象,一个是new新建的字符串对象。若是还有一条建立语句String s1=new String("Hello world");堆中有几个值为"Hello world"的字符串呢? 答案是3个,你们好好想一想为何吧!
(2)将String s="Hello world";编译成class文件后的指令:
和上面的建立指令有很大的不一样,局部变量s存储的是早已建立好的拘留字符串的堆地址(没有new 的对象了)。 你们好好想一想,若是还有一条穿件语句String s1="Hello word";此时堆中有几个值为"Hello world"的字符串呢?答案是1个。那么局部变量s与s1存储的地址是否相同呢? 呵呵, 这个你应该知道了吧。
★ 镜头总结: String类型脱光了其实也很普通。真正让她神秘的缘由就在于CONSTANT_String_info常量表 和拘留字符串对象 的存在。如今咱们能够解决江湖上的许多纷争了。
【 纷争1】关于字符串相等关系的争论
代码1中局部变量sa,sb中存储的是JVM在堆中new出来的两个String对象的内存地址。虽然这两个String对象的值(char[]存放的字符序列)都是"Hello world"。 所以"=="比较的是两个不一样的堆地址。代码2中局部变量sc,sd中存储的也是地址,但却都是常量池中"Hello world"指向的堆的惟一的那个拘留字符串对象的地址 。天然相等了。
【纷争2】 字符串“+”操做的内幕
代码1中局部变量sa,sb存储的是堆中两个拘留字符串对象的地址。而当执行sa+sb时,JVM首先会在堆中建立一个StringBuilder类,同时用sa指向的拘留字符串对象完成初始化,而后调用append方法完成对sb所指向的拘留字符串的合并操做,接着调用StringBuilder的toString()方法在堆中建立一个String对象,最后将刚生成的String对象的堆地址存放在局部变量sab中。而局部变量s存储的是常量池中"abcd"所对应的拘留字符串对象的地址。 sab与s地址固然不同了。这里要注意了,代码1的堆中实际上有五个字符串对象:三个拘留字符串对象、一个String对象和一个StringBuilder对象。
代码2中"ab"+"cd"会直接在编译期就合并成常量"abcd", 所以相同字面值常量"abcd"所对应的是同一个拘留字符串对象,天然地址也就相同。
【镜头二】 String三姐妹(String,StringBuffer,StringBuilder)
String扒的差很少了。但他还有两个妹妹StringBuffer,StringBuilder长的也不错哦!咱们也要下手了:
String(大姐,出生于JDK1.0时代) 不可变字符序列
StringBuffer(二姐,出生于JDK1.0时代) 线程安全的可变字符序列
StringBuilder(小妹,出生于JDK1.5时代) 非线程安全的可变字符序列
★StringBuffer与String的可变性问题。
咱们先看看这两个类的部分源代码:
很显然,String和StringBuffer中的value[]都用于存储字符序列。可是,
(1) String中的是常量(final)数组,只能被赋值一次。
好比:new String("abc")使得value[]={'a','b','c'}(查看jdk String 就是这么实现的),以后这个String对象中的value[]不再能改变了。这也正是你们常说的,String是不可变的缘由 。
注意:这个对初学者来讲有个误区,有人说String str1=new String("abc"); str1=new String("cba");不是改变了字符串str1吗?那么你有必要先搞懂对象引用和对象自己的区别。这里我简单的说明一下,对象自己指的是存放在堆空间中的该对象的实例数据(非静态很是量字段)。而对象引用指的是堆中对象自己所存放的地址,通常方法区和Java栈中存储的都是对象引用,而非对象自己的数据。
(2) StringBuffer中的value[]就是一个很普通的数组,并且能够经过append()方法将新字符串加入value[]末尾。这样也就改变了value[]的内容和大小了。
好比:new StringBuffer("abc")使得value[]={'a','b','c','',''...}(注意构造的长度是str.length()+16)。若是再将这个对象append("abc"),那么这个对象中的value[]={'a','b','c','a','b','c',''....}。这也就是为何你们说 StringBuffer是可变字符串 的涵义了。从这一点也能够看出,StringBuffer中的value[]彻底能够做为字符串的缓冲区功能。其累加性能是很不错的,在后面咱们会进行比较。
总结,讨论String和StringBuffer可不可变。本质上是指对象中的value[]字符数组可不可变,而不是对象引用可不可变。
★StringBuffer与StringBuilder的线程安全性问题
StringBuffer和StringBuilder能够算是双胞胎了,这二者的方法没有很大区别。但在线程安全性方面,StringBuffer容许多线程进行字符操做。这是由于在源代码中StringBuffer的不少方法都被关键字synchronized 修饰了,而StringBuilder没有。
有多线程编程经验的程序员应该知道synchronized。这个关键字是为线程同步机制 设定的。我简要阐述一下synchronized的含义:
每个类对象都对应一把锁,当某个线程A调用类对象O中的synchronized方法M时,必须得到对象O的锁才可以执行M方法,不然线程A阻塞。一旦线程A开始执行M方法,将独占对象O的锁。使得其它须要调用O对象的M方法的线程阻塞。只有线程A执行完毕,释放锁后。那些阻塞线程才有机会从新调用M方法。这就是解决线程同步问题的锁机制。
了解了synchronized的含义之后,你们可能都会有这个感受。多线程编程中StringBuffer比StringBuilder要安全多了 ,事实确实如此。若是有多个线程须要对同一个字符串缓冲区进行操做的时候,StringBuffer应该是不二选择。
注意:是否是String也不安全呢?事实上不存在这个问题,String是不可变的。线程对于堆中指定的一个String对象只能读取,没法修改。试问:还有什么不安全的呢?
★String和StringBuffer的效率问题(这但是个热门话题呀!)
首先说明一点:StringBuffer和StringBuilder可谓双胞胎,StringBuilder是1.5新引入的,其前身就是StringBuffer。StringBuilder的效率比StringBuffer稍高,若是不考虑线程安全,StringBuilder应该是首选。另外,JVM运行程序主要的时间耗费是在建立对象和回收对象上。
咱们用下面的代码运行1W次字符串的链接操做,测试String,StringBuffer所运行的时间。
(1) String常量与String变量的"+"操做比较
▲测试①代码: (测试代码位置1) String str="";
(测试代码位置2) str="Heart"+"Raid";
[耗时: 0ms]
▲测试②代码 (测试代码位置1) String s1="Heart";
String s2="Raid";
String str="";
(测试代码位置2) str=s1+s2;
[耗时: 15—16ms]
结论:String常量的“+链接” 稍优于 String变量的“+链接”。
缘由:测试①的"Heart"+"Raid"在编译阶段就已经链接起来,造成了一个字符串常量"HeartRaid",并指向堆中的拘留字符串对象。运行时只须要将"HeartRaid"指向的拘留字符串对象地址取出1W次,存放在局部变量str中。这确实不须要什么时间。
测试②中局部变量s1和s2存放的是两个不一样的拘留字符串对象的地址。而后会经过下面三个步骤完成“+链接”:
一、StringBuilder temp=new StringBuilder(s1),
二、temp.append(s2);
三、str=temp.toString();
咱们发现,虽然在中间的时候也用到了append()方法,可是在开始和结束的时候分别建立了StringBuilder和String对象。可想而知:调用1W次,是否是就建立了1W次这两种对象呢?不划算。
可是,String变量的"+链接"操做比String常量的"+链接"操做使用的更加普遍。 这一点是不言而喻的。
(2)String对象的"累+"链接操做与StringBuffer对象的append()累和链接操做比较。
▲测试①代码: (代码位置1) String s1="Heart";
String s="";
(代码位置2) s=s+s1;
[耗时: 4200—4500ms]
▲测试②代码 (代码位置1) String s1="Heart";
StringBuffer sb=new StringBuffer();
(代码位置2) sb.append(s1);
[耗时: 0ms(当循环100000次的时候,耗时大概16—31ms)]
结论:大量字符串累加时,StringBuffer的append()效率远好于String对象的"累+"链接
缘由:测试① 中的s=s+s1,JVM会利用首先建立一个StringBuilder,并利用append方法完成s和s1所指向的字符串对象值的合并操做,接着调用StringBuilder的 toString()方法在堆中建立一个新的String对象,其值为刚才字符串的合并结果。而局部变量s指向了新建立的String对象。
由于String对象中的value[]是不能改变的,每一次合并后字符串值都须要建立一个新的String对象来存放。循环1W次天然须要建立1W个String对象和1W个StringBuilder对象,效率低就可想而知了。
测试②中sb.append(s1);只须要将本身的value[]数组不停的扩大来存放s1便可。循环过程当中无需在堆中建立任何新的对象。效率高就不足为奇了。
★ 镜头总结:
(1) 在编译阶段就可以肯定的字符串常量,彻底没有必要建立String或StringBuffer对象。直接使用字符串常量的"+"链接操做效率最高。
(2) StringBuffer对象的append效率要高于String对象的"+"链接操做。
(3) 不停的建立对象是程序低效的一个重要缘由。那么相同的字符串值可否在堆中只建立一个String对象那。显然拘留字符串可以作到这一点,除了程序中的字符串常量会被JVM自动建立拘留字符串以外,调用String的intern()方法也能作到这一点。当调用intern()时,若是常量池中已经有了当前String的值,那么返回这个常量指向拘留对象的地址。若是没有,则将String值加入常量池中,并建立一个新的拘留字符串对象。