• 死啃了String源码之后


    Java源码之String

    说在前面:

    为什么看源码: 最好的学习的方式就是模仿,接下来才是创造。而源码就是我们最好的模仿对象,因为写源码的人都不是一般的人,所以用心学习源码,也就可能变成牛逼的人。其次,看源码,是一项修练内功的重要方式,书看百遍其意自现,源码也是一样,前提是你不要惧怕源码,要用心的看,看不懂了,不要怀疑自己的智商,回过头来多看几遍,我就是这样做的,一遍有一遍的感受,等你那天看源码不由的惊叹一声,这个代码写得太好了,恭喜你,你已经离优秀不远了。最后,看源码,能培养我们的编程思维,当然这个层次有点高了,需要时间积累,毕竟我离这个境界也有点远。

    今天就来谈谈java的string的源码实现,后续我也会写javaSe的源码系列,欢迎围观和交流。

    1.继承关系

    继承三个接口的说明:

    Comparable接口:

    实现对象之间比较的接口,它的核心方法只有一个:

    public int compareTo(T o);
    

    CharSequence接口:

    CharSequence是char值的可读序列。 该接口提供对许多不同种类的char序列的统一只读访问。CharSequence是一个接口,它只包括length(), charAt(int index), subSequence(int start, int end)这几个API接口。除了String实现了CharSequence之外,StringBufferStringBuilder也实现了 CharSequence接口。

    那么String为什么实现Charsequence这个接口呢。这里就要涉及一个java的重要特性,也就是多态。看下面的代码

       public void charSetTest(CharSequence charSequence){
            System.out.println(charSequence+"实现了多态");
        }
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
            StringTest strTest = new StringTest();
            strTest.charSetTest(new String("我是String"));
            strTest.charSetTest(new StringBuffer("我是StringBuffer"));
            strTest.charSetTest(new StringBuilder("我是StringBuilder"));
    }
    

    执行结果:

    我是String实现了多态
    我是StringBuffer实现了多态
    我是StringBuilder实现了多态
    

    继承这个接口的原因就很明显:

    因为String对象是不可变的,StringBuffer和StringBuilder这两个是可变的,所以我们在构造字符串的过程中往往要用到StringBuffer和StringBuilder。如果那些方法定义String作为参数类型,那么就没法对它们用那些方法,先得转化成String才能用。但StringBuffer和StringBuilder转换为String再转换过来很化时间的,用它们而不是直接用String的“加法”来构造新String本来就是为了省时间。

    Serializable接口:

    继承该接口,就是表明这个类是是可以别序列化的,这里就标记了string这个类是可以被序列化的,序列化的定义以及使用时机可以移步这里

    2.常用方法的使用和源码解析:

    2.1构造方法

    string总共提供了15种构造方法:

    当然,这么多的构造方法,只需搞明白四个构造方法就可以了,因为其他的构造方法就是这四个构造方法的调用:

    在看构造方法之前,先看看String的属性

     private final char value[];
     private int hash; // Default to 0
     private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];
    

    通过属性了解到,底层声明了一个final类型的char数组,这个char数组就是String的构造方法的核心,因为String的本质就是字符char(只针对javaSE8 以前的版本),下面来分析有代表的四个构造方法:

    1. String()和String(String original)

      这两个构造方法我们平时用的比较经常,源码如下:

       public String() {   this.value = "".value; }
      
       public String(String original) {
              this.value = original.value;
              this.hash = original.hash;
          }
      

      分析:

      通过源码可以看到,核心还是和String类声明的char[]数组value属性建立联系,进而来处理这个属性的值。

      在String()这个空构造的方法中,就是属性char数组的值声明为一个空的字符串赋值给属性value.

      而在String(String original),也是将方法参数的value的属性赋值给声明char[]数组的value属性,方便String的其他方法对char[]数组处理。

      记住,在java的String操作中,大多数情况下还是对char[]数组的操作,这点很重要。

      一般情况下定义一个新的字符串,有下面的两种方式:

      String chen = new String("chen");   // 这个我们一般不会使用
      String chen = "chen";
      

      我们一般会选择第二种,这又是什么原因呢:

      其实这两种声明的方式在JVM看来时等价的。

      划重点:

      但是String password="chen",利用了字符串缓冲池,也就是说如果缓冲池中已经存在了相同的字符串,就不会产生新的对象,而直接返回缓冲池中的字符串对象的引用。

      如:
      String a = "chen";
      String b = "chen";
      String c = new String("chen");
      String d = new String("chen");
      
      System.out.println(a==b);//将输出"true";因为两个变量指向同一个对象。利用了字符串的缓冲池
      System.out.println(c==d);//将输出"flase";因为两个变量不指向同一个对象。虽然值相同,只有用c.equals(d)才能返回true.
      

      所以实际中,建议用第一种,可以减少系统资源消耗。

    2. String(char[]vlaue,int offest,int count) 与字符相关的构造方法

      源码如下:

      public String(char value[], int offset, int count) {
              if (offset < 0) {
                  throw new StringIndexOutOfBoundsException(offset);
              }
              if (count <= 0) {
                  if (count < 0) {
                      throw new StringIndexOutOfBoundsException(count);
                  }
                  if (offset <= value.length) {
                      this.value = "".value;
                      return;
                  }
              }
              // Note: offset or count might be near -1>>>1.
              if (offset > value.length - count) {
                  throw new StringIndexOutOfBoundsException(offset + count);
              }
              this.value = Arrays.copyOfRange(value, offset, offset+count);
          }
      

      分析:

      这个构造方法的作用就是传入指定大小的char[] 数组,指定一个起始位置,然后构造出从起始位置开始计算的指定长度个数的字符串,具体用法:

      public static void main(String[] args) {
              char[] chars = new char[]{'a','b','c','d'};
              
             // 从chars数组的第二位开始,总数为3个字符的字符串 
              String rangeChar=new String(chars,1,3);
              System.out.println(rangeChar);
          }
      
      输出:
          abc   
      

      这个构造方法的核心在于:

      this.value = Arrays.copyOfRange(value, offset, offset+count);
      
      //而这个方法在追一层,就到了Arrays这个工具类copyOfRange这个方法
          public static char[] copyOfRange(char[] original, int from, int to) {
              int newLength = to - from;
              if (newLength < 0)
                  throw new IllegalArgumentException(from + " > " + to);
              char[] copy = new char[newLength];
              System.arraycopy(original, from, copy, 0,
                               Math.min(original.length - from, newLength));
              return copy;
          }
      

      其实看到这里,他的实现原理就基本清楚了,分析copyOfRange()这个方法的执行步骤:

      首先是获取原字符数组的orginal[]要构造字符串的长度,也就是 这一行代码:

      int newLength = to - from;
      

      然后异常判断,并声明新的数组,来存储原数组指定长度的值

       if (newLength < 0)
                  throw new IllegalArgumentException(from + " > " + to);
              char[] copy = new char[newLength];
      

      将原字符数组指定长度的值拷贝到新数组,返回这个数组:

       System.arraycopy(original, from, copy, 0,
                               Math.min(original.length - from, newLength));
              return copy;
      

      最后再将数组的值赋值给String的属性value,完成初始化:

       this.value = Arrays.copyOfRange(value, offset, offset+count);
      

      归根结底还是和String声明的属性value建立联系,完成相关的操作。

    3. String(byte[] bytes,int offest,int length,Charset charset) 字节相关的 构造方法

      这个构造方法的作用就是将指定长度的字节数组,构造成字符串,且还可以指定编码值:

      涉及的源码如下:

      public String(byte bytes[], int offset, int length, Charset charset) {
              if (charset == null)
                  throw new NullPointerException("charset");
              checkBounds(bytes, offset, length);
              this.value =  StringCoding.decode(charset, bytes, offset, length);
          }
      
      // StringCoding中的方法:
       static char[] decode(Charset cs, byte[] ba, int off, int len) {
               // 1, 构造解码器
              CharsetDecoder cd = cs.newDecoder();
              int en = scale(len, cd.maxCharsPerByte());
              char[] ca = new char[en];
              if (len == 0)
                  return ca;
              boolean isTrusted = false;
              if (System.getSecurityManager() != null) {
                  if (!(isTrusted = (cs.getClass().getClassLoader0() == null))) {
                      ba =  Arrays.copyOfRange(ba, off, off + len);
                      off = 0;
                  }
              }
              cd.onMalformedInput(CodingErrorAction.REPLACE)
                .onUnmappableCharacter(CodingErrorAction.REPLACE)
                .reset();
              if (cd instanceof ArrayDecoder) {
                  int clen = ((ArrayDecoder)cd).decode(ba, off, len, ca);
                  return safeTrim(ca, clen, cs, isTrusted);
              } else {
                  ByteBuffer bb = ByteBuffer.wrap(ba, off, len);
                  CharBuffer cb = CharBuffer.wrap(ca);
                  try {
                      CoderResult cr = cd.decode(bb, cb, true);
                      if (!cr.isUnderflow())
                          cr.throwException();
                      cr = cd.flush(cb);
                      if (!cr.isUnderflow())
                          cr.throwException();
                  } catch (CharacterCodingException x) {
                      // Substitution is always enabled,
                      // so this shouldn't happen
                      throw new Error(x);
                  }
                  return safeTrim(ca, cb.position(), cs, isTrusted);
              }
          }
      
       private static char[] safeTrim(char[] ca, int len,
                                         Charset cs, boolean isTrusted) {
              if (len == ca.length && (isTrusted || System.getSecurityManager() == null))
                  return ca;
              else
                  return Arrays.copyOf(ca, len);
          }
      

      这个方法构造的方法的复杂之处就是在于对于指定编码的处理,但是我们如果看完这个方法调用的整个流程最终还是落到

      return Arrays.copyOf(ca, len);
      返回一个指定编码的字符数组,然后和String类的value属性建立联系
      

      字节数组构造方法的基本逻辑:就是将字节数组转化为字符数组,再和String的value属性建立联系,完成初始化。

    4. String(StringBuilder builder) 和String(StringBuffer buffer)与String扩展类相关的构造方法:

      这个构造方法就是将StringBuilder或者StringBuffer类初始化String类,源码如下:

      public String(StringBuffer buffer) {
              synchronized(buffer) {
                  this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
              }
          }
      
       public String(StringBuilder builder) {
              this.value = Arrays.copyOf(builder.getValue(), builder.length());
          }
      

      分析:

      核心的代码还是这一句:

        this.value = Arrays.copyOf(builder.getValue(), builder.length());
      

    ​ 在往下看builder.getValue(),的源码

       final char[] getValue() {
            return value;
        }
    返回一个字符数组
    

    这样就能很好理解这个构造方法了: 先利用builder.getValue()将指定的类型转化为字符数组,通过Arrays.copyOf()方法进行拷贝,将返回的数组赋值给String的属性value,完成初始化。

    2.2常用的方法分析

    String的方法大概有60多个,这里只分析几个常用的方法,了解其他的方法,可以移步javaSE官方文档:

    1. 字符串转化为字符的方法:charAt(int index)

       public char charAt(int index) {
               //1. 判断异常
              if ((index < 0) || (index >= value.length)) {
                  throw new StringIndexOutOfBoundsException(index);
              }
              // 2.返回指定位置的字符
              return value[index];
          }
      

      用法示例:

              String StrChar = "chen";
              char getChar = StrChar.charAt(1);
              System.out.println(getChar);
      输出:
          h
      

      2.字符串转化为字节数组的方法:getBytes()

      // 源码
      public byte[] getBytes() {
          return StringCoding.encode(value, 0, value.length);
      }
      
      // encode的源码
      static byte[] encode(char[] ca, int off, int len) {
              String csn = Charset.defaultCharset().name();
              try {
                  // use charset name encode() variant which provides caching.
                  return encode(csn, ca, off, len);
              } catch (UnsupportedEncodingException x) {
                  warnUnsupportedCharset(csn);
              }
              try {
                  return encode("ISO-8859-1", ca, off, len);
              } catch (UnsupportedEncodingException x) {
                  // If this code is hit during VM initialization, MessageUtils is
                  // the only way we will be able to get any kind of error message.
                  MessageUtils.err("ISO-8859-1 charset not available: "
                                   + x.toString());
                  // If we can not find ISO-8859-1 (a required encoding) then things
                  // are seriously wrong with the installation.
                  System.exit(1);
                  return null;
              }
          }
      

    ​ 用法示例:

           String strByte = "chen";
           
           // 将string转化为字节数组
           byte[] getBytes = strByte.getBytes();
           
           // 遍历输出
            for (byte getByte : getBytes) {
                System.out.println(getByte);
            }
     输出结果:
         99
         104
         101
         110
    

    3.返回字符串中指定字符的下标的方法: indexOf(String str):

    这里的参数为字符串:

    这个方法一共涉及了四个方法,源码如下:

     public int indexOf(String str) {
            return indexOf(str, 0);
        }
    
     public int indexOf(String str, int fromIndex) {
            return indexOf(value, 0, value.length,
                    str.value, 0, str.value.length, fromIndex);
        }
    
     static int indexOf(char[] source, int sourceOffset, int sourceCount,
                String target, int fromIndex) {
            return indexOf(source, sourceOffset, sourceCount,
                           target.value, 0, target.value.length,
                           fromIndex);
        }
    
    static int indexOf(char[] source, int sourceOffset, int sourceCount,
                char[] target, int targetOffset, int targetCount,
                int fromIndex) {
        
            // 1. 判断范围
            if (fromIndex >= sourceCount) {
                return (targetCount == 0 ? sourceCount : -1);
            }
            if (fromIndex < 0) {
                fromIndex = 0;
            }
            if (targetCount == 0) {
                return fromIndex;
            }
           
            // 2,判断目标字符串是否时原子符串的子序列,并返回目标序列的第一个字符在原字符序列的索引。
            char first = target[targetOffset];
            int max = sourceOffset + (sourceCount - targetCount);
    
            for (int i = sourceOffset + fromIndex; i <= max; i++) {
                /* Look for first character. */
                if (source[i] != first) {
                    while (++i <= max && source[i] != first);
                }
    
                /* Found first character, now look at the rest of v2 */
                if (i <= max) {
                    int j = i + 1;
                    int end = j + targetCount - 1;
                    for (int k = targetOffset + 1; j < end && source[j]
                            == target[k]; j++, k++);
    
                    if (j == end) {
                        /* Found whole string. */
                        return i - sourceOffset;
                    }
                }
            }
            return -1;
        }
    

    具体执行过程已在方法的注释中进行了说明:

    用法示例:

     String strIndex = "chen";
             int getIndex = strIndex.indexOf("he");
            System.out.println(getIndex);
    输出:
        1
    

    注意:也就是输出字符串中的第一个字符在原子符串中的索引,前提是传入的参数必须是原子符串的子序列,以上面的列子为例,传入的字符串序列必须是chen这个字符串的子序列,才能输出正确的索引,比如传入的序列不是chen的子序列,输出为-1

     String strIndex = "chen";
             int getIndex = strIndex.indexOf("hew");
            System.out.println(getIndex);
    输出:
        -1        
    

    使用这个方法时,这点非常注意。

    4.将字符串中的某个字符进行替换:replace(char oldChar, char newChar)

    参数就是被替换的字符和新的字符,源码如下:

    public String replace(char oldChar, char newChar) {
        
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */
            
            // 1.遍历字符数组,找到原子符的位置
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            //2. 声明一个临时的字符数组,用来存储替换后的字符串,
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    // 3. 将原字符数组拷贝到新的字符数组中去
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    // 4.
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                // 3. 初始化一个新的字符串
                return new String(buf, true);
            }
        }
        return this;
    }
    

    具体的执行逻辑就是注释的语句。

    用法示例:

     String strIndex = "chee";
            String afterReplace = strIndex.replace('e','n');
            System.out.println(afterReplace);
    输出:
        chnn
    

    注意:这里的替换是字符串中的所有与旧字符的相同的字符,比如上面的这个例子,就是将原子符中的e全部替换为n。

    5.字符串的分隔:split(String regex) :

    源码如下:

       public String[] split(String regex) {
            return split(regex, 0);
        }
    
      public String[] split(String regex, int limit) {
            /* fastpath if the regex is a
             (1)one-char String and this character is not one of the
                RegEx's meta characters ".$|()[{^?*+\", or
             (2)two-char String and the first char is the backslash and
                the second is not the ascii digit or ascii letter.
             */
            char ch = 0;
            if (((regex.value.length == 1 &&
                 ".$|()[{^?*+\".indexOf(ch = regex.charAt(0)) == -1) ||
                 (regex.length() == 2 &&
                  regex.charAt(0) == '\' &&
                  (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
                  ((ch-'a')|('z'-ch)) < 0 &&
                  ((ch-'A')|('Z'-ch)) < 0)) &&
                (ch < Character.MIN_HIGH_SURROGATE ||
                 ch > Character.MAX_LOW_SURROGATE))
            {
                int off = 0;
                int next = 0;
                boolean limited = limit > 0;
                ArrayList<String> list = new ArrayList<>();
                while ((next = indexOf(ch, off)) != -1) {
                    if (!limited || list.size() < limit - 1) {
                        list.add(substring(off, next));
                        off = next + 1;
                    } else {    // last one
                        //assert (list.size() == limit - 1);
                        list.add(substring(off, value.length));
                        off = value.length;
                        break;
                    }
                }
                // If no match was found, return this
                if (off == 0)
                    return new String[]{this};
    
                // Add remaining segment
                if (!limited || list.size() < limit)
                    list.add(substring(off, value.length));
    
                // Construct result
                int resultSize = list.size();
                if (limit == 0) {
                    while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                        resultSize--;
                    }
                }
                String[] result = new String[resultSize];
                return list.subList(0, resultSize).toArray(result);
            }
            return Pattern.compile(regex).split(this, limit);
        }
    
    

    用法示例:

    // 将一句话使用空格进行分隔 
    String sentence = "People who hear thumb up can get rich";
            
           // 使用空格进行分隔
           String[] subSequence = sentence.split("\s");
            for (String s : subSequence) {
                System.out.println(s);
        }
    
    输出:
        People
        who
        hear
        thumb
        up
        can
        get
        rich
    

    注意: 使用这个方法,对正则表达式有所了解,才能实现更强大的功能,正则表达式的学习,可以移步菜鸟教程

    6.实现字符串的指定范围的切割substring(int beginIndex, int endIndex):

    源码如下:

     public String substring(int beginIndex, int endIndex) {
         
            // 1.判断异常
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            if (endIndex > value.length) {
                throw new StringIndexOutOfBoundsException(endIndex);
            }
            // 2,确定切割的长度
            int subLen = endIndex - beginIndex;
            if (subLen < 0) {
                throw new StringIndexOutOfBoundsException(subLen);
            }
            // 3.使用构造方法,返回切割后字符串
            return ((beginIndex == 0) && (endIndex == value.length)) ? this
                    : new String(value, beginIndex, subLen);
        }
    

    具体的执行逻辑如注释所示,这个方法的逻辑总体比较简单:

    具体用法:

         String sentence = "hhchennn";
           String subSequence = sentence.substring(2,6);
            System.out.println(subSequence);
    输出:
        chen
    

    注意: 从源码了解到,这个方法的在切割的时候,一般将第一个参数包含,包含第二个参数,也就是说上面的例子中在切割后的字符串中包含2这个字符,但是不包含6这个字符。

    7.当然处理这些还有一些常用的方法,比如:

    // 1.去除字符串前后空格的方法
    trim()
        
    //2.大小写转换的方法
     toLowerCase()
     toUpperCase()
        
    //3. 将字符串转化为数组
     toCharArray()
        
    // 4.将基本类型转化字符串    
     valueOf(boolean b)
        
    // 5.返回对象本身的字符串形式   
     toString()
    

    这些方法使用起来都比较简单,强烈建议看看java官方文档

    3.面试常问

    3.1. 为什么是不可变的

    1、什么是不可变?

    从 java角度来讲就是说成final的。参考Effective Java 中第 15 条 使可变性最小化 中对 不可变类 的解释:

    不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。为了使类不可变,要遵循下面五条规则:
    
    1. 不要提供任何会修改对象状态的方法。
    
    2. 保证类不会被扩展。 一般的做法是让这个类称为 `final` 的,防止子类化,破坏该类的不可变行为。
    
    3. 使所有的域都是 final 的。
    
    4. 使所有的域都成为私有的。 防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。
    
    5. 确保对于任何可变性组件的互斥访问。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。
    

    当然在 Java 平台类库中,包含许多不可变类,例如 String , 基本类型的包装类,BigInteger, BigDecimal 等等。综上所述,不可变类具有一些显著的通用特征:类本身是 final 修饰的;所有的域几乎都是私有 final 的;不会对外暴露可以修改对象属性的方法。通过查阅 String 的源码,可以清晰的看到这些特征。

    2.为什么不可变

    String real = "chen"
     real = "Wei";
    

    下图就很好解释了代码的执行过程:

    执行第一行代码时,在堆上新建一个对象实例 chen , real是一个指向该实例的引用,引用包含的仅仅只是实例在堆上的内存地址而已。执行第二行代码时,仅仅只是改变了 real 这个引用的地址,指向了另一个实例 wei。所以,正如前面所说过的,不可变类只是其实例不能被修改的类。real 重新赋值仅仅只是改变了它的引用而已,并不会真正去改变它本来的内存地址上的值。这样的好处也是显而易见的,最简单的当存在多个 String 的引用指向同一个内存地址时,改变其中一个引用的值并不会对其他引用的值造成影响。

    那么,String 是如何保持不可变性的呢?结合 Effective Java 中总结的五条原则,阅读它的 源码 之后就一清二楚了。

    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;
    
        private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];
    
    

    String 类是 final 修饰的,满足第二条原则:保证类不会被扩展。 分析一下它的几个域:

    • private final char value[] : 可以看到 Java 还是使用字节数组来实现字符串的,并且用 final 修饰,保证其不可变性。这就是为什么 String 实例不可变的原因。
    • private int hash : String的哈希值缓存
    • private static final long serialVersionUID = -6849794470754667710L : String对象的 serialVersionUID
    • private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0] : 序列化时使用

    其中最主要的域就是 value,代表了 String对象的值。由于使用了 private final 修饰,正常情况下外界没有办法去修改它的值的。正如第三条 使所有的域都是 final 的。 和第四条 使所有的域都成为私有的 所描述的。难道这样一个 private 加上 final 就可以保证万无一失了吗?看下面代码示例:

        final char[] value = {'a', 'b', 'c'};
        value[2] = 'd';
    

    这时候的 value 对象在内存中已经是 a b d 了。其实 final 修饰的仅仅只是 value 这个引用,你无法再将 value 指向其他内存地址,例如下面这段代码就是无法通过编译的:

        final char[] value = {'a', 'b', 'c'};
        value = {'a', 'b', 'c', 'd'};
    

    所以仅仅通过一个 final 是无法保证其值不变的,如果类本身提供方法修改实例值,那就没有办法保证不变性了。Effective Java 中的第一条原则 不要提供任何会修改对象状态的方法 。String 类也很好的做到了这一点。在 String 中有许多对字符串进行操作的函数,例如 substring concat replace replaceAll 等等,这些函数是否会修改类中的 value 域呢?我们看一下 concat() 函数的内部实现:

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }
    

    注意其中的每一步实现都不会对 value产生任何影响。首先使用 Arrays.copyOf() 方法来获得 value 的拷贝,最后重新 new 一个String对象作为返回值。其他的方法和 contact 一样,都采取类似的方法来保证不会对 value 造成变化。的的确确,String 类中并没有提供任何可以改变其值的方法。相比 final 而言,这更能保障 String 不可变。

    3.不可变类的好处:

    Effective Java 中总结了不可变类的特点。

    • 不可变类比较简单。
    • 不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由地共享。
    • 不仅可以共享不可变对象,甚至可以共享它们的内部信息。
    • 不可变对象为其他对象提供了大量的构建。
    • 不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。

    3.2. 使用什么方式可以改变String类的不可变性

    当然使用反射,java的反射机制可以做到我们平常做不到的很多事情:

            String str = "chen";
            System.out.println(str);
            Field field = String.class.getDeclaredField("value");
            field.setAccessible(true);
            char[] value = (char[]) field.get(str);
            value[1] = 'a';
            System.out.println(str);
    
    执行结果:
      chen
      caen  
    

    3.3. String和stringBuffer和StringBuilder的区别

    从以下三个方面来考虑他们之间的异同点:

    1.可变和不可变性:

    String: 字符串常量,在修改时,不会改变自身的值,若修改,就会重新生成新的字符串对象。

    StringBuffer: 在修改时会改变对象本身,不会生成新的对象,使用场景:对字符经常改变的情况下,主要方法: append(),insert() 等。

    2.线程是否安全

    String: 定义之后不可改变,线程安全

    String Buffer: 是线程安全的,但是执行效率比较低,适用于多线程下操作字符串缓冲区的大量数据。

    StringBuilder: 线程不安全的,适用于单线程下操作字符串缓冲区的大量数据

    3.共同点

    StringBuilder和StringBuffer有共有的父类 AbstractStringBuilder(抽象类)。

    StringBuilder,StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如: super().append()...

    只是StringBuffer会在方法上加上synchronized关键字,进行同步。

    4.优秀的工具包推荐

    4.1guava

    Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。具体的中文参考文档,Guava中文参考文档

    4.2 Hutool

    Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。Hutool参考文档

    追本溯源,方能阔步前行

    参考资料:

    参考博客:

    https://juejin.im/post/59cef72b518825276f49fe40

    https://www.cnblogs.com/ChrisMurphy/p/4760197.html

    参考书籍: java官方文档 《深入理解JVM虚拟机》

  • 相关阅读:
    为什么网站不被百度收录或收录清零?
    XmlSerializer序列化一组成员到文本文件
    windows phone不同页面间传值
    windows phone下进行Isolated的IO读写
    windows Phone 后退键历史的清除
    Window Phone ListBox的DataBinding:
    VGA的相关代码
    如何避免Quartus II自動將未宣告的信號視為wire?
    XINLINX约束心得
    VIM配置文件备份
  • 原文地址:https://www.cnblogs.com/chentang/p/13067765.html
Copyright © 2020-2023  润新知