Java String 的详解

结合代码来看

 public static void main(String[]args){
/**
*情景一:字符串池
*JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象;
*并且可以被共享使用,因此它提高了效率。
*由于String类是final的,它的值一经创建就不可改变。
*字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
*/
String s1="abc";
//↑在字符串池创建了一个对象
String s2="abc";
//↑字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象
System.out.println("s1==s2:"+(s1==s2));
//↑true指向同一个对象,
System.out.println("s1.equals(s2):"+(s1.equals(s2)));
//↑true值相等
//↑------------------------------------------------------over
/**
/*
*情景二:关于new String("")
*/
String s3 = new String("abc");
//↑创建了两个对象,一个 "abc" 存放在字符串池中,一个 String 对象存在与堆区中;
//↑还有一个对象引用s3存放在栈中
String s4 = new String("abc");
//↑字符串池中已经存在“abc”对象,所以只在堆中创建了一个String对象
//↑还有一个对象引用s4存放在栈中
System.out.println("s3==s4:"+(s3==s4));
//↑false s3和s4指向堆区的不同地址;
System.out.println("s3.equals(s4):"+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1==s3:"+(s1==s3));
//↑false存放的地区不同,一个栈区,一个堆区
System.out.println("s1.equals(s3):"+(s1.equals(s3)));
//↑true值相同
//↑------------------------------------------------------over
/**
/*情景三:
*由于常量的值在编译的时候就被确定(优化)了。
*在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。
*这行代码编译后的效果等同于:String str1 ="abcd";
*/
String str1 = "ab" + "cd";//1个对象,在编译时直接创建为"abcd"
String str11="abcd";
System.out.println("str1=str11:"+(str1==str11));
//↑--------true--------同情景一
/**
/*情景四:
*局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。
*
*第三行代码原理(str2+str3):
*运行期JVM首先会在堆中创建一个StringBuilder类,
*同时用str2指向的拘留字符串对象完成初始化,
*然后调用append方法完成对str3所指向的拘留字符串的合并,
*接着调用StringBuilder的toString()方法在堆中创建一个String对象,
*最后将刚生成的String对象的堆地址存放在局部变量str3中。
*
*而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。
*str4与str5地址当然不一样了。
*
*内存中实际上有五个字符串对象:
*三个拘留字符串对象、一个String对象和一个StringBuilder对象。
*/
String str2 = "ab";//1个对象
String str3 = "cd";//1个对象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4=str5:"+(str4==str5));//false
//↑------------------------------------------------------over
/**
/*情景五:
*JAVA编译器对string+基本类型/常量是当成常量表达式直接求值来优化的。
*运行期的两个string相加,会产生新的对象的,存储在堆(heap)中
*/
Stringstr6="b";
Stringstr7="a"+str6;
Stringstr67="ab";
System.out.println("str7=str67:"+(str7==str67));
//↑str6为变量,在运行期才会被解析。
finalStringstr8="b";
Stringstr9="a"+str8;
Stringstr89="ab";
System.out.println("str9=str89:"+(str9==str89));
//↑str8为常量变量,编译期会被优化
//↑------------------------------------------------------over
}

总结:
1.String类初始化后是不可变的(immutable)
这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:Stringstr=”kv”+”ill”+”“+”ans”;是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和””生成“kvill“存在内存中,最后又和生成了”kvillans”;并把这个字符串的地址赋给了str,就是因为String的”不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。
下面是一些String相关的常见问题:
String中的final用法和理解
finalStringBuffera=newStringBuffer(“111”);
finalStringBufferb=newStringBuffer(“222”);
a=b;//此句编译不通过finalStringBuffera=newStringBuffer(“111”);
a.append(“222”);//编译通过
可见,final只对引用的”值”(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。
2.代码中的字符串常量在编译的过程中收集并放在class文件的常量区中,如”123”、”123”+”456”等,含有变量的表达式不会收录,如”123”+a。
3.JVM在加载类的时候,根据常量区中的字符串生成常量池,每个字符序列如”123”会生成一个实例放在常量池里,这个实例是不在堆里的,也不会被GC,这个实例的value属性从源码的构造函数看应该是用new创建数组置入123的,所以按我的理解此时value存放的字符数组地址是在堆里,如果有误的话欢迎大家指正。
4.使用String不一定创建对象
在执行到双引号包含字符串的语句时,如Stringa=“123”,JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。如果是Stringa=“123”+b(假设b是”456”),前半部分”123”还是走常量池的路线,但是这个+操作符其实是转换成[SringBuffer].Appad()来实现的,所以最终a得到是一个新的实例引用,而且a的value存放的是一个新申请的字符数组内存空间的地址(存放着”123456”),而此时”123456”在常量池中是未必存在的。
要注意:我们在使用诸如Stringstr=“abc”;的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象
5.使用newString,一定创建对象
在执行Stringa=newString(“123”)的时候,首先走常量池的路线取到一个实例的引用,然后在堆上创建一个新的String实例,走以下构造函数给value属性赋值,然后把实例引用赋值给a:
publicString(Stringoriginal){
intsize=original.count;
char[]originalValue=original.value;
char[]v;
if(originalValue.length>size){
//ThearrayrepresentingtheStringisbiggerthanthenew
//Stringitself.Perhapsthisconstructorisbeingcalled
//inordertotrimthebaggage,somakeacopyofthearray.
intoff=original.offset;
v=Arrays.copyOfRange(originalValue,off,off+size);
}else{
//ThearrayrepresentingtheStringisthesame
//sizeastheString,sonopointinmakingacopy.
v=originalValue;
}
this.offset=0;
this.count=size;
this.value=v;
}
从中我们可以看到,虽然是新创建了一个String的实例,但是value是等于常量池中的实例的value,即是说没有new一个新的字符数组来存放”123”。
如果是Stringa=newString(“123”+b)的情况,首先看回第4点,”123”+b得到一个实例后,再按上面的构造函数执行。
6.String.intern()
String对象的实例调用intern方法后,可以让JVM检查常量池,如果没有实例的value属性对应的字符串序列比如”123”(注意是检查字符串序列而不是检查实例本身),就将本实例放入常量池,如果有当前实例的value属性对应的字符串序列”123”在常量池中存在,则返回常量池中”123”对应的实例的引用而不是当前实例的引用,即使当前实例的value也是”123”。
publicnativeStringintern();
存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看示例就清楚了

publicstaticvoidmain(String[]args){
Strings0="kvill";
Strings1=newString("kvill");
Strings2=newString("kvill");
System.out.println(s0==s1);//false
System.out.println("**********");
s1.intern();//虽然执行了s1.intern(),但它的返回值没有赋给s1
s2=s2.intern();//把常量池中"kvill"的引用赋给s2
System.out.println(s0==s1);//flase
System.out.println(s0==s1.intern());//true//说明s1.intern()返回的是常量池中"kvill"的引用
System.out.println(s0==s2);//true
}

最后我再破除一个错误的理解:有人说,“使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中”如果我把他说的这个全局的String表理解为常量池的话,他的最后一句话,”如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:

publicstaticvoidmain(String[]args){
Strings1=newString("kvill");
Strings2=s1.intern();
System.out.println(s1==s1.intern());//false
System.out.println(s1+""+s2);//kvillkvill
System.out.println(s2==s1.intern());//true
}

在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。
s1==s1.intern()为false说明原来的”kvill”仍然存在;s2现在为常量池中”kvill”的地址,所以有s2==s1.intern()为true。
StringBuffer与StringBuilder的区别,它们的应用场景是什么?
jdk的实现中StringBuffer与StringBuilder都继承自AbstractStringBuilder,对于多线程的安全与非安全看到StringBuffer中方法前面的一堆synchronized就大概了解了。
这里随便讲讲AbstractStringBuilder的实现原理:我们知道使用StringBuffer等无非就是为了提高java中字符串连接的效率,因为直接使用+进行字符串连接的话,jvm会创建多个String对象,因此造成一定的开销。AbstractStringBuilder中采用一个char数组来保存需要append的字符串,char数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,也即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式,这里是2倍

StringBuffer始于JDK1.0
StringBuilder始于JDK1.5
从JDK1.5开始,带有字符串变量的连接操作(+),JVM内部采用的是
StringBuilder来实现的,而之前这个操作是采用StringBuffer实现的。

我们通过一个简单的程序来看其执行的流程:
publicclassBuffer{
publicstaticvoidmain(String[]args){
Strings1="aaaaa";
Strings2="bbbbb";
Stringr=null;
inti=3694;
r=s1+i+s2;
for(intj=0;i<10;j++){
r+="23124";
}
}
}
使用命令javap-cBuffer查看其字节码实现:

将清单1和清单2对应起来看,清单2的字节码中ldc指令即从常量池中加载“aaaaa”字符串到栈顶,istore_1将“aaaaa”存到变量1中,后面的一样,sipush是将一个短整型常量值(-32768~32767)推送至栈顶,这里是常量“3694”。
让我们直接看到13,1317是new了一个StringBuffer对象并调用其初始化方法,2021则是先通过aload_1将变量1压到栈顶,前面说过变量1放的就是字符串常量“aaaaa”,接着通过指令invokevirtual调用StringBuffer的append方法将“aaaaa”拼接起来,后续的24~30同理。最后在33调用StringBuffer的toString函数获得String结果并通过astore存到变量3中。
看到这里可能有人会说,“既然JVM内部采用了StringBuffer来连接字符串了,那么我们自己就不用用StringBuffer,直接用”+“就行了吧!“。是么?当然不是了。俗话说”存在既有它的理由”,让我们继续看后面的循环对应的字节码。
3742都是进入for循环前的一些准备工作,37,38是将j置为1。44这里通过if_icmpge将j与10进行比较,如果j大于10则直接跳转到73,也即return语句退出函数;否则进入循环,也即4766的字节码。这里我们只需看47到51就知道为什么我们要在代码中自己使用StringBuffer来处理字符串的连接了,因为每次执行“+”操作时jvm都要new一个StringBuffer对象来处理字符串的连接,这在涉及很多的字符串连接操作时开销会很大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • Tip:笔者马上毕业了,准备开始 Java 的进阶学习计划。于是打算先从 String 类的源码分析入手,作为后面...
    石先阅读 11,988评论 16 58
  • 使用Java语言进行编程,我们每天都要用到String类,但是以前只是拿来就用,并不知道String类的实现原理和...
    lunabird阅读 429评论 0 1
  • 【七月影语】20170914学习力践行Day116 1.和舅舅家一起长大的小哥哥视频,给哥哥背古诗《敕勒歌》《所见...
    暖小柒阅读 159评论 0 0
  • 火车上睡了一觉,梦见乱七八糟的东西 头疼
    达浪打啦阅读 126评论 0 0