AI智能
改变未来

JDK1.8源码(三)——java.lang.String类


一、概述

1、介绍

  String是一个final类,不可被继承,代表不可变的字符序列,是一个类类型的变量。Java程序中的所有字符串字面量(如\”abc\”)都作为此类的实例实现,\”abc\”是一个对象。
字符串是常量,创建之后不能更改,包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串)。
  String对象的字符内容是存储在一个字符数组 value[] 中的。

二、类源码

1、类声明

源码示例:

* @author  Lee Boynton* @author  Arthur van Hoff* @author  Martin Buchholz* @author  Ulf Zibis* @see     java.lang.Object#toString()* @see     java.lang.StringBuffer* @see     java.lang.StringBuilder* @see     java.nio.charset.Charset* @since   JDK1.0*/public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {}

  实现了 Serializable 接口,标识该类可序列化。
  实现了 Comparable 接口,用于比较两个字符串的大小。
  实现了 CharSequence 接口,表示是一个有序字符的集合。

2、类属性

  源码示例:读一下源码中的英文注释。

// 被用于存储字符/** The value is used for character storage. */private final char value[];// 用于缓存字符串的哈希码.默认是 0/** Cache the hash code for the string */private int hash; // Default to 0// 实现序列化标识后的UID/** use serialVersionUID from JDK 1.0.2 for interoperability */private static final long serialVersionUID = -6849794470754667710L;

  可以看到,String 底层维护了一个 final 的 char[] 。

3、类构造器

  String 类有多个重载的构造器。

4、equals() 方法

  String 类重写了 equals 方法,比较的是组成字符串的每一个字符是否相同,如果都相同则返回true,否则返回false。
  源码示例:

public boolean equals(Object anObject) {// 如果引用相同,则为trueif (this == anObject) {return true;}if (anObject instanceof String) {String anotherString = (String)anObject;int n = value.length;// 判断入参与当前 String 长度是否一致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;}

5、hashCode() 方法

  源码示例:

public int hashCode() {int h = hash;// 判断缓存起来的哈希值是否为 0 且字符长度大于0if (h == 0 && value.length > 0) {char val[] = value;// 字符串每一个字符都参与 哈希值 的计算for (int i = 0; i < value.length; i++) {h = 31 * h + val[i]; // 为什么是 31 ?}hash = h;}return h;}

  这个方法不难读懂,中间的 for 循环,计算公式如下:

  s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

  这里,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:
  ①31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。
  ②31可以被 JVM 优化,31 * i = (i << 5) – i。因为移位运算比乘法运行更快更省性能。
  具体解释可以参考这篇文章。

6、charAt() 方法

  源码示例:

public char charAt(int index) {// 判断索引是否越界if ((index < 0) || (index >= value.length)) {throw new StringIndexOutOfBoundsException(index);}// 根据索引下标返回数组中字符return value[index];}

7、compareTo() 和 compareToIgnoreCase() 方法

  源码示例:

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];// 如果不相等了,返回他们的 ASCII 差值if (c1 != c2) {return c1 - c2;}k++;}// 若 lim 的长度值都相同,返回两个字符串长度之差。return len1 - len2;}public int compareToIgnoreCase(String str) {return CASE_INSENSITIVE_ORDER.compare(this, str);}

  compareToIgnoreCase() 方法在 compareTo 方法的基础上忽略大小写,我们知道大写字母是比小写字母的 ASCII 值小32的。

8、concat() 方法

  该方法是将指定的字符串拼接到该字符串的末尾。
  源码示例:

public String concat(String str) {int otherLen = str.length();// 如果拼接的字符串长度为 0 ,返回当前字符串本身.if (otherLen == 0) {return this;}int len = value.length;// 该方法可以拷贝 value 数组中的值到长度为 len + otherLen 的数组中// 前面是 value 字符,后面是空char buf[] = Arrays.copyOf(value, len + otherLen);// 将要拼接的字符串放入新数组 buf 后面为空的位置。str.getChars(buf, len);// 重新通过 new 关键字创建了一个新的字符串,原字符串是不变的。return new String(buf, true);}

  注意:最后重新通过 new 关键字创建了一个新的字符串,原字符串是不变的。这里也体现了字符序列的不可变性。

9、indexOf() 方法

  返回指定字符第一次出现的此字符串中的索引。
  源码示例:

public int indexOf(int ch) {// 从第一个字符开始搜索return indexOf(ch, 0);}// 从第 fromIndex 个字符开始搜索public int indexOf(int ch, int fromIndex) {final int max = value.length;// 小于0, 默认从 0 开始搜索if (fromIndex < 0) {fromIndex = 0;} else if (fromIndex >= max) {// Note: fromIndex might be near -1>>>1.// 大于了字符串的长度,默认直接找不到,返回 -1return -1;}//一个char占用两个字节,如果ch小于2的16次方(65536),绝大多数字符都在此范围内if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {// handle most cases here (ch is a BMP code point or a// negative value (invalid code point))final char[] value = this.value;// 循环从fromIndex开始查找每一个字符是否是chfor (int i = fromIndex; i < max; i++) {if (value[i] == ch) {return i;}}// 找不到,返回 -1return -1;} else {// 当字符大于65536,判断是否是有效字符,然后依次进行比较return indexOfSupplementary(ch, fromIndex);}}

10、split() 方法

  将该字符串按指定的正则表达式进行切割。对于 split(String regex,int limit) 中 limit 的取值有三种情况:
  ①、limit > 0 ,则pattern(模式)应用 n – 1 次

String str = \"a,b,c\";String[] c1 = str.split(\",\", 2);System.out.println(c1.length); // 2System.out.println(Arrays.toString(c1)); // {\"a\",\"b,c\"}

  ②、limit = 0 ,则pattern(模式)应用无限次并且省略末尾的空字串

String str = \"a,b,c,,\";String[] c1 = str.split(\",\", 0);System.out.println(c1.length); // 3System.out.println(Arrays.toString(c1)); // {\"a\",\"b\",\"c\"}

  ③、limit < 0 ,则pattern(模式)应用无限次

String str = \"a,b,c,,\";String[] c1 = str.split(\",\", -1);System.out.println(c1.length); // 5System.out.println(Arrays.toString(c1)); // {\"a\",\"b\",\"c\",\"\",\"\"}

  源码示例:

public String[] split(String regex) {return split(regex, 0);}public String[] split(String regex, int limit) {/* 1、单个字符,且不是\".$|()[{^?*+\\\\\"其中一个* 2、两个字符,第一个是\"\\\",第二个大小写字母或者数字*//* fastpath if the regex is a(1)one-char String and this character is not one of theRegEx\'s meta characters \".$|()[{^?*+\\\\\", or(2)two-char String and the first char is the backslash andthe 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) {// 当参数limit <= 0 或者 集合list的长度小于 limit-1if (!limited || list.size() < limit - 1) {list.add(substring(off, next));off = next + 1;} else {    // last one//assert (list.size() == limit - 1);// 判断最后一个list.size() == limit - 1list.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// 当 limit<=0 时,limited==false,或者集合的长度 小于 limit时,截取添加剩下的字符串if (!limited || list.size() < limit)list.add(substring(off, value.length));// Construct result// 当 limit == 0 时,如果末尾添加的元素为空(长度为0),则集合长度不断减1,直到末尾不为空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);}

11、replace() 和 replaceAll() 方法

  ①将原字符串中所有的oldChar字符都替换成newChar字符,返回一个新的字符串。
  ②将匹配正则表达式regex的匹配项都替换成replacement字符串,返回一个新的字符串。
  源码示例:

public String replace(char oldChar, char newChar) {if (oldChar != newChar) {int len = value.length;int i = -1;char[] val = value; /* avoid getfield opcode */// 找到 value 中的 oldChar 起始位置while (++i < len) {if (val[i] == oldChar) {break;}}if (i < len) {char buf[] = new char[len];// 将前面的字段放入buffor (int j = 0; j < i; j++) {buf[j] = val[j];}// 遍历 i 后面的字符while (i < len) {char c = val[i];// 将 oldChar 替换成 newChar 放入bufbuf[i] = (c == oldChar) ? newChar : c;i++;}// 重新通过 new 关键字创建了一个新的字符串,原字符串是不变的。return new String(buf, true);}}return this;}

12、substring() 方法

  ①返回一个从索引 beginIndex 开始一直到结尾的子字符串。
  ②返回一个从索引 beginIndex 开始,到 endIndex 结尾的子字符串。
  源码示例:

public String substring(int beginIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}// 表示从 beginIndex 开始int subLen = value.length - beginIndex;if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}// 如果索引值beginIdex == 0,直接返回原字符串// 如果不等于0,则返回从beginIndex开始,一直到结尾return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);}

13、intern() 方法

  这是一个本地方法:返回String对象在常量池中的引用。详情可以参考这篇文章。

public native String intern();

  调用一个String对象的intern()方法,如果常量池中:
  有,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中)。
  没有,则将该对象添加到池中,并返回池中的引用。

String str1 = \"hello\"; // 字面量 只会在常量池中创建对象String str2 = str1.intern();System.out.println(str1 == str2); //trueString str3 = new String(\"world\"); // new 关键字只会在堆中创建对象String str4 = str3.intern();System.out.println(str3 == str4); // falseString str5 = str1 + str2; // 变量拼接的字符串,会在常量池中和堆中都创建对象String str6 = str5.intern(); // 这里由于池中已经有对象了,返回池中的引用System.out.println(str5 == str6); // trueString str7 = \"hello1\" + \"world1\"; // 常量拼接的字符串,只会在常量池中创建对象String str8 = str7.intern();System.out.println(str7 == str8); // true

三、String 真的不可变吗?

  String 字符串是由许多单个字符组成的,存放在char[] value 字符数组中。
  value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实存放的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。
  代码示例:

public class Main {public static void main(String[] args) throws Exception {String str = \"vae\";System.out.println(str); // vae// 获取String类中名为 value 的字段Field fieldStr = String.class.getDeclaredField(\"value\");// 因为value是private的,这里修改其访问权限fieldStr.setAccessible(true);// 获取str对象上的value属性的值char[] value = (char[]) fieldStr.get(str);// 将第一个字符修改为 V(小写改大写)value[0] = \'V\';System.out.println(str); // Vae}}

  显然:String 被改变了。但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,依然认为 String 类型是不可变的。

  那么,为什么String 类被设计成不可变呢?

  安全:
  ①引发安全问题。比如:数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接;在socket编程中,主机名和端口都是以字符串的形式传入。若改变字符串指向的对象的值,会造成安全漏洞。
  ②保证线程安全。在并发场景下,多个线程同时读写资源时,会引竞态条件,由于 String 是不可变的,不会引发线程的问题而保证了线程。
  ③HashCode。当 String 被创建出来的时候,hashcode也会随之被缓存,hashcode的计算与value有关。若 String 可变,那么 hashcode 也会随之变化,针对于 Map、Set 等容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。
  性能:
  当字符串是不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的String.intern()方法也失效,每次创建新的 String 将在堆内开辟出新的空间,占据更多的内存。

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » JDK1.8源码(三)——java.lang.String类