- 浏览: 905194 次
- 性别:
- 来自: 武汉
-
文章分类
最新评论
-
小宇宙_WZY:
膜拜一下大神,解决了我一个大问题,非常感谢 orz
【解惑】深入jar包:从jar包中读取资源文件 -
JKL852qaz:
感谢,遇到相同的问题!
【解惑】深入jar包:从jar包中读取资源文件 -
lgh1992314:
为什么java中调用final方法是用invokevirtua ...
【解惑】Java动态绑定机制的内幕 -
鲁曼1991:
说的都有道理,protected只能被同一级包的类所调用
【解惑】真正理解了protected的作用范围 -
鲁曼1991:
...
【总结】String in Java
作者:每次上网冲杯Java时,都能看到关于String无休无止的争论。还是觉得有必要让这个讨厌又很可爱的String美眉,赤裸裸的站在我们这些Java色狼面前了。嘿嘿....
众所周知,String是由字符组成的串,在程序中使用频率很高。Java中的String是一个类,而并非基本数据类型。 不过她却不是普通的类哦!!!
【镜头1】 String对象的创建
1、关于类对象的创建,很普通的一种方式就是利用构造器,String类也不例外:String s=new String("Hello world"); 问题是参数"Hello world"是什么东西,也是字符串对象吗?莫非用字符串对象创建一个字符串对象?
2、当然,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文件内容及常量池 》
public class HelloWorld{ void hello(){ System.out.println("Hello world"); } }
通过上图可见,代码中的"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()方法来使得一个常规字符串对象成为拘留字符串对象。我们会在后面介绍这个方法的。
★
操作码助忆符指令
有了上面阐述的两个知识前提,下面我们将根据二进制指令来区别两种字符串对象的创建方式:
(1) String s=new String("Hello world");编译成class文件后的指令(在myeclipse中查看):
0 new java.lang.String [15] //在堆中分配一个String类对象的空间,并将该对象的地址堆入操作数栈。 3 dup //复制操作数栈顶数据,并压入操作数栈。该指令使得操作数栈中有两个String对象的引用值。 4 ldc <String "Hello world"> [17] //将常量池中的字符串常量"Hello world"指向的堆中拘留String对象的地址压入操作数栈 6 invokespecial java.lang.String(java.lang.String) [19] //调用String的初始化方法,弹出操作数栈栈顶的两个对象地址,用拘留String对象的值初始化new指令创建的String对象,然后将这个对象的引用压入操作数栈 9 astore_1 [s] // 弹出操作数栈顶数据存放在局部变量区的第一个位置上。此时存放的是new指令创建出的,已经被初始化的String对象的地址。
事实上,在运行这段指令之前,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文件后的指令:
0 ldc <String "Hello world"> [15]//将常量池中的字符串常量"Hello world"指向的堆中拘留String对象的地址压入操作数栈 2 astore_1 [str] // 弹出操作数栈顶数据存放在局部变量区的第一个位置上。此时存放的是拘留字符串对象在堆中的地址
和上面的创建指令有很大的不同,局部变量s存储的是早已创建好的拘留字符串的堆地址。 大家好好想想,如果还有一条穿件语句String s1="Hello word";此时堆中有几个值为"Hello world"的字符串呢?答案是1个。那么局部变量s与s1存储的地址是否相同呢? 呵呵, 这个你应该知道了吧。
★ 镜头总结: String类型脱光了其实也很普通。真正让她神秘的原因就在于CONSTANT_String_info常量表 和拘留字符串对象 的存在。现在我们可以解决江湖上的许多纷争了。
【 纷争1】关于字符串相等关系的争论
//代码1 String sa=new String("Hello world"); String sb=new String("Hello world"); System.out.println(sa==sb); // false //代码2 String sc="Hello world"; String sd="Hello world"; System.out.println(sc==sd); // true
代码1中局部变量sa,sb中存储的是JVM在堆中new出来的两个String对象的内存地址。虽然这两个String对象的值(char[]存放的字符序列)都是"Hello world"。 因此"=="比较的是两个不同的堆地址。代码2中局部变量sc,sd中存储的也是地址,但却都是常量池中"Hello world"指向的堆的唯一的那个拘留字符串对象的地址 。自然相等了。
【纷争2】 字符串“+”操作的内幕
//代码1 String sa = "ab"; String sb = "cd"; String sab=sa+sb; String s="abcd"; System.out.println(sab==s); // false //代码2 String sc="ab"+"cd"; String sd="abcd"; System.out.println(sc==sd); //true
代码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 public final class String { private final char value[]; public String(String original) { // 把原字符串original切分成字符数组并赋给value[]; } } //StringBuffer public final class StringBuffer extends AbstractStringBuilder { char value[]; //继承了父类AbstractStringBuilder中的value[] public StringBuffer(String str) { super(str.length() + 16); //继承父类的构造器,并创建一个大小为str.length()+16的value[]数组 append(str); //将str切分成字符序列并加入到value[]中 } }
很显然,String和StringBuffer中的value[]都用于存储字符序列。但是,
(1) String中的是常量(final)数组,只能被赋值一次。
比如:new String("abc")使得value[]={'a','b','c'},之后这个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所运行的时间。
//测试代码 public class RunTime{ public static void main(String[] args){ ● 测试代码位置1 long beginTime=System.currentTimeMillis(); for(int i=0;i<10000;i++){ ● 测试代码位置2 } long endTime=System.currentTimeMillis(); System.out.println(endTime-beginTime); } }
(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存放的是两个不同的拘留字符串对象的地址。然后会通过下面三个步骤完成“+连接”:
1、StringBuilder temp=new StringBuilder(s1),
2、temp.append(s2);
3、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值加入常量池中,并创建一个新的拘留字符串对象。
评论
<div class="quote_div">
<p>作者:Java标准类库有几千个类,唯独String不太一样。为什么这么说?就因为每次上网冲杯Java时,都能看到关于String无休无止的争论。<span style="color: #ff0000;">还是觉得有必要让这个讨厌又很可爱的String美眉,赤裸裸的站在我们这些Java色狼面前了。嘿嘿....</span><br></p>
<p> </p>
</div>
<p> </p>
<p> </p>
<p><span style="color: #800000; font-size: small;"><strong>讨论的议题有意义!但措辞能不能别让人老觉得咱们Programmer都是SL呀。</strong><br></span></p>
我用的是电子版的(中文的)。如果你的英文不错,建议看英文。中文的翻译还是有不少地方很蹩脚。比如拘留字符串。实在是看的哭笑不得
指正楼主:
ldc指令 的作用是把常量压入栈
invokespecial指令 是弹出栈顶对象,然后执行出栈对象的指定方法
支持,我对JVM的指令集合了解不深,主要是对JVM的体系结构还没有了解。所以我在解析class指令的时候只是大概说说意思,指令详细的含义我也不很清楚。
没有系统的JVM的知识,所以扒衣服的时候不免扒破了,大伙多批评,多指正。
学习啦
指正楼主:
ldc指令 的作用是把常量压入栈
invokespecial指令 是弹出栈顶对象,然后执行出栈对象的指定方法
<div class="quote_div">
<p>作者:Java标准类库有几千个类,唯独String不太一样。为什么这么说?就因为每次上网冲杯Java时,都能看到关于String无休无止的争论。还是觉得有必要让这个讨厌又很可爱的String美眉,赤裸裸的站在我们这些Java色狼面前了。嘿嘿....<br><br>众所周知,String是由字符组成的串,在程序中使用频率很高。Java中的String是一个类,而并非基本数据类型。 不过她却不是普通的类哦!!!</p>
<p> </p>
<p><span style="color: #800000;"><strong>【镜头1】 String对象的创建</strong> </span><br><br> 1、关于类对象的创建,很普通的一种方式就是利用构造器,String类也不例外:<br> String s=new String("Hello world");<br> 问题:参数"Hello world"是什么东西,也是字符串对象吗?莫非用字符串对象创建一个字符串对象???<br><br> 2、当然,String类对象还有一种大家都很喜欢的创建方式:<br> String s="Hello world";<br> 问题:有点怪呀,怎么与基本数据类型的赋值操作(int i=1)很像呀???<br><br>在开始解释这些问题之前,我们先引入一些必要的知识:<br><strong><span style="color: #800000;">(1) Java class文件结构</span> </strong><br> 我们都知道,Java程序要运行,首先需要编译器将源代码文件编译成字节码文件(也就是.class文件)。然后在由JVM解释执行。<br> class文件是8位字节的<span style="color: #800000;">二进制流</span> 。这些二进制流的涵义由一些<span style="color: #800000;">紧凑的有意义的项</span> 组成。比如class字节流中最开始的4个字节组成的项叫做<span style="color: #800000;">魔数</span> (magic),其意义在于分辨class文件(值为0xCAFEBABE)与非class文件。class字节流大致结构如下图左侧。</p>
<p> <img style="vertical-align: middle;" src="http://dl.iteye.com/upload/picture/pic/50015/1f7bd098-2b41-3f95-b129-460190f351ba.gif" alt="" width="366" height="258"></p>
<p> 其中,在class文件中有一个非常重要的项——<span style="color: #ff0000;">常量池</span> <strong></strong>。这个常量池专门放置源代码中的常量信息(并且不同的常量存放在不同标志的常量表中)。如上图右侧是HelloWorld代码中的常量表(HelloWorld代码如下),其中有四个不同类型的常量表(四个不同的常量池入口)。关于常量池的具体细节,请参照《深入Java虚拟机》第二版第6章。</p>
<pre name="code" class="java">public class HelloWorld{
void hello(){
System.out.println("Hello world");
}
}</pre>
<p> 显然,HelloWorld代码中的"Hello world"被编译之后,可以清楚的看到存放在了class二进制流的常量池项中(上图右侧红框区域)。并且我们还发现常量池中专门有为<span style="color: #ff0000;">String类型设置的常量表</span> <strong></strong>。也就是说,<span style="color: #0000ff;">在编译阶段,就已经将代码中的这种("****")形式作为了字符串常量存放在常量池中了</span> ,这一点和下面代码中出现的整形常量(142),浮点型常量(12.1)等的处理是没有区别的。</p>
<p> String s="Hello world";<br> int intData=142;<br> double dblData=12.1;</p>
<p><strong><span style="color: #800000;"><br></span></strong></p>
<p><strong><span style="color: #800000;">(2) Java虚拟机运行class文件</span> </strong><br> 当Java虚拟机需要运行一个class文件时,它首先会用类装载器装载进class文件。当然也就需要在内存中存放许多东西。比如class的二进制字节码。还有需要存储class文件中得到的其他信息,比如程序创建的对象,传递给方法的参数,返回值,局部变量等等。怎么多麻烦的数据当然需要管理,JVM会把这些东西都组织到几个“<span style="color: #800000;">运行时数据区</span> ”中。这些数据区中就有我们动不动就谈到的"堆"呀,"栈"呀什么的?想要详细了解这部分东西可以看《深入Java虚拟机》第二版第5章。<br> <br> 在这里我只谈谈“<span style="color: #ff0000;">方法区</span> ”这个运行时数据区。在Java虚拟机中,关于被装载类型的信息会在一个逻辑上被称为"方法区"的内存中,当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入该文件。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区中。<br> <br> 方法区中的这些类型信息是很有用的,比如:这个类型的全限定名(net.single.HelloWorld);这个类型的直接超类的全限定名;这个类型是类类型还是接口类型;这个类型的访问修饰符(public,final,static)等。还有两个大家都很熟悉的引用:指向类ClassLoader的引用和指向Class类的引用。这是Java反射机制能够运行的关键所在。这里我们要提到的是一个非常重要的信息——<span style="color: #ff0000;">该类型的常量池</span> <strong></strong>。<br><br> 上面提到的,class文件结构中的常量池二进制流就被JVM存储在方法区中进行管理。当程序运行时需要使用到常量值的时候,直接在方法区常量池所在的内存中寻找就可以了。</p>
<p> </p>
<p><strong><span style="color: #800000;">(3) 操作码助忆符指令集</span> </strong><br> 将String s=new String("Hello world");编译成class文件后的指令(由eclipse打开class文件查看的):</p>
<pre name="code" class="Class字节码指令集">0 new java.lang.String [15]
3 dup
4 ldc <String "Hello word"> [17]
6 invokespecial java.lang.String(java.lang.String) [19]
9 astore_1 [s]
10 retur</pre>
<p> 下面通俗的解释一下这些指令,详细见《深入Java虚拟机》第二版附表:按操作码助忆符排列的指令集。<br> ★ new指令: 在内存的堆区域中为新字符串对象分配足够大的空间,并将对象的实例变量设为默认值。<br> ★ ldc指令:在内存的方法区常量池中找到<span style="color: #ff0000;">String类型字面值常量表</span> 的入口,然后定位到的"Hello word"所在内存中的位置。<br> ★ invokespecial指令:调用指定的类构造器(这里调用的是String(String)这一个构造器。将ldc指令所找到的"Hello word"的内容传入到new指令所开辟在堆中的字符串对象中。<br> ★ astore_1:<span style="background-color: #ffffff; color: #0000ff;">将new指令所开辟堆的内存位置存入局部变量s中</span> 。</p>
<p> </p>
<p> 将String s="Hello world";编译成class文件后的指令:</p>
<pre name="code" class="Class字节码指令集">0 ldc <String "Hello world"> [15]
2 astore_1 [str]
3 return</pre>
<p> ★ ldc指令:在内存的方法区常量池中找到<span style="color: #ff0000;">String类型字面值常量表</span> 的入口,然后定位到的"Hello word"所在内存中的位置(如果常量池中没有"Hello word",则会在其中添加一个"Hello word")。<br> ★ astore_1:<span style="color: #0000ff;">将ldc指令定位到的常量池中的位置存入局部变量s中</span> 。</p>
<p> </p>
<p><strong><span style="color: #800000;">镜头总结:</span> </strong>String类型脱光了其实也很普通。真正让她神秘的原因就在于<span style="color: #ff0000;">String类型字面值常量表</span> 的存在。</p>
<p> </p>
<p><strong><span style="color: #800000;">相关问题解决 </span></strong></p>
<p> (问题1) 代码1 代码2</p>
<p> String sa=new String("Hello world"); String sc="Hello world";<br> String sb=new String("Hello world"); String sd="Hello world";<br> System.out.println(sa==sb); // false System.out.println(sc==sd); // true <br> 变量sa,sb中存储的内容是JVM在堆中开辟的两个String对象的内存地址。==比较就是sa,sb变量存储的内容,也就是两个不同的内存地址,当然是false;<br> 变量sc,sd中存储的内容也是地址,但却都是方法区常量池中"Hello word"所在的地址,自然一样。</p>
<p> (问题2) 代码1 代码2<br> String sa = "ab"; String sc="ab"+"cd";<br> String sb = "cd"; String sd="abcd";<br> String sab=sa+sb; System.out.println(sc==sd); //true<br> String s="abcd";<br> System.out.println(sab==s); // false<br> 代码1中sa+sb被编译以后使用的是StringBuilder.append(String)方法。JVM会在堆中创建一个StringBuilder类,将sa所指向常量池中的内容"ab"传入,然后调用append(sb所指向的常量池内容)完成字符串合并功能,最后将堆中StringBuilder对象的地址赋给变量sab。而s存储的是常量池中"abcd"的地址。sab与s地址当然不一样了。<br> 代码2中"ab"+"cd"会直接在编译阶段就合并成常量"abcd",所以相同的字符串在常量池中的地址也相同了。</p>
</div>
<p> </p>
请看下例子
public class StringTest { public static void main(String[] args) { String s1 = "a"; String s2 = "a2"; String s3 = s1+s2; String s4 = "aa2"; System.out.println(s3==s4); } }
StringTest的class文件
public static void main(java.lang.String[] args); 0 ldc <String "a"> [16] 2 astore_1 [s1] 3 ldc <String "a2"> [18] 5 astore_2 [s2] 6 new java.lang.StringBuilder [20] 9 dup 10 aload_1 [s1] 11 invokestatic java.lang.String.valueOf(java.lang.Object) : java.lang.String [22] 14 invokespecial java.lang.StringBuilder(java.lang.String) [28] 17 aload_2 [s2] 18 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder[31] 21 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [35] 24 astore_3 [s3] 25 ldc <String "aa2"> [39] 27 astore 4 [s4] 29 getstatic java.lang.System.out : java.io.PrintStream [41] 32 aload_3 [s3] 33 aload 4 [s4] 35 if_acmpne 42 38 iconst_1 39 goto 43 42 iconst_0 43 invokevirtual java.io.PrintStream.println(boolean) : void [47] 46 return
18 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder[31]
请注意红色标注,谢谢LZ分享!
1、class文件是二进制字节流。其结构是按顺序组成的有意义的项。比如说class文件字节流中最开始的4个字节是magic项,后面4个字节是主、次Version项,然后是Constant pool常量池项.... 。字节流逻辑图就是(http://dl.iteye.com/upload/picture/pic/50015/1f7bd098-2b41-3f95-b129-460190f351ba.gif)左侧部分。
2、所谓“四个不同类型的常量表”,是指在这篇文章中我用HelloWord代码做例子,这段代码的.class文件在常量池中有四种不同类型的常量表组成。其实常量表的种类远不只这些。在《深入Java虚拟机》第二版第6章P124有详细说明。这里帖点出来:
【表 常量池标志】
入口类型 标志值 描述
CONSTANT_Utf8 1 UTF-8编码的字符串
CONSTANT_Integer 3 int类型字面值
CONSTANT_Float 4 float类型字面值
CONSTANT_Long 5 long类型字面值
CONSTANT_Double 6 double类型字面值
CONSTANT_Class 7 对一个类或接口的符号引用
CONSTANT_String 8 String类型字面值
CONSTANT_Fieldref 9 对一个字段的符号引用
CONSTANT_Methodref 10 对一个类中声明的方法的符号引用
CONSTANT_InterfaceMethodref 11 对一个接口中声明的方法的符号引用
CONSTANT_NameAndType 12 对一个字段或方法的部分符号引用
昨天网络不行,没看到图。现已看到图,清楚多了,谢谢博主。
钻研精神&&分享心理一般是大师所有。
楼主在武汉啦?哪里啊
发表评论
-
NIO
2010-08-05 10:36 0在JDK1.4以前,I/O输入输出处理,我们把它称为旧 ... -
【总结】Java线程同步机制深刻阐述
2010-05-16 10:21 6082全文转载:http://www.iteye ... -
【JDK优化】java.util.Arrays的排序研究
2010-05-12 21:06 9269作者题记:JDK中有很多算法具有优化的闪光点,值得好好研究。 ... -
【JDK优化】 Integer 自动打包机制的优化
2010-03-12 19:14 4298我们首先来看一段代码: Integer i=100; In ... -
【总结】Java与字符编码问题详谈
2009-12-30 09:11 9613一、字符集和字符编码方式 计算机只懂得0/1两种信号 ... -
【解惑】 正确理解线程等待和释放(wait/notify)
2009-12-29 13:40 19879对于初学者来说,下面这个例子是一个非常常见的错误。 /** ... -
【解惑】JVM如何理解Java泛型类
2009-12-16 11:08 12489//泛型代码 public class Pair<T& ... -
【解惑】正确的理解this 和 super
2009-12-05 09:46 4546转载: 《无聊 ... -
【解惑】真正理解了protected的作用范围
2009-11-21 18:00 5144一提到访问控 ... -
【解惑】真正理解了protected的作用范围
2009-11-16 17:11 585一提到访问控制符protected,即使是初学者 ... -
总结Java标准类库中类型相互转化的方法
2009-11-09 21:57 210组一: ☆ String → byte[ ... -
方法没覆盖住带来的烦恼
2009-11-05 09:18 100Object类是所有类的祖宗,它的equals方法比较的 ... -
【解惑】数组向上转型的陷阱
2009-11-03 11:44 1928问题提出: 有两个类Manager和Em ... -
【总结】java命令解析以及编译器,虚拟机如何定位类
2009-11-01 16:25 5906学Java有些日子了,一直都使用IDE来写程序。这 ... -
【解惑】剖析float型的内存存储和精度丢失问题
2009-10-26 15:10 16278问题提出:12.0f-11.9f=0.10 ... -
【解惑】领略内部类的“内部”
2009-10-19 15:38 3653内部类有两种情况: (1) 在类中定义一个类(私有内部类 ... -
【解惑】深入jar包:从jar包中读取资源文件
2009-10-08 21:13 66204我们常常在代码中读取一些资源文件(比如图片,音乐,文 ... -
【解惑】理解java枚举类型
2009-09-26 09:37 3512枚举类型是JDK5.0的新特征。Sun引进了一个全新的关键字e ... -
编写自己的equals方法
2009-09-20 14:18 129在我的《令人头疼的"相等"关 ... -
【解惑】Java类型间的转型
2009-09-11 16:03 5728★ 基本数据类型间的转换 1、Java要做到平台无关 ...
相关推荐
### Java字符串操作详解:String1.java程序分析 在Java编程语言中,字符串处理是一项非常重要的技能,无论是进行数据处理还是用户交互,字符串都是一个不可或缺的数据类型。本篇将基于提供的`String1.java`代码示例...
### Java的String用法类型总结 #### 一、概述 在Java编程语言中,`String` 类是最常用的数据类型之一,用于表示不可变的字符序列。由于字符串在实际开发中的重要性和高频使用特性,深入理解并掌握其用法至关重要。...
JAVA使用ElasticSearch查询in和not in的实现方式 Elasticsearch是一个基于Lucene的搜索服务器,提供了一个...本文总结了使用Elasticsearch查询in和not in的实现方式,希望对大家的学习或者工作具有一定的参考价值。
12.4.4 String和StringBuffer类 12.4.5 字串的特殊性 12.5 总结 12.6 练习 第13章 创建窗口和程序片 13.1 为何要用AWT? 13.2 基本程序片 13.2.1 程序片的测试 13.2.2 一个更图形化的例子 13.2.3 框架方法的演示 ...
《Thinking in Java》是一本深度解析Java编程语言的经典著作,其深入浅出的讲解和丰富的实例使得读者能够全面理解Java的精髓。以下是对书中部分关键知识点的总结: 1. **Java 泛型**:泛型是Java SE 5.0引入的重要...
### Algorithms in Java Source Code #### 章节一:简介 本章节主要介绍了一种用于实现快速连接(Quick Find)和优化过的快速联合(Quick Union with Weighting and Path Compression)算法的基本思想及其 Java ...
### Thinking in Java 自学笔记——第二章 一切皆对象 #### 重要概念解析 ##### 2.1 用引用操纵对象 在Java中,一切都被视为对象,这意味着无论是字符串、数字还是其他数据类型都可以被视为对象来进行操作。当...
### TCP/IP Sockets in Java — 实用编程指南 #### 一、引言 《TCP/IP Sockets in Java — 实用编程指南》(第二版)是一本面向程序员的实用指南,旨在帮助读者掌握如何在Java中使用TCP/IP套接字进行网络编程。...
12.4.4 String和StringBuffer类 12.4.5 字串的特殊性 12.5 总结 12.6 练习 第13章 创建窗口和程序片 13.1 为何要用AWT? 13.2 基本程序片 13.2.1 程序片的测试 13.2.2 一个更图形化的例子 13.2.3 框架方法的演示 ...
`List<String>`作为Java中最常用的数据结构之一,其转换为`String[]`的需求非常普遍。 #### 二、基础知识 1. **List**: Java中的一种容器类型,用于存储有序的元素集合。其中`ArrayList`是最常用的实现方式之一。 ...
### 黑马程序员入学Java精华总结 #### 一、Java概述与基础知识 1. **何为编程?** - 编程是指通过编写计算机能够理解的指令来解决问题或完成特定任务的过程。这些指令通常被组织成算法,并使用某种编程语言实现。...
### Java基础学习总结 #### 一、Java基础知识 Java是一种广泛使用的高级编程语言,由Sun Microsystems于1995年发布。它具有平台独立性、安全性高、面向对象等特点,适用于开发各种应用程序。 ##### 1. IO流 IO流...
12.4.4 String和StringBuffer类 12.4.5 字串的特殊性 12.5 总结 12.6 练习 第13章 创建窗口和程序片 13.1 为何要用AWT? 13.2 基本程序片 13.2.1 程序片的测试 13.2.2 一个更图形化的例子 13.2.3 框架方法的演示 ...
### Java上机题目知识点总结 #### 一、二维数组的创建与填充 **知识点概述:** 本题目主要涉及Java中二维数组的创建、初始化及字符串的解析操作。 **详细解析:** 1. **二维数组创建:** - 首先,通过字符串参数...
### 知识点一:Java中的String与char类型转换 #### 概述 在Java编程语言中,`String` 类型和 `char` 类型是两种基本的数据类型。`String` 类型用于表示一系列字符的集合,而 `char` 类型则代表单个字符。有时我们...
本教程将基于标题"audio_java.rar_audio java_audio player in java"和描述中提到的两个Java音频播放程序,深入探讨如何使用Java来实现这一功能,并结合平台选择JBuilder进行开发。 首先,我们要了解Java中的音频...
获取String字符串中指定下标位置的char类型字符,如果index超出有效范围 StringIndexOutOfBoundsException int indexOf(char ch); int indexOf(String str); int indexOf(char ch, int fromIndex); int indexOf...
总结,将string数组转化为SQL `IN`条件是一个常见的需求,主要目的是方便在数据库查询中使用。上述两种方法都简单易懂,可以根据实际项目需求选择合适的方式。在实际开发中,确保安全性、效率和代码可维护性是至关...
### Think in Java 学习笔记知识点总结 #### 第1章:对象导论 - **一切皆为对象**:Java 中的几乎所有事物都是对象,对象通过发送消息的方式进行交互。 - **对象模型**:每个对象都有自己的存储空间,该空间由其他...