String是我们其中用的最多的一个类,但是我们有很多细节我们可能没有去深深去研究,这里主要通过阅读源码去了解这个类。了解这个类我们分三个步骤去研究:
1.String类信息,修饰的类关键字以及实现的接口,继承的类,实现的接口实现了什么样的规范,父类主要完成什么方法;
2.基本类属性,包括静态属性,以及普通的属性;
3.方法,可以了解这个类的用法,实现避免一些不必要的坑。
1.类属性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** 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类被final这个关键字修饰了,说明String是不可以继承的。这个时候我们就会有疑问这里为什么用final修饰,其实简单来讲就是两点:安全和效率。安全,首先这个基本类提供给用户使用,就是希望这个类不轻易的而改变,容易出现不同的问题,还有点String还有native方法,不暴露成员变量。final关键字详解
然后看这个String继承的接口:java.io.Serializable, Comparable<String>, CharSequence。Serializable这个接口被继承的类可以被序列化以及反序列化; CharSequence主要提供一些对字符串序列一些只读的方法规范,例如,获取字符串长度,以及索引值的字符等;Comparable<String>主要定义一个类的大小比较规范。我们可以大概看下实现方式:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
我们可以熊源码中总结以下结论:1.先按照字符串优先逐个比较字符大小,如果遇到不一样的字符,就直接判断两个不同的字符,直到最小串结束;2.然后比较两个字符串的长度
2.成员属性
从源码中我们可以看到四个属性,两个静态属性:serialVersionUID,serialPersistentFields ,这两个属性和继承的Serializable的接口有关系,这个serialVersionUID主要用来在JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException) /** 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;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
还有两个普通的成员hash以及private final char value[]。首先了解一下这个hash的使用,这个hash值的只有在两个地方才会初始化,一个是被其他的String构造出新的String时候;还有一个是在第一次调用hashCode方法的时候才会“懒加载”生成。 public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
我们这里有一个疑问就是为什么要以31作为一个权值呢??详情可以看下这个博客String为啥用31作为权值然后我们主要看下value这个值被final修饰,也就意味着一旦String对象生成,value指向就是不可变的,这里的不可变不是绝对的不可变,我们可以通过反射修改内存数据:
final String a="ab";
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
char[] o = (char[])value.get(a);
o[0]='b';
o[1]='A';
System.out.println(a);
除下使用反射去改变内存情况,,一个String生成就不会发生改变了,因此subString等一些写操作基本都是new一个新的String对象(返回的String和this的一样时返回本身),
3.String内存分配
其他的方法就不一一介绍,接下来介绍一下String和虚拟机的内存模型的关系。其实要你有了解过jvm(jdk1.7),jvm对String创建进行一个优化,就是使用字符串常量池,String使用一个字符串的时候,jvm首先会检查运行时常量区,是否有String对象,如果就返回这个对象的引用,否则实例化并且把该String放入字符串常量区,结论:在字符串常量区,不可能出现两份及以上的同样的String实例。这样有什么好处呢?这样的可以大大减少程序使用的内存。我们知道String是不变的(引用不可变),一份同样的String没有必要创建两份,变这样既减少了对象创建的时间以及使用的内存。接下来通过几String创建分析内存的分布。
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s="test";
char test[]={'t','e','s','t'};
String te="te";
String st="st";
String s1=new String(s);
String s2="te"+"st";
String s3="te"+new String("st");
String s4=new String(test);
String s5=te+st;
if (s==s1){
System.out.println("s==s1");
}
if (s==s2){
System.out.println("s==s2");
}
if (s==s3){
System.out.println("s==s3");
}
if (s==s4){
System.out.println("s==s4");
}
if (s==s5){
System.out.println("s==s5");
}
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);//查看value的分配方式
char[] sValue = (char[]) valueField.get(s);
char[] s1Value = (char[]) valueField.get(s1);
char[] s2Value = (char[]) valueField.get(s2);
char[] s3Value = (char[]) valueField.get(s3);
char[] s4Value = (char[]) valueField.get(s4);
char[] s5Value = (char[]) valueField.get(s5);
if (sValue==s1Value){
System.out.println("s1value is same");
}
if (sValue==s2Value){
System.out.println("s2value is same");
}
if (sValue==s3Value){
System.out.println("s3value is same");
}
if (sValue==s4Value){
System.out.println("s4value is same");
}
if (sValue==s5Value){
System.out.println("s5value is same");
}
}
}
运行结果:
s==s2
s1value is same
s2value is same
结果对比结果:s和s2指向同一个字符串常量,s,s1,s2的char value[]指向同一片内存地址;s1,s3,s4,s5都和s都引用不相等。
这里我们就看String 这个“+”是怎么起作用的,我们可以通过反编译(jad -o -a -s d.java Test.class)得到我们想要的代码(已经删除字节码注释):
public class Test
{
public Test()
{
}
public static void main(String args[])
throws NoSuchFieldException, IllegalAccessException
{
String s = "test";
char test[] = {
't', 'e', 's', 't'
};
String te = "te";
String st = "st";
String s1 = new String(s);
String s2 = "test";
String s3 = (new StringBuilder()).append("te").append(new String("st")).toString();
String s4 = new String(test);
String s5 = (new StringBuilder()).append(te).append(st).toString();
if(s == s1)
System.out.println("s==s1");
if(s == s2)
System.out.println("s==s2");
if(s == s3)
System.out.println("s==s3");
if(s == s4)
System.out.println("s==s4");
if(s == s5)
System.out.println("s==s5");
Field valueField = java/lang/String.getDeclaredField("value");
valueField.setAccessible(true);
char sValue[] = (char[])(char[])valueField.get(s);
char s1Value[] = (char[])(char[])valueField.get(s1);
char s2Value[] = (char[])(char[])valueField.get(s2);
char s3Value[] = (char[])(char[])valueField.get(s3);
char s4Value[] = (char[])(char[])valueField.get(s4);
char s5Value[] = (char[])(char[])valueField.get(s5);
if(sValue == s1Value)
System.out.println("s1value is same");
if(sValue == s2Value)
System.out.println("s2value is same");
if(sValue == s3Value)
System.out.println("s3value is same");
if(sValue == s4Value)
System.out.println("s4value is same");
if(sValue == s5Value)
System.out.println("s5value is same");
}
}
1. 1.s!=s1 s.value==s1.value
s是常量区String对象的引用,s1是使用new在堆里面生成的对象引用,所以s!=s1;而s.value==s1.value相等是因为构造方法是一个浅拷贝:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
因为我们看String源代码可以看到数据底层的value是指向同一个char[]实例的,相当于多实例出一个String对象,但是用了同一个底层数据,对资源是一种浪费,所以平时尽量减少这种方式使用。
2. 1.s!=s2 s.value==s2.value &s!=s5 s.value==s5.value&s!=s3 s.value==s3.value
分析:我们看编译之后字节码就可以发现,在编译期间jvm就可以直接优化s2直接引用常量区的字符串常量:
String s2="te"+"st";优化为String s2="test"
而使用引用被使用"+"重载:
String s3="te"+new String("st")
String s3 = (new StringBuilder()).append("te").append(new String("st")).toString();
String s5=te+st
String s5 = (new StringBuilder()).append(te).append(st).toString();
对于s3和s5在编译期间不能不确定性,所以没有被编译器优化,所以使用的是StringBuilder,通过StringBuilder源码可以发现toString方法也是直接在堆上直接创建一个新的String对象,所以导致String以及属性value内存都不相等。
@Override //StringBuilder.toString()
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
2. 1.s!=s4 s.value==s4.value
这个看源码就可以知道String对象以及velue内存是直接从堆里面分配的
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
总结:由以上实验我们可以看到,不同的String构造,内存分配地址是不同的。编译期间确定的String是分配在常量区的,运行期间初始化是分配在堆上的,从char[]初始化以及+拼接生成的String对象以及内部value是分配至堆上的。字符串常量区有且仅有一份字符串,不会出现重复的字符串,但是堆上可能存在多份一样的字符串。