String源码分析

       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是分配至堆上的。字符串常量区有且仅有一份字符串,不会出现重复的字符串,但是堆上可能存在多份一样的字符串。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章