String源码分析
从一段代码说起:
public void stringTest(){
String a = "a"+"b"+1;
String b = "ab1";
System.out.println(a == b);
}
大家猜一猜结果如何?如果你的结论是true。好吧,再来一段代码:
public void stringTest(){
String a = new String("ab1");
String b = "ab1";
System.out.println(a == b);
}
结果如何呢?正确答案是false。
让我们看看经过编译器编译后的代码如何
//第一段代码
public void stringTest() {
String a = "ab1";
String b = "ab1";
System.out.println(a == b);
}
//第二段代码
public void stringTest() {
String a1 = new String("ab1");
String b = "ab1";
System.out.println(a1 == b);
}
也就是说第一段代码经过了编译期优化,原因是编译器发现"a"+"b"+1和"ab1"的效果是一样的,都是不可变量组成。但是为什么他们的内存地址会相同呢?如果你对此还有兴趣,那就一起看看String类的一些重要源码吧。
一 String类
String类被final所修饰,也就是说String对象是不可变量,并发程序最喜欢不可变量了。String类实现了Serializable, Comparable, CharSequence接口。
Comparable接口有compareTo(String s)方法,CharSequence接口有length(),charAt(int index),subSequence(int start,int end)方法。
二 String属性
String类中包含一个不可变的char数组用来存放字符串,一个int型的变量hash用来存放计算后的哈希值。
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
三 String构造函数
//不含参数的构造函数,一般没什么用,因为value是不可变量
public String() {
this.value = new char[0];
}
//参数为String类型
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//参数为char数组,使用java.utils包中的Arrays类复制
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//从bytes数组中的offset位置开始,将长度为length的字节,以charsetName格式编码,拷贝到value
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
//调用public String(byte bytes[], int offset, int length, String charsetName)构造函数
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
三 String常用方法
boolean equals(Object anObject)
public boolean equals(Object anObject) {
//如果引用的是同一个对象,返回真
if (this == anObject) {
return true;
}
//如果不是String类型的数据,返回假
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
//如果char数组长度不相等,返回假
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//从后往前单个字符判断,如果有不相等,返回假
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
//每个字符都相等,返回真
return true;
}
}
return false;
}
equals方法经常用得到,它用来判断两个对象从实际意义上是否相等,String对象判断规则:
内存地址相同,则为真。
如果对象类型不是String类型,则为假。否则继续判断。
如果对象长度不相等,则为假。否则继续判断。
从后往前,判断String类中char数组value的单个字符是否相等,有不相等则为假。如果一直相等直到第一个数,则返回真。
由此可以看出,如果对两个超长的字符串进行比较还是非常费时间的。
int compareTo(String anotherString)
public int compareTo(String anotherString) {
//自身对象字符串长度len1
int len1 = value.length;
//被比较对象字符串长度len2
int len2 = anotherString.value.length;
//取两个字符串长度的最小值lim
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
//从value的第一个字符开始到最小长度lim处为止,如果字符不相等,返回自身(对象不相等处字符-被比较对象不相等字符)
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//如果前面都相等,则返回(自身长度-被比较对象长度)
return len1 - len2;
}
这个方法写的很巧妙,先从0开始判断字符大小。如果两个对象能比较字符的地方比较完了还相等,就直接返回自身长度减被比较对象长度,如果两个字符串长度相等,则返回的是0,巧妙地判断了三种情况。
int hashCode()
public int hashCode() {
int h = hash;
//如果hash没有被计算过,并且字符串不为空,则进行hashCode计算
if (h == 0 && value.length > 0) {
char val[] = value;
//计算过程
//s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
//hash赋值
hash = h;
}
return h;
}
String类重写了hashCode方法,Object中的hashCode方法是一个Native调用。String类的hash采用多项式计算得来,我们完全可以通过不相同的字符串得出同样的hash,所以两个String对象的hashCode相同,并不代表两个String是一样的。
boolean startsWith(String prefix,int toffset)
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
//如果起始地址小于0或者(起始地址+所比较对象长度)大于自身对象长度,返回假
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
//从所比较对象的末尾开始比较
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
起始比较和末尾比较都是比较经常用得到的方法,例如在判断一个字符串是不是http协议的,或者初步判断一个文件是不是mp3文件,都可以采用这个方法进行比较。
String concat(String str)
public String concat(String str) {
int otherLen = str.length();
//如果被添加的字符串为空,返回对象本身
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
concat方法也是经常用的方法之一,它先判断被添加字符串是否为空来决定要不要创建新的对象。
String replace(char oldChar,char newChar)
public String replace(char oldChar, char newChar) {
//新旧值先对比
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
//找到旧值最开始出现的位置
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
//从那个位置开始,直到末尾,用新值代替出现的旧值
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
这个方法也有讨巧的地方,例如最开始先找出旧值出现的位置,这样节省了一部分对比的时间。replace(String oldStr,String newStr)方法通过正则表达式来判断。
String trim()
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
//找到字符串前段没有空格的位置
while ((st < len) && (val[st] <= ' ')) {
st++;
}
//找到字符串末尾没有空格的位置
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果前后都没有出现空格,返回字符串本身
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
trim方法用起来也6的飞起
String intern()
public native String intern();
intern方法是Native调用,它的作用是在方法区中的常量池里通过equals方法寻找等值的对象,如果没有找到则在常量池中开辟一片空间存放字符串并返回该对应String的引用,否则直接返回常量池中已存在String对象的引用。
将引言中第二段代码
//String a = new String("ab1");
//改为
String a = new String("ab1").intern();
则结果为为真,原因在于a所指向的地址来自于常量池,而b所指向的字符串常量默认会调用这个方法,所以a和b都指向了同一个地址空间。
int hash32()
private transient int hash32 = 0;
int hash32() {
int h = hash32;
if (0 == h) {
// harmless data race on hash32 here.
h = sun.misc.Hashing.murmur3_32(HASHING_SEED, value, 0, value.length);
// ensure result is not zero to avoid recalcing
h = (0 != h) ? h : 1;
hash32 = h;
}
return h;
}
在JDK1.7中,Hash相关集合类在String类作key的情况下,不再使用hashCode方式离散数据,而是采用hash32方法。这个方法默认使用系统当前时间,String类地址,System类地址等作为因子计算得到hash种子,通过hash种子在经过hash得到32位的int型数值。
public int length() {
return value.length;
}
public String toString() {
return this;
}
public boolean isEmpty() {
return value.length == 0;
}
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
以上是一些简单的常用方法。
总结
String对象是不可变类型,返回类型为String的String方法每次返回的都是新的String对象,除了某些方法的某些特定条件返回自身。
String对象的三种比较方式:
==内存比较:直接对比两个引用所指向的内存值,精确简洁直接明了。
equals字符串值比较:比较两个引用所指对象字面值是否相等。
hashCode字符串数值化比较:将字符串数值化。两个引用的hashCode相同,不保证内存一定相同,不保证字面值一定相同。
String str="hello world"和String str=new String("hello world")的区别
public
class
Main {
public
static
void
main(String[] args) {
String str1 =
"hello world"
;
String str2 =
new
String(
"hello world"
);
String str3 =
"hello world"
;
String str4 =
new
String(
"hello world"
);
System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str2==str4);
}
}
在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。因此在上述代码中,String str1 = "hello world";和String str3 = "hello world"; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的.
String、StringBuffer以及StringBuilder的区别
public
class
Main {
public
static
void
main(String[] args) {
String string =
""
;
for
(
int
i=
0
;i<
10000
;i++){
string +=
"hello"
;
}
}
}
这句 string += "hello";的过程相当于将原有的string变量指向的对象内容取出与"hello"作字符串相加操作再存进另一个新的String对象当中,再让string变量指向新生成的对象。如果大家还有疑问可以反编译其字节码文件便清楚了:
从这段反编译出的字节码文件可以很清楚地看出:从第8行开始到第35行是整个循环的执行过程,并且每次循环会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象。也就是说这个循环执行完毕new出了10000个对象,试想一下,如果这些对象没有被回收,会造成多大的内存资源浪费。从上面还可以看出:string+="hello"的操作事实上会自动被JVM优化成:
StringBuilder str = new StringBuilder(string);
str.append("hello");
str.toString();
再看下面这段代码:
1
2
3
4
5
6
7
8
9
|
public
class
Main {
public
static
void
main(String[] args) {
StringBuilder stringBuilder =
new
StringBuilder();
for
(
int
i=
0
;i<
10000
;i++){
stringBuilder.append(
"hello"
);
}
}
}
|
反编译字节码文件得到:
从这里可以明显看出,这段代码的for循环式从13行开始到27行结束,并且new操作只进行了一次,也就是说只生成了一个对象,append操作是在原有对象的基础上进行的。因此在循环了10000次之后,这段代码所占的资源要比上面小得多。
事实上,StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。public
StringBuilder insert(
int
index,
char
str[],
int
offset,
int
len)
{
super
.insert(index, str, offset, len);
return
this
;
}
public
synchronized
StringBuffer insert(
int
index,
char
str[],
int
offset,
int
len)
{
super
.insert(index, str, offset, len);
return
this
;
|
public
class
Main {
private
static
int
time =
50000
;
public
static
void
main(String[] args) {
testString();
testStringBuffer();
testStringBuilder();
test1String();
test2String();
}
public
static
void
testString () {
String s=
""
;
long
begin = System.currentTimeMillis();
for
(
int
i=
0
; i<time; i++){
s +=
"java"
;
}
long
over = System.currentTimeMillis();
System.out.println(
"操作"
+s.getClass().getName()+
"类型使用的时间为:"
+(over-begin)+
"毫秒"
);
}
public
static
void
testStringBuffer () {
StringBuffer sb =
new
StringBuffer();
long
begin = System.currentTimeMillis();
for
(
int
i=
0
; i<time; i++){
sb.append(
"java"
);
}
long
over = System.currentTimeMillis();
System.out.println(
"操作"
+sb.getClass().getName()+
"类型使用的时间为:"
+(over-begin)+
"毫秒"
);
}
public
static
void
testStringBuilder () {
StringBuilder sb =
new
StringBuilder();
long
begin = System.currentTimeMillis();
for
(
int
i=
0
; i<time; i++){
sb.append(
"java"
);
}
long
over = System.currentTimeMillis();
System.out.println(
"操作"
+sb.getClass().getName()+
"类型使用的时间为:"
+(over-begin)+
"毫秒"
);
}
public
static
void
test1String () {
long
begin = System.currentTimeMillis();
for
(
int
i=
0
; i<time; i++){
String s =
"I"
+
"love"
+
"java"
;
}
long
over = System.currentTimeMillis();
System.out.println(
"字符串直接相加操作:"
+(over-begin)+
"毫秒"
);
}
public
static
void
test2String () {
String s1 =
"I"
;
String s2 =
"love"
;
String s3 =
"java"
;
long
begin = System.currentTimeMillis();
for
(
int
i=
0
; i<time; i++){
String s = s1+s2+s3;
}
long
over = System.currentTimeMillis();
System.out.println(
"字符串间接相加操作:"
+(over-begin)+
"毫秒"
);
}
}
public
class
Main {
private
static
int
time =
50000
;
public
static
void
main(String[] args) {
testString();
testOptimalString();
}
public
static
void
testString () {
String s=
""
;
long
begin = System.currentTimeMillis();
for
(
int
i=
0
; i<time; i++){
s +=
"java"
;
}
long
over = System.currentTimeMillis();
System.out.println(
"操作"
+s.getClass().getName()+
"类型使用的时间为:"
+(over-begin)+
"毫秒"
);
}
public
static
void
testOptimalString () {
String s=
""
;
long
begin = System.currentTimeMillis();
for
(
int
i=
0
; i<time; i++){
StringBuilder sb =
new
StringBuilder(s);
sb.append(
"java"
);
s=sb.toString();
}
long
over = System.currentTimeMillis();
System.out.println(
"模拟JVM优化操作的时间为:"
+(over-begin)+
"毫秒"
);
}
}
1)对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I"+"love"+"java"; 的字符串相加,在编译期间便被优化成了"Ilovejava"。这个可以用javap -c命令反编译生成的class文件进行验证。
对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。
2)String、StringBuilder、StringBuffer三者的执行效率:
StringBuilder > StringBuffer > String
当然这个是相对的,不一定在所有情况下都是这样。
比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。
因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:
当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。
String a = "hello2";
String b = "hello" + 2;
System.out.println((a == b));
输出结果为:true。原因很简单,"hello"+2在编译期间就已经被优化成"hello2",因此在运行期间,变量a和变量b指向的是同一个对象
String a = "hello2";
String b = "hello";
String c = b + 2;
System.out.println((a == c));
输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的。因此a和c指向的并不是同一个对象。
String a = "hello2";
final String b = "hello";
String c = b + 2;
System.out.println((a == c));
输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2;
public
class
Main {
public
static
void
main(String[] args) {
String a =
"hello2"
;
final
String b = getHello();
String c = b +
2
;
System.out.println((a == c));
}
public
static
String getHello() {
return
"hello"
;
}
}
public
class
Main {
public
static
void
main(String[] args) {
String a =
"hello"
;
String b =
new
String(
"hello"
);
String c =
new
String(
"hello"
);
String d = b.intern();
System.out.println(a==b);
System.out.println(b==c);
System.out.println(b==d);
System.out.println(a==d);
}
}
String str = new String("abc")创建了多少个对象?
这个问题在很多书籍上都有说到比如《Java程序员面试宝典》,包括很多国内大公司笔试面试题都会遇到,大部分网上流传的以及一些面试书籍上都说是2个对象,这种说法是片面的。
首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建2个对象么?毫无疑问不可能,用javap -c反编译即可得到JVM执行的字节码内容: 首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建2个对象么?毫无疑问不可能,用javap -c反编译即可得到JVM执行的字节码内容:而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念 该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个
public
class
Main {
public
static
void
main(String[] args) {
String str1 =
"I"
;
//str1 += "love"+"java"; 1)
str1 = str1+
"love"
+
"java"
;
//2)
}
}
1)的效率比2)的效率要高,1)中的"love"+"java"在编译期间会被优化成"lovejava",而2)中的不会被优化。
可以看出,在1)中只进行了一次append操作,而在2)中进行了两次append操作。