java.lang.String源码分析

作者: Oliver_Li | 来源:发表于2019-12-05 23:21 被阅读0次
    1. 描述
    • 关键字段:
      • private final char value[]:表明String内部实际上就是一个不可变的字符数组,final保证引用不会变,但数组本身可以被修改,所以String把value[]定义为private,类中也做了控制,所以除反射外String可以认为是不可变的。
    1. 构造函数
    • String的构造函数有很多种类。传入空串、字符串、字符数组、字节数组+字符集、字符数组 + 位置截取、StringBuffer、StringBuilder等等。最终目的都是通过转换给value[]赋值。下面列举几种:
     public String(String original) {
         this.value = original.value;
         this.hash = original.hash;
     }
    
     public String(char value[]) {
         this.value = Arrays.copyOf(value, value.length);
     }
       
     String(char[] value, boolean share) {
         this.value = value;
     }
    
    • 第一种:因为String类型传入时就是不可变的所以直接赋值即可。
    • 第二种:传入的value[]不能直接赋值,传入的对象可能会带外部的引用,外部修改会导致数据被改,所以内部使用System.arraycopy()新建一个对象然后赋值给value[],传入的数组类参数都需要copy后再赋值。
    • 第三种:一种特殊的赋值,包内可调,为了提升速度不创建新char[]直接赋值,后面StringBuffer的toString时会遇到。
    1. isEmpty()
     public boolean isEmpty() {
          return value.length == 0;
      }
    
    • 判断字符串是否为空,value不可变,直接判断长度即可
    1. equals(Object anObject)、equalsIgnoreCase(String anotherString)
         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。然后通过instanceof判断类型是否相同或有继承关系,然后依次判断value[]的字符是否一致,一致则返回true。
    • equalsIgnoreCase():equals()的不区分大小版,通过两个字符数组Character.toUpperCase()比较,代码很简单,就不贴了。
    1. 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然后加上当前字符(Unicode低16位)
    • Unicode和ASCII的区别(百度):这两种编码的目的都是为了计算机中表示字符,ASCII码占一个字节,包括英文大小写、数字、制表符等等,范围是0x00 - 0xFF一共256个。后来因为要包括其他国家语言而进行扩展,最大到两个字节0x0000 - 0xFFFF,就是Unicode。Unicode包含了ASCII。
    1. charAt(int index)
    • 返回数组下标的元素,代码略。
    1. split(String regex)、split(String regex, int limit)
    • split(String regex); = split(String regex, 0)
    • split(String regex, int limit)第二个参数有三种处理方法,直接举例说明传入"a,b,c,,":
      • limit大于0:匹配n-1次后停止。例如n = 2; // {"a","b,c,,"}
      • limit小于零:完全匹配。例如n = -2; // {"a","b","c","",""}
      • limit等于零:完全匹配,而且清除结尾的空串。例如n = 0; // {"a","b","c"}
    1. replace(CharSequence target, CharSequence replacement)
    • 字符串中所有target替换成replacement。
    1. intern()
    • native方法,如果常量池中存在该字符串,就会直接返回常量池中该字符串,如果没有, 会将字符串放入常量池后, 再返回,下面通过对象创建看一下这个方法。
    1. 字符串对象的创建
    • String str = new String("a");创建几个对象?1 or 2
      • 堆里一个String对象。常量池里一个"a"常量,如果之前有就直接引用没有就创建。引用路线大概是栈str -> 堆String -> 常量池"a"
      • 网上有很多说法,来做个测试,证明一下这个结论,还有字符常量池到底是怎么运作的?(虽然常量池也划在堆中,但测试单独区分方便分析)
    • 先来看一下对象是否相同有两种常用方法:
      • "=="
      • Object.HashCode() 或者 System.identityHashCode():这个可能会有一些歧义,hash码确实不能直接表示对象相等,但Object的hashCode有个特点,if(a==b)则HashCode(a) == HashCode(b),反之则不一定,不一定的原因是大量数据产生的hash冲突,如果只是几个对象的测试,还是可靠的。还有一个问题就是String重写了hashCode()重写后只和value[]有关与对象无关,System.identityHashCode()可以替代。
    • 下面通过hash码和intern()通过现象验证一下上述的推测 ,代码分别运行,以免常量池复用影响结果,各段代码和分割线之间的hash码无关,只有在同段代码中能证明对象是不是同一个。网上几乎没有用这种测试方法的,都是"=="判断,所以如果测试有疏漏请及时提醒。
    1. 对象创建测试
    String a = "呵呵";            
    String a1 = "呵" + "呵";      
    String a2 = new String("呵呵");
    String a3 = a2.intern();    
    ---------------------------------------------------------------------------
    String s = new String("嘻嘻");  
    String s1 = "嘻嘻";            
    String s3 = s.intern();
    ---------------------------------------------------------------------------
    String s4 = new String("嘻嘻");  
    String s5 = s.intern();        
    String s6 = "嘻嘻";             
    
    • 第一段代码(直接列出identityHashCode()输出的结果):
      • a = 654845766:a会直接放在常量池
      • a1 = 654845766:a1这样的声明会经过编译优化,优化后和a的声明完全相同,都指向常量池所以相等
      • a2 = 1712536284:new对象的方式会在常量池找有没有"呵呵",如果没有在常量池创建"呵呵"然后创建堆对象指向这个"呵呵",如果有就创建堆对象直接指过去。a2之前已经有了a和a1所以a2声明应该是 "栈里a2的引用" -> "堆里的String对象a2" -> "常量池的字符‘呵呵’ ",但这个hash码是"堆里的String对象a2"的hash码,所以和上面的常量池对象肯定不等。
      • a3 = 654845766:根据intern()的解释,常量池已经有常量"呵呵"了,直接返回,所以和a、a1相等。
    • 第二段代码(每段代码单独运行,以免污染常量池):
      • s = 654845766:后两段代码调整对象生成顺序,可以证明上面new对象”先创建常量池对象,然后堆创建对象最后栈指向“的理论
      • s1 = 1712536284
      • s2 = 1712536284
    • 第三段代码:
      • s4 = 654845766
      • s5 = 1712536284
      • s6 = 1712536284
    String o = "嘻嘻";
    String b = new String("嘻嘻");            
    String d = new String("嘻") + new String("嘻");
    String e = "嘻" + new String("嘻");   
    String h = new StringBuilder("嘻").append("嘻").toString();
    
    • 上面证明了字面量声明和直接new对象两种方式,现在来测试一下复杂的情况
      • o = 654845766:位于常量池。其他hash码都不一样,所以这些都不是直接指向常量池的而是堆,再写测试代码证明。
    String s4 = new String("嘻") + new String("嘻");    
    String s5 = "嘻嘻";                                 
    String s6 = s4.intern();
    ------------------------------------------------------------------------------
    String s7 = new String("嘻") + new String("嘻");    
    String s8 = s7.intern();    
    String s9 = "嘻嘻";   
    
    • 第一段代码:
      • s4 = 379110473:堆
      • s5 = 99550389:常量池
      • s6 = 99550389:常量池,这段看不出问题,再来对比第二段代码
    • 第二段代码:
      • s7 = 654845766:堆
      • s8 = 654845766:堆?
      • s9 = 654845766:堆?为什么一样?这时候我发现了一篇博客说,s7声明时不会在常量池创建"嘻嘻"("嘻"会创建,当然并不需要讨论它),s7的String对象在堆,s8.intern()时发现堆里有s7而且字符内容相同,不创建常量池的"嘻嘻",而是直接指到了s7,intern()时常量池创建的对象会指到堆的"嘻嘻",字面量声明时也会这样,所以最后都指到了堆!
    • 剩下的三种new String + new String / "" + new String / new StringBuilder的用最后一种方法验证也是一样的就省略了
    1. String为什么不可变
    • 第一个因素是字符常量池,常量池是一个存储可复用字符串的内存空间,位于堆,大致的原理是创建字符串对象时,如果池里有这个字符串则返回引用,没有就在池里创建并返回。如果String是可变的,常量池内容被改所有引用这个字符串的对象都会变,这样常量池就没意义了。
    • 第二个是线程安全,线程间修改同个字符串不用再做单独的并发处理。
    • 第三个是因为hashCode缓存在对象里,可以避免重复计算。

    结语:对象生成测试大多数是根据代码现象、网上资料推测出来的难免有疏漏,欢迎指正!

    相关文章

      网友评论

        本文标题:java.lang.String源码分析

        本文链接:https://www.haomeiwen.com/subject/afjjgctx.html