一、概述

1、介绍

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

二、类源码

1、类声明

源码示例:

 1  * @author  Lee Boynton
2 * @author Arthur van Hoff
3 * @author Martin Buchholz
4 * @author Ulf Zibis
5 * @see java.lang.Object#toString()
6 * @see java.lang.StringBuffer
7 * @see java.lang.StringBuilder
8 * @see java.nio.charset.Charset
9 * @since JDK1.0
10 */
11 public final class String
12 implements java.io.Serializable, Comparable<String>, CharSequence {}

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

2、类属性

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

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

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

3、类构造器

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

4、equals() 方法

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

 1 public boolean equals(Object anObject) {
2 // 如果引用相同,则为true
3 if (this == anObject) {
4 return true;
5 }
6 if (anObject instanceof String) {
7 String anotherString = (String)anObject;
8 int n = value.length;
9 // 判断入参与当前 String 长度是否一致
10 if (n == anotherString.value.length) {
11 char v1[] = value;
12 char v2[] = anotherString.value;
13 int i = 0;
14
15 // 循环判断两个字符串的每一个字符是否相同
16 while (n-- != 0) {
17 if (v1[i] != v2[i])
18 return false;
19 i++;
20 }
21 return true;
22 }
23 }
24 return false;
25 }

5、hashCode() 方法

  源码示例:

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

  这个方法不难读懂,中间的 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() 方法

  源码示例:

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

7、compareTo() 和 compareToIgnoreCase() 方法

  源码示例:

 1 public int compareTo(String anotherString) {
2 int len1 = value.length;
3 int len2 = anotherString.value.length;
4
5 // 取当前字符串与入参字符串的长度最小值
6 int lim = Math.min(len1, len2);
7 char v1[] = value;
8 char v2[] = anotherString.value;
9
10 int k = 0;
11 // 循环比较两个字符串的 字符
12 while (k < lim) {
13 char c1 = v1[k];
14 char c2 = v2[k];
15
16 // 如果不相等了,返回他们的 ASCII 差值
17 if (c1 != c2) {
18 return c1 - c2;
19 }
20 k++;
21 }
22
23 // 若 lim 的长度值都相同,返回两个字符串长度之差。
24 return len1 - len2;
25 }
26
27
28 public int compareToIgnoreCase(String str) {
29 return CASE_INSENSITIVE_ORDER.compare(this, str);
30 }

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

8、concat() 方法

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

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

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

9、indexOf() 方法

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

 1 public int indexOf(int ch) {
2 // 从第一个字符开始搜索
3 return indexOf(ch, 0);
4 }
5
6 // 从第 fromIndex 个字符开始搜索
7 public int indexOf(int ch, int fromIndex) {
8 final int max = value.length;
9 // 小于0, 默认从 0 开始搜索
10 if (fromIndex < 0) {
11 fromIndex = 0;
12 } else if (fromIndex >= max) {
13 // Note: fromIndex might be near -1>>>1.
14
15 // 大于了字符串的长度,默认直接找不到,返回 -1
16 return -1;
17 }
18
19 //一个char占用两个字节,如果ch小于2的16次方(65536),绝大多数字符都在此范围内
20 if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
21 // handle most cases here (ch is a BMP code point or a
22 // negative value (invalid code point))
23 final char[] value = this.value;
24
25 // 循环从fromIndex开始查找每一个字符是否是ch
26 for (int i = fromIndex; i < max; i++) {
27 if (value[i] == ch) {
28 return i;
29 }
30 }
31
32 // 找不到,返回 -1
33 return -1;
34 } else {
35 // 当字符大于65536,判断是否是有效字符,然后依次进行比较
36 return indexOfSupplementary(ch, fromIndex);
37 }
38 }

10、split() 方法

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

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

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

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

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

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

  源码示例:

 1 public String[] split(String regex) {
2 return split(regex, 0);
3 }
4
5 public String[] split(String regex, int limit) {
6
7 /* 1、单个字符,且不是".$|()[{^?*+\\"其中一个
8 * 2、两个字符,第一个是"\",第二个大小写字母或者数字
9 */
10 /* fastpath if the regex is a
11 (1)one-char String and this character is not one of the
12 RegEx's meta characters ".$|()[{^?*+\\", or
13 (2)two-char String and the first char is the backslash and
14 the second is not the ascii digit or ascii letter.
15 */
16 char ch = 0;
17 if (((regex.value.length == 1 &&
18 ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
19 (regex.length() == 2 &&
20 regex.charAt(0) == '\\' &&
21 (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
22 ((ch-'a')|('z'-ch)) < 0 &&
23 ((ch-'A')|('Z'-ch)) < 0)) &&
24 (ch < Character.MIN_HIGH_SURROGATE ||
25 ch > Character.MAX_LOW_SURROGATE))
26 {
27 int off = 0;
28 int next = 0;
29
30 // 判断模式
31 boolean limited = limit > 0;
32 ArrayList<String> list = new ArrayList<>();
33 while ((next = indexOf(ch, off)) != -1) {
34 // 当参数limit <= 0 或者 集合list的长度小于 limit-1
35 if (!limited || list.size() < limit - 1) {
36 list.add(substring(off, next));
37 off = next + 1;
38 } else { // last one
39 //assert (list.size() == limit - 1);
40 // 判断最后一个list.size() == limit - 1
41 list.add(substring(off, value.length));
42 off = value.length;
43 break;
44 }
45 }
46 // If no match was found, return this
47 // 如果没有一个能匹配的,返回一个新的字符串,内容和原来的一样
48 if (off == 0)
49 return new String[]{this};
50
51 // Add remaining segment
52 // 当 limit<=0 时,limited==false,或者集合的长度 小于 limit时,截取添加剩下的字符串
53 if (!limited || list.size() < limit)
54 list.add(substring(off, value.length));
55
56 // Construct result
57 // 当 limit == 0 时,如果末尾添加的元素为空(长度为0),则集合长度不断减1,直到末尾不为空
58 int resultSize = list.size();
59 if (limit == 0) {
60 while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
61 resultSize--;
62 }
63 }
64 String[] result = new String[resultSize];
65 return list.subList(0, resultSize).toArray(result);
66 }
67 return Pattern.compile(regex).split(this, limit);
68 }

11、replace() 和 replaceAll() 方法

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

 1 public String replace(char oldChar, char newChar) {
2 if (oldChar != newChar) {
3 int len = value.length;
4 int i = -1;
5 char[] val = value; /* avoid getfield opcode */
6
7 // 找到 value 中的 oldChar 起始位置
8 while (++i < len) {
9 if (val[i] == oldChar) {
10 break;
11 }
12 }
13
14 if (i < len) {
15 char buf[] = new char[len];
16 // 将前面的字段放入buf
17 for (int j = 0; j < i; j++) {
18 buf[j] = val[j];
19 }
20 // 遍历 i 后面的字符
21 while (i < len) {
22 char c = val[i];
23 // 将 oldChar 替换成 newChar 放入buf
24 buf[i] = (c == oldChar) ? newChar : c;
25 i++;
26 }
27 // 重新通过 new 关键字创建了一个新的字符串,原字符串是不变的。
28 return new String(buf, true);
29 }
30 }
31 return this;
32 }

12、substring() 方法

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

 1 public String substring(int beginIndex) {
2 if (beginIndex < 0) {
3 throw new StringIndexOutOfBoundsException(beginIndex);
4 }
5
6 // 表示从 beginIndex 开始
7 int subLen = value.length - beginIndex;
8 if (subLen < 0) {
9 throw new StringIndexOutOfBoundsException(subLen);
10 }
11
12 // 如果索引值beginIdex == 0,直接返回原字符串
13 // 如果不等于0,则返回从beginIndex开始,一直到结尾
14 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
15 }

13、intern() 方法

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

1 public native String intern();

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

 1 String str1 = "hello"; // 字面量 只会在常量池中创建对象
2 String str2 = str1.intern();
3 System.out.println(str1 == str2); //true
4
5 String str3 = new String("world"); // new 关键字只会在堆中创建对象
6 String str4 = str3.intern();
7 System.out.println(str3 == str4); // false
8
9 String str5 = str1 + str2; // 变量拼接的字符串,会在常量池中和堆中都创建对象
10 String str6 = str5.intern(); // 这里由于池中已经有对象了,返回池中的引用
11 System.out.println(str5 == str6); // true
12
13 String str7 = "hello1" + "world1"; // 常量拼接的字符串,只会在常量池中创建对象
14 String str8 = str7.intern();
15 System.out.println(str7 == str8); // true

三、String 真的不可变吗?

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

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

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

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

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

JDK1.8源码(三)——java.lang.String类的更多相关文章

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

    String 类也是java.lang 包下的一个类,算是日常编码中最常用的一个类了,那么本篇博客就来详细的介绍 String 类. 1.String 类的定义 public final class ...

  2. JDK1.8源码(一)——java.lang.Object类

    本系列博客将对JDK1.8版本的相关类从源码层次进行介绍,JDK8的下载地址. 首先介绍JDK中所有类的基类——java.lang.Object. Object 类属于 java.lang 包,此包下 ...

  3. JDK1.8源码(二)——java.lang.Integer 类

    上一篇博客我们介绍了 java.lang 包下的 Object 类,那么本篇博客接着介绍该包下的另一个类 Integer.在前面 浅谈 Integer 类 博客中我们主要介绍了 Integer 类 和 ...

  4. JDK1.8源码(二)——java.lang.Integer类

    一.初识 1.介绍 int 是Java八大基本数据类型之一,占据 4 个字节,范围是 -2^31~2^31 - 1,即 -2147483648~2147483647.而 Integer 是 int 包 ...

  5. JDK1.8源码(八)——java.lang.ThreadLocal类

    https://www.cnblogs.com/xdd666/p/14734047.html ThreadLocal https://www.cnblogs.com/yanfei1819/p/1473 ...

  6. JDK1.8源码(五)——java.util.Vector类

    JDK1.8源码(五)--java.lang. https://www.cnblogs.com/IT-CPC/p/10897559.html

  7. Java源码学习 -- java.lang.String

    java.lang.String是使用频率非常高的类.要想更好的使用java.lang.String类,了解其源代码实现是非常有必要的.由java.lang.String,自然联想到java.lang ...

  8. 从源码分析java.lang.String.isEmpty()

    今天在写代码的时候用到了java.lang.String.isEmpty()的这个方法,之前也用过,今天突发奇想,就看了看源码,了解了解它的实现方法,总结出来,大家可以交流交流. 通常情况下,我们使用 ...

  9. JDK1.8源码(四)——java.util.Arrays类

    一.概述 1.介绍 Arrays 类是 JDK1.2 提供的一个工具类,提供处理数组的各种方法,基本上都是静态方法,能直接通过类名Arrays调用. 二.类源码 1.asList()方法 将一个泛型数 ...

随机推荐

  1. JSTL标签工具类

    一.介绍: ​ 1.Jsp Standrad Tag Lib: JSP中标准的标签工具类 ​ 2.由 sun公司 提供 ​ 3.组成:(1)核心标签:Java在jsp上基本功能进行封装 if whil ...

  2. one_gadget的一些姿势

    概要 one_gadget是libc中存在的一些执行execve("/bin/sh", NULL, NULL)的片段,当可以泄露libc地址,并且可以知道libc版本的时候,可以使 ...

  3. mac Charles抓包

    手机配置http代理 1.配置iPhone或Android 的wifi配置.首先保证Mac电脑和手机是在同一个局域网内. 2.设置手机wifi配置,在HTTP代理中选择手选代理,服务器填写Mac的IP ...

  4. spring cloud 知识总结

    ### 单体应用存在的问题 - 随着业务的发展,开发变得越来越复杂.- 修改.新增某个功能,需要对整个系统进行测试.重新部署.- 一个模块出现问题,很可能导致整个系统崩溃.- 多个开发团队同时对数据进 ...

  5. .Net Core 踩坑记录--无法逐步调试类库文件

    前提 新建类库 在新项目中引用该类库 将类库对应的.PDB文件 拷贝至新项目的bin文件夹下 结果 无法进行跟踪调试 狗带 分析与解决 1: 打开 工具-->选项-->调试 2: 常规-- ...

  6. Socket 网络编程和IO模型

    最近做了一个织机数据采集的服务器程序. 结构也非常简单,织机上的嵌入式设备,会通过Tcp 不停的往服务器发送一些即时数据.织机大改有个几十台到几百台不定把 刨去业务,先分析一下网络层的大概情况.每台织 ...

  7. Sublime Text3 显示左侧的目录树

    file->open folder选择一个文件夹,打开一个新窗口把原来的关掉 View->Sise Bar->Hide Side Bar就可以了

  8. 深入理解Java类加载器(二):线程上下文类加载器

    摘要: 博文<深入理解Java类加载器(一):Java类加载原理解析>提到的类加载器的双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式.在Java ...

  9. K8s 系列(三) - 如何配置 etcd https 证书?

    在 K8s 中,kube-apiserver 使用 etcd 对 REST object 资源进行持久化存储,本文介绍如何配置生成自签 https 证书,搭建 etcd 集群给 apiserver 使 ...

  10. C#中的文本到语音

    本演示说明了如何使用c#.net Windows Forms应用程序中的system.speech库将文本转换为语音.Microsoft .NET框架提供System.Speech.Synthesis ...