奥利给!!!再也不怕面试官问我String源码了

前言

简述

字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String
类来创建和操作字符串。字符串缓冲区支持可变字符串。因为 String
对象是不可变的,因此可以共享它们。

String
类代表字符串,Java程序中的所有字符串字面值如 "abc"
都是这个类的实例对象。 String
类是不可改变的,所以你一旦创建了 String
对象,那它的值就无法改变了。如果需要对字符串做很多修改,那么应该选择使用 StringBuilder
或者 StringBuffer

最简单的创建字符串的方式: String qc = "qiu chan"
编译器会使用该值创建一个 对象。我们也可以使用 关键字New
创建 String
对象。

String
类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的 String
    对象会直接存储在常量池中。
  • String
    String
    intern
    intern 方法
    

继承/实现关系

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // 省略
}    
复制代码

String是final修饰
的不能够被继承和修改。

源码

String的底层使用的是char数组用于存储。

private final char value[];
复制代码

缓存字符串的哈希码默认值为0

private int hash;
复制代码

参数构造函数

public String() {
  this.value = "".value;
}
复制代码

解析:初始化一个新创建的 String
对象,使其代表一个空字符序列。 注意,由于 String
是不可变的,所以不需要使用这个构造函数。

参数为字符串的构造函数

public String(String original) {
 this.value = original.value;
 this.hash = original.hash;
}
复制代码

解析:初始化一个新创建的 String
对象,使其代表与参数相同的字符序列。换句话说,新创建的字符串是参数字符串的副本。除非需要参数字符串的显式拷贝,否则不需要使用这个构造函数,因为 String
是不可变的。

参数为char数组的构造函数

public String(char value[]) {
  this.value = Arrays.copyOf(value, value.length);
}
复制代码

解析:分配一个新的 String
,使其代表当前字符数组参数中包含的字符序列。使用 Arrays.copyOf
方法进行字符数组的内容被复制。字符数组的后续修改不会影响新创建的字符串。

参数为char数组并且带有偏移量的构造方法

// value[]:作为字符源的数组,offset:偏移量、下标从0开始并且包括offset,count:从数组中取到的元素的个数。
public String(char value[], int offset, int count) {
  // 如果偏移量小于0抛出IndexOutOfBoundsException异常
 if (offset < 0) {
  throw new StringIndexOutOfBoundsException(offset);
 }
  // 判断要取的元素的个数是否小于等于0
 if (count <= 0) {
    // 要取的元素的个数小于0,抛出IndexOutOfBoundsException异常
  if (count < 0) {
   throw new StringIndexOutOfBoundsException(count);
  }
    // 在要取的元素的个数等于0的情况下,判断偏移量是否小于等于数组的长度
  if (offset <= value.length) {
      // 偏移量小于等于数组的长度,返回一个空字符串数组的形式
   this.value = "".value;
   return;
  }
 }
 // 如果偏移量的值大于数组的长度减去取元素的个数抛出IndexOutOfBoundsException异常
 if (offset > value.length - count) {
  throw new StringIndexOutOfBoundsException(offset + count);
 }
  // 复制元素
 this.value = Arrays.copyOfRange(value, offset, offset+count);
}
复制代码

解析:分配一个新的 Sting
,来源于给定的 char数组
中的字符。 offset
参数是子数组中第一个字符的索引count
参数指定子数组的长度。子数组被被复制以后,对字符数组的修改不会影响新创建的字符串。

参数为StringBuffer的构造方法

public String(StringBuffer buffer) {
  // 这里对StringBuffer进行了加,然后再进行拷贝操作。这里对其进行加锁正是为了保证在多线程环境下只能有一个线程去操作StringBuffer对象。
 synchronized(buffer) {
  this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
 }
}
复制代码

解析:分配一个新的字符串,该字符串包含当前字符串缓冲区参数中包含的字符序列。 Arrays.copyOf
方法进行字符串缓冲区中内容的复制。这里对 StringBuffer
进行了加锁,然后再进行拷贝操作。这里对其进行加锁正是为了保证在多线程环境下只能有一个线程去操作 StringBuffer
对象。

参数为StringBuilder的构造方法

public String(StringBuilder builder) {
 this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
复制代码

解析:参数是 StringBuilder
,这个是线程不安全的,但是性能相对于 StringBuffer
有很大的提升,源码的注释中说通过 toString
方法从字符串构建器中获取字符串可能会运行得更快,通常是首选。

length方法

public int length() {
    // 查看源码发现,这个value是一个char数组,本质获取的是字符串对应的char数组的长度。
  return value.length;
 }
复制代码

解析:返回此字符串的长度。查看源码发现,这个 value
是一个 char数组
,本质获取的是字符串对应的 char数组
的长度。

isEmpty方法

public boolean isEmpty() {
    // 底层的char数组的长度是否为0进行判断
  return value.length == 0;
}

//举例
@Test
public void test_string_isEmpty(){
 System.out.println(" ".isEmpty());// true
  System.out.println("".isEmpty());// false
}
复制代码

解析:判断给定的字符串是否为空,底层实现是根据 char
数组的长度是否为 0
进行判断。

charAt方法

public char charAt(int index) {
  // 给定的索引小于0或者给定的索引大于这个字符串对应的char数组的长度抛出角标越界异常
 if ((index < 0) || (index >= value.length)) {
  throw new StringIndexOutOfBoundsException(index);
 }
  // 获取当前的指定位置的char字符
 return value[index];
}
复制代码

解析:根据给定的索引获取当前的指定位置的 char
字符。如果给定的索引否小于 0
,或者给定的索引是大于这个字符串对应的char数组的长度抛出角标越界异常。 index
是从 0
开始到 length-1
结束。序列的第一个 char
值在索引 0
处,下一个在索引1处,依此类推,与数组索引一样。

getChars方法

// srcBegin:要复制的字符串中第一个字符的索引【包含】。srcEnd:要复制的字符串中最后一个字符之后的索引【不包含】。dst[]:目标数组。dstBegin:目标数组中的起始偏移量。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
  // 校验起始索引小于0抛出角标越界异常
 if (srcBegin < 0) {
  throw new StringIndexOutOfBoundsException(srcBegin);
 }
  // 校验结束索引大于原始字符串的长度抛出角标越界异常
 if (srcEnd > value.length) {
  throw new StringIndexOutOfBoundsException(srcEnd);
 }
  // 校验结束索引大于起始索引抛出角标越界异常
 if (srcBegin > srcEnd) {
  throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
 }
  // 数组的拷贝
 System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

// 案例
@Test
public void test_string_codePointAt(){
  // 原始字符串
 String h = "ahelloworld";
  // 目标char数组
 char[] data = new char[4];
  // 执行拷贝
 h.getChars(2, 6, data, 0);
 System.out.println(data);
}
复制代码

解析:将字符串中的字符复制到目标字符数组中。索引包含 srcBegin
,不包含 srcEnd

equals方法

// anObject:与此String进行比较的对象。
public boolean equals(Object anObject) {
  // 引用相同直接返回true
 if (this == anObject) {
  return true;
 }
 // 判断给定的对象是否是String类型的
 if (anObject instanceof String) {
    // 给定的对象是字符串类型的转换为字符串类型
  String anotherString = (String)anObject;
    // 获取当前字符串的长度
  int n = value.length;
    // 判断给定字符串的长度是否等于当前字符串的长度
  if (n == anotherString.value.length) {
      // v1[]代表当前字符串对应的char数组
   char v1[] = value;
      // v2[]代表给定的字符串对应的char数组
   char v2[] = anotherString.value;
      // 遍历原始char数组,并且与给定的字符串对应的数组进行比较
   int i = 0;
   while (n-- != 0) {
    if (v1[i] != v2[i])
          // 任意一个位置上不相等返回false
     return false;
    i++;
   }
      // 都相等返回true
   return true;
  }
 }
  // 不是String类型,或者长度不一致返回false
 return false;
}
复制代码

解析:这个方法重写了 Object
中的 equals
方法。方法中的将此字符串与指定对象进行比较。接下来附赠一个手写的 String
字符串 equals
方法。

手写equals方法

private boolean mineEquals(String srcObject, Object anObject){
  // 比较引用是否相同
 if (srcObject == anObject){
  return true;
 }
  // 引用不相同比较内容
 if (anObject instanceof String){
  String ans = (String) anObject;
  char[] srcChar = srcObject.toCharArray();
  char[] anChar = ans.toCharArray();
  int n = srcChar.length;
  if (n == anChar.length){
   int i = 0;
   while (n-- != 0){
    if (srcChar[i] != anChar[i])
     return false;
    i++;
   }
   return true;
  }
 }
 return false;
}

// 测试我们自己写的equals方法
 @Test
 public void test_string_mine(){
  String s = new String("aaa");
    // 走的是引用的比较
  System.out.println(s.equals(s));// true 
  boolean b = mineEquals(s, s);
  System.out.println(b);// true
 }
复制代码

equalsIgnoreCase方法

public boolean equalsIgnoreCase(String anotherString) {
  // 引用相同返回true。引用不相同进行长度、各个位置上的char是否相同
 return (this == anotherString) ? true
   : (anotherString != null)
   && (anotherString.value.length == value.length)
   && regionMatches(true, 0, anotherString, 0, value.length);
}
复制代码

解析:将此字符串与另一个字符串进行比较,而忽略大小写注意事项。 regionMatches
方法的源码很有趣的,源码里面有一个while循环,先进行未忽略大小的判断,然后进行忽略大小的判断,在忽略大小的判断中,先进行的是大写的转换进行比较,但是可能会失败【这种字体 Georgian alphabet
】。所以在大写转换以后的比较失败,进行一次小写的转换比较。

startsWith方法

// 判断是否以指定的前缀开头
public boolean startsWith(String prefix) {
  // 0代表从开头进行寻找
  return startsWith(prefix, 0);
}
复制代码

endsWith方法

// 判断是否以指定的前缀结尾
public boolean endsWith(String suffix) {
  // 从【value.length - suffix.value.length】开始寻找,这个方法调用的还是startsWith方法
  return startsWith(suffix, value.length - suffix.value.length);
}
复制代码

startsWith和endsWith最终的实现方法

// prefix: 测试此字符串是否以指定的前缀开头。toffset: 从哪里开始寻找这个字符串。
public boolean startsWith(String prefix, int toffset) {
  // 原始的字符串对应的char[]
 char ta[] = value;
  // 开始寻找的位置
 int to = toffset;
  // 获取指定的字符串对应的char[]
 char pa[] = prefix.value;
 int po = 0;
  // 获取指定的字符串对应的char[]长度
 int pc = prefix.value.length;
 // 开始寻找的位置小于0,或者起始位置大于要查找的长度【value.length - pc】返回false。
 if ((toffset < 0) || (toffset > value.length - pc)) {
  return false;
 }
  // 比较给定的字符串的char[]里的每个元素是否跟原始的字符串对应的char数组的元素相同
 while (--pc >= 0) {
  if (ta[to++] != pa[po++]) {
      // 有一个char不相同返回false
   return false;
  }
 }
  // 相同返回true
 return true;
}
复制代码

substring方法

// 返回一个字符串,该字符串是该字符串的子字符串。beginIndex开始截取的索引【包含】。
public String substring(int beginIndex) {
  // 校验指定的索引,小于0抛出角标越界
 if (beginIndex < 0) {
  throw new StringIndexOutOfBoundsException(beginIndex);
 }
  // 子字符串的长度
 int subLen = value.length - beginIndex;
  // 子字符串的长度小于0抛出角标越界
 if (subLen < 0) {
  throw new StringIndexOutOfBoundsException(subLen);
 }
  // 开始位置为0,返回当前字符串,不为0,创建一个新的子字符串对象并返回
 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
复制代码

解析:返回一个字符串,该字符串是该字符串的子字符串。子字符串以指定索引处的字符开头【包含】,并且扩展到该字符串的末尾。

substring方法

// beginIndex:开始位置【包含】。 endIndex:结束位置【不包含】。
public String substring(int beginIndex, int endIndex) {
  // 校验指定的开始索引,小于0抛出角标越界
 if (beginIndex < 0) {
  throw new StringIndexOutOfBoundsException(beginIndex);
 }
  // 校验指定的结束索引,大于给定的字符串的char数组的长度抛出角标越界
 if (endIndex > value.length) {
  throw new StringIndexOutOfBoundsException(endIndex);
 }
  // 要截取的长度
 int subLen = endIndex - beginIndex;
  // 要截取的长度小于0,抛出角标越界
 if (subLen < 0) {
  throw new StringIndexOutOfBoundsException(subLen);
 }
  // 截取字符串
 return ((beginIndex == 0) && (endIndex == value.length)) ? this
   : new String(value, beginIndex, subLen);
}
复制代码

解析:返回一个字符串,该字符串是该字符串的子字符串。子字符串从指定的 beginIndex
开始【包含】,并且扩展到索引 endIndex-1
处的字符【不包含】。

concat方法

public String concat(String str) {
  // 获取给定的字符串的长度
 int otherLen = str.length();
  // 长度为0,直接返回当前的字符串
 if (otherLen == 0) {
  return this;
 }
  // 获取当前字符串的长度
 int len = value.length;
  // 构建一个新的长度为len + otherLen的字符数组,并且将原始的数据放到这个数组
 char buf[] = Arrays.copyOf(value, len + otherLen);
  // 这个底层调用是System.arraycopy这个方法的处理是使用c语言写的
 str.getChars(buf, len);
 return new String(buf, true);
}
复制代码

将指定的字符串连接到该字符串的末尾。字符串拼接。

format方法

// 使用指定的格式字符串和参数返回格式化的字符串。
public static String format(String format, Object... args) {
 return new Formatter().format(format, args).toString();
}

// 案例,这里是使用%s替换后面的如"-a-"
@Test
public void test_start(){
 System.out.println(String.format("ha %s hh %s a %s h", "-a-", "-b-", "-c-"));
}
复制代码

trim方法

public String trim() {
  // 指定字符串的长度
 int len = value.length;
  // 定义一个开始位置的索引0
 int st = 0;
  // 定义一个char[] val,用于避免使用getfiled操作码,这个可以写段代码反编译一下看看
 char[] val = value;
  // 对于字符串的开头进行去除空格,并记录这个索引
 while ((st < len) && (val[st] <= ' ')) {
  st++;
 }
  // 对于字符串的尾部进行去除空格,也记录这个索引,这个索引就是去除尾部空格后的索引
 while ((st < len) && (val[len - 1] <= ' ')) {
  len--;
 }
  // 根据上面记录的长度判断是否要截取字符串
 return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
复制代码

返回一个字符串,其值就是这个字符串,并去掉任何首部和尾部的空白。

join方法

// 返回一个新的String,该字符串由给定的分隔符和要连接的元素组成。delimiter:分隔每个元素的分隔符。elements:连接在一起的元素。
public static String join(CharSequence delimiter, CharSequence... elements) {
  // delimiter和elements为空抛出空指针异常,null会被拦截,""不会被拦截
 Objects.requireNonNull(delimiter);
 Objects.requireNonNull(elements);
 // 
 StringJoiner joiner = new StringJoiner(delimiter);
  // 遍历给定的要拼接的元素,拼接的元素允许为null
 for (CharSequence cs: elements) {
    // 执行拼接方法
  joiner.add(cs);
 }
 return joiner.toString();
}

// 拼接方法
public StringJoiner add(CharSequence newElement) {
  // prepareBuilder()方法首次调用会创建StringBuilder对象,后面再调用会执行拼接分隔符
 prepareBuilder().append(newElement);
 return this;
}

// 未进行拼接创建StringBuilder对象,已经拼接以后value != null执行拼接分隔符
private StringBuilder prepareBuilder() {
  // 判断拼接的value是否为空
 if (value != null) {
    // 不为空执行拼接分隔符
  value.append(delimiter);
 } else {
    // 最开始使用拼接的时候,调用这个方法创建一个空的StringBuilder对象,只调一次
  value = new StringBuilder().append(prefix);
 }
 return value;
}

// 上面是调用的这个拼接元素方法
@Override
public StringBuilder append(CharSequence s) {
  // 这里啥都没处理,调用的是父类的append方法,设计模式为建造者模式
 super.append(s);
 return this;
}

// 上面的prepareBuilder方法是拼接分隔符,这个方法是将分隔符和给定的元素拼接的方法
@Override
public AbstractStringBuilder append(CharSequence s) {
  // 以下3个判断根据类型和是否为空进行区别拼接
 if (s == null)
  return appendNull();
 if (s instanceof String)
  return this.append((String)s);
 if (s instanceof AbstractStringBuilder)
  return this.append((AbstractStringBuilder)s);
  // 拼接
 return this.append(s, 0, s.length());
}
复制代码

将给定的字符串以给定的分割符分割并返回分隔后的字符串。

replace方法

// target:要被替换的目标字符串。 replacement:替换的字符串
public String replace(CharSequence target, CharSequence replacement) {
 return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
   this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}
复制代码

解析:用指定的字符串替换这个字符串中与之匹配的每个子字符串。替换从字符串的开头到结尾,例如,在字符串 "aaa "中用 "b "替换 "aa "将导致 "ba "而不是 "ab"。

replaceAll方法

// regex:这个支持正则表达式,也可以是要被替换的目标字符串。
public String replaceAll(String regex, String replacement) {
 return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
复制代码

问题: replace
replaceAll
方法的区别是啥?

  • replaceAll
    支持正则表达式。

针对char的replace方法

// oldChar:要被替换的字符,newChar:替换的字符
public String replace(char oldChar, char newChar) {
  // oldChar不等于newChar
 if (oldChar != newChar) {
    // 当前字符串的长度
  int len = value.length;
    // 这个用于下面的while循环里的条件比较,val[i]中的i是从0开始的
  int i = -1;
    // 定义一个char[] val,用于避免使用getfiled操作码,这个可以写段代码反编译一下看看
  char[] val = value; /* avoid getfield opcode */
    // 这个用于记录这个i的值,并且判断是否有要替换的,这个循环有利于性能的提升
  while (++i < len) {
      // val[i]中的i是从0开始的
   if (val[i] == oldChar) {
        // 有要替换的直接跳出循环
    break;
   }
  }
    // 上面的while循环中如果有要替换的i肯定小于len,如果没有下面这个判断就不会执行
  if (i < len) {
      // 能进到这个循环肯定是有要替换的,创建一个长度为len的char数组
   char buf[] = new char[len];
      // 上面的i是记录第一个可以替换的char的索引,下面这个循环是将这个i索引前的不需要被替换的填充到buf[]数组中
   for (int j = 0; j < i; j++) {
        // 填充buf[]数组
    buf[j] = val[j];
   }
      // 从可以替换的索引i开始将剩余的字符一个一个填充到 buf[]中
   while (i < len) {
        // 获取要被替换的字符
    char c = val[i];
        // 判断这个字符是否真的需要替换,c == oldChar成立就替换,否则不替换
    buf[i] = (c == oldChar) ? newChar : c;
    i++;
   }
      // 返回替换后的字符串
   return new String(buf, true);
  }
 }
  // oldChar等于newChar直接返回当前字符串
 return this;
}
复制代码

案例

@Test
public void test_matches(){
 String a = "adddfdefe";
 System.out.println(a.replace('d', 'b'));// abbbfbefe
}

仿写replace方法参数针对char
复制代码

仿写

// 和源码给的唯一不同的是参数传递,其他的都和源码一样,自己写一遍可以加深记忆和借鉴编程思
public String replace(String source, char oldChar, char newChar) {
  char[] value = source.toCharArray();
 if (oldChar != newChar) {
  int len = value.length;
  int i = -1;
  char[] val = value; /* avoid getfield opcode */
  while (++i < len) {
   if (val[i] == oldChar) {
    break;
   }
  }
  if (i < len) {
   char buf[] = new char[len];
   for (int j = 0; j < i; j++) {
    buf[j] = val[j];
   }
   while (i < len) {
    char c = val[i];
    buf[i] = (c == oldChar) ? newChar : c;
    i++;
   }
   return new String(buf);
  }
 }
 return new String(value);
}
复制代码

intern方法

public native String intern();
复制代码

这是一个 native
方法。调用 String#intern
方法时,如果池中已经包含一个由 equals
方法确定的等于此 String
对象的字符串,则返回来自池的字符串。否则,将此 String
对象添加到池中,并返回这个 String
的引用。

原文 

https://juejin.im/post/5efc6db3e51d45349a4939b5

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 奥利给!!!再也不怕面试官问我String源码了

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址