看到了以前2016.5月学习java写的笔记,这里放在一起。
String实现的细节原理分析
一、jdk源码中String 的实现
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
上面是源码中String的一部分,从中可以得到几个重要的信息:
- 首先,String类是final的,所以不能被继承;
- 再者,String在内存中的实现,是以一个字符数组char[]实现的,所以是一个连续存储的区域,而且字符数组是final的,所以String对象一旦被创建就不能被修改的;
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = new char[0];
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
上面是String 的部分构造方法,从中可以看出,其实就是对String外壳内部的字符数组的操作,String就像是一美丽的外套一样。
注意第二个构造方法,this.value=original.value,仅仅是赋值引用给新的字符串!但是我们的理解新的字符串应该是以前字符串的一个完整的副本才对啊。这里其实是java做的一个优化,当新的字符串要进行删除或者其他操作是才会真正创建一个字符串并且将内存地址赋值给刚刚创建的引用。
二、String中 == 和 equals()的区别
前两天一个考试,遇到了非常坑爹的字符串比较相等的问题。原题大概是如下:
public class TestString {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String(s1);
System.out.println(s1==s2);
System.out.println(s3==s4);
System.out.println(s1==s3);
System.out.println(s3.equals(s4));
}
}
当时其他题目都可以搞定,唯独这一题纠结了好久,当时想要是机考可以编译运行跑出来多好,哈哈。
这里的正确答案应该是:
true,false,false,true
这里我们先放下这个问题,先探讨一下==的实现,以及equals()的实现,回过头再来看这个问题。
- equals()的实现
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
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()首先比较是不是两个引用指向同一个对象,若是直接返回true;否则在带比较对象也是String类型的基础上比较串的长度,然后逐个比较字符。所以只要两个字符串内部的串,也就是字符序列是一样的,方法肯定返回true。
==的实现
大家都知道,java中保留了一些基本类型,比如int,float,是可以不已对象的形式保留在内存中的,数组也支持这些基础类型。而==是比较两个的两边如果是基础类型常量,也就是比较存在stack中常量引用是否指向常量池中相同大小的数值,如果==两边是基础类型变量,则是比较位于栈中的变量数值的是否相等。但是字符串比较奇怪,它是引用类型,但是在进行一些实验的时候又发现似乎具有一些基本类型的性质。这里我们来分析一下String类型的存储。-
String的存储
- 用常量字符串赋值给String引用
String s1 = "hello";
String s2 = "hello";
这里,首先编译期间,编译器检查到有字符串出现,就在常量池中查找,有没有引用指向堆中的一个String对象,并且该对象的字符序列等于“hello”,如果有则让s2也指向堆中同样的string对象,这就是为什么没有使用new符号的String对象也能使用String的各种方法了,应为常量池中不存在对象,所以从合格角度也说明字符串常量池中放的是字符串的引用;如果没有,就是s1创建的时候,就在堆中创建一个字符串对象,将引用放到常量池中,并且让s1指向堆中的对象s1。
String s3 = new String("hello");
String s4 = new String("hello");
注意这里是使用new来创建对象,是发生在运行期的事儿。当解释器发现有new的时候,果断的在堆中创建对象s3,并且让栈中的引用s3指向创建的对象,创建s4的时候也是这样,所以s3,s4指向堆中两个不同的对象,即使他们内容相同。
由上面的分析可知:
- 直接用一个字符串常量赋值给引用的s1和s2肯定是相等的,因为他们指向字符串常量池中相同的引用
- 而用new关键字生成的对象s3,s4肯定是s3!=s4的,因为他们指向heap中不同的对象,虽然这两个不同对象内部的字符数组是相等的
- s1,s3分别一个指向常量池,一个指向堆heap,肯定也是s1!=s3
- equals()的实现是在都是String对象基础上比较对象内部的内容,也就是字符数组
value[],所以肯定有s3.equals(s4)
三、从源代码中看不到的很多东西
字符串+的实现
public class Test {
public static void main(String[] args) {
String s1 = "hello" + "world";
String s2 = "good morning " + s1;
}
}
刚开始学习java从书上看到,java中不允许运算符重载机制,不想C++那样*。但是看到String这种引用类型都这样玩的时候我就慌了。后来知道可以去源码中看String的实现,也没有找着具体是怎么实现的。最近看了一些牛人的博客,才知道了一点点“内幕”。
编译之后在命令行中输入
javap -c Test
就会执行反编译,得到如下的JVM的汇编代码:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hellohello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String good morning
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}
看起来很复杂哈,不用掌握汇编语言,只用看右边的注释,可以发现加载了StringBuilder类,而且调用了两次append()方法。这就是编译器在搞鬼了。
java知道String的拼接太耗内存和时间,所以在这里默认的帮我们生成StringBuilder,将字符串连接好了之后,调用toString()生成字符串赋值给目标的引用就行了。合理的两个append()方法分别是为两个+操作符调用的。
然而这些在java类库的源码中永远找不到。
四、求教大牛解答
- java常量池中存放的到底是什么?
- 就拿字符串常量池来说,如果存放的是字符串常量,那为什么栈中的引用指向这个常量,还可以使用String的各种方法呢?难道是复制常量到堆中对象?岂不是很浪费吗?如果是存放引用的话,通过引用查找是否有相同字符串存在,开销好像也是很大的…
- 希望有大牛给予赐教,不甚感激。