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