博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
String、StringBuilder、StringBuffer、StringJoiner源码分析
阅读量:3943 次
发布时间:2019-05-24

本文共 10698 字,大约阅读时间需要 35 分钟。

String类特点

  • 字符串内容一旦声明则不可改变(final修饰),String类对象内容的改变是依靠引用关系的变更实现的。
  • 正是因为字符串内容不可改变,所以字符串是可以共享使用的,常量池。也是线程安全的。
  • 字符串底层是final修饰的char[]数组,(JDK9之后是byte[]字节数组)
  • String类对象的相等判断使用equals() 方法完成,重写了。
  • String类有两种实例化方式,使用直接赋值可以不产生垃圾空间,并且可以自动入池,不要使用构造方法完成。

String中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; }

String中有一个字段hash来存储该串的哈希值,在第一次调用hashCode方法时,字符串的哈希值被计算并且赋值给hash字段。之后再调用hashCode方法便可以直接取hash字段返回。

String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位字符的ASCII值进行计算,用自然溢出来等效取模。

哈希计算公式可以记为s[0]*31(n-1)+s[1]*31(n-2)+…+s[n-1]。

主要原因是因为31是一个奇素数,所以31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算块很多。

String类可以被继承吗

String类底层是一个final的字节数组,类被final修饰,final修饰的类不能被继承。

String类+操作

public static void main(String[] args) {
String s1 = "a" + "b" + "c"; // 字面量+得到abc,存放到常量池 String s2 = "abc"; // abc存放在常量池,直接将常量池的地址返回 /** * 最终java编译成.class,再执行.class */ System.out.println(s1 == s2); // true,因为存放在字符串常量池 System.out.println(s1.equals(s2)); // true String a = "abc"; String b = "def"; //自JDK1.5之后,Java虚拟机执行字符串的+操作时,内部实现也是StringBuilder,之前采用StringBuffer实现。 //+操作时,是在堆创建了对象 String c = a + b; System.out.println(c == "abcdef"); //false System.out.println(a == "abc"); //true final String a1 = "abc"; final String b1= "def"; //a1,b1常量编译期就赋值了,常量池,a1 + b1也是编译器确定赋值给了c1,常量池 String c1 = a1 + b1; System.out.println(c1 == "abcdef"); //true System.out.println(a1 == "abc"); //true final String a2 = "abc"; String b2= "def"; //也是StringBuilder String c2 = a2 + b2; System.out.println(c2 == "abcdef"); //false System.out.println(a2 == "abc"); //true }

intern()方法

JDK6中

String s = new String("1");  // 在常量池中已经有了s.intern(); // 将该对象放入到常量池。但是调用此方法没有太多的区别,因为已经存在了1String s2 = "1";System.out.println(s == s2); // false, 堆vs池String s3 = new String("1") + new String("1");s3.intern(); // 字符串常量池没有11,则创建一个11放到字符串常量池中String s4 = "11";System.out.println(s3 == s4); // false

输出结果

falsefalse

为什么对象会不一样呢?

  • 一个是new创建的对象,一个是常量池中的对象,显然不是同一个

如果是下面这样的,那么就是true

String s = new String("1");s = s.intern(); // 字符串常量池有1,则直接返回常量池中的1String s2 = "1";System.out.println(s == s2); // true

而对于下面的来说,因为 s3变量记录的地址是 new String(“11”),然后这段代码执行完以后,常量池中不存在 “11”,这是JDK6的关系,然后执行 s3.intern()后,就会在常量池中生成 “11”,最后 s4用的就是s3的地址

为什么最后输出的 s3 == s4 会为false呢?

这是因为在JDK6中创建了一个新的对象 “11”,也就是有了新的地址, s2 = 新地址

而在JDK7中,在JDK7中,并没有创新一个新对象,而是指向常量池中的新对象

JDK7/8中

String s = new String("1");s.intern(); // 字符串常量池建立地址引用指向sString s2 = "1";System.out.println(s == s2); // trueString s3 = new String("1") + new String("1");s3.intern(); // 字符串常量池建立地址引用指向s3String s4 = "11";System.out.println(s3 == s4); // true

JDK1.6中,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址

JDK1.7起,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址

JDK1.6和JDK1.7String的intern()变化本质原因是字符串常量池位置的变化,JDK7后字符串常量池是在堆中,以前是在方法区中。JDK7后,intern(),如果池中不存在该字符串,不会新建该字符串,而是在常量池建立引用指向堆中该字符串对象。所以JDK7的intern()后都是相同的对象地址,这是为了节省内存空间,因为字符串常量池已经属于堆了。JDK6则会在常量池创建新的字符串。

new String(“a”) + new String(“b”) 会创建几个对象

public class StringNewTest {
public static void main(String[] args) {
String str = new String("a") + new String("b"); }}

字节码文件为

0 new #2 
3 dup 4 invokespecial #3
> 7 new #4
10 dup11 ldc #5
13 invokespecial #6
>16 invokevirtual #7
19 new #4
22 dup23 ldc #8
25 invokespecial #6
>28 invokevirtual #7
31 invokevirtual #9
34 astore_135 return

我们创建了6个对象

  • 对象1:new StringBuilder()
  • 对象2:new String(“a”)
  • 对象3:常量池的 a
  • 对象4:new String(“b”)
  • 对象5:常量池的 b
  • 对象6:toString中会创建一个 new String(“ab”)
    • 调用toString方法,不会在常量池中生成ab

StringBuilder和StringBuffer的区别

StringBuilder是线程不安全的(线程同步访问的时候会出问题),但是效率相对较高。

String类型使用加号进行拼接字符串的时候,会产生很多临时字符串对象,底层是StringBuilder拼接。

StringBuffer是线程安全的。(StringBUffer只会产生一个对象)

StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。

StringBuilder线程不安全原理

多线程案例:

public class StringBuilderDemo {
public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override public void run() {
for (int j = 0; j < 1000; j++){
stringBuilder.append("a"); } } }).start(); } Thread.sleep(100); System.out.println(stringBuilder.length()); } }

这段代码创建了10个线程,每个线程循环1000次往StringBuilder对象里面append字符。正常情况下代码应该输出10000,但是实际运行会输出什么呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u717XSwg-1618559289762)(./images/113.jpg)]

结果小于预期的10000,并且还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)。

结果分析:

StringBuilder和StringBuffer都继承了AbstractStringBuilder,AbstractStringBuilder有两个成员变量

//存储字符串的具体内容  char[] value;  //已经使用的字符数组的数量  int count;

StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法

public AbstractStringBuilder append(String str) {
if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }

我们先不管代码的第五行和第六行干了什么,直接看第七行,count += len不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比10000小的原因。

异常分析:

为什么会抛出ArrayIndexOutOfBoundsException异常

我们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。

private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); } void expandCapacity(int minimumCapacity) {
//计算新的容量 int newCapacity = value.length * 2 + 2; ... value = Arrays.copyOf(value, newCapacity); }

扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arrayCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。

AbstractStringBuilder的append()方法源码的第六行,是将String对象(新增的字符串)里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下:

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
... System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); }

拷贝流程见下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECrYJTuP-1618559289765)(./images/114.jpg)]

假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了第五行的ensureCapacityInternal()方法,此刻count=5。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MwpYxcQk-1618559289767)(./images/115.jpg)]

这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZrXRHqYG-1618559289770)(./images/116.png)]

线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。

StringBuffer线程安全原理

//安全是因为方法加了synchronized	public AbstractStringBuilder append(String str) {
if (str == null) return appendNull(); int len = str.length(); //扩大char数组大小 ensureCapacityInternal(count + len); //getChars方法是将此字符串中的字符复制到目标字符数组 //-->底层是System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);是一个native方法,帮我们把新加的字符串this添加到目标字符串value后 str.getChars(0, len, value, count); count += len; return this; }

StringJoiner拼接类

Java8新增辅助类,新特性之一。

StringJoiner替代StringBuilder,支持分割,可以添加分隔符、前缀、后缀。

// 效果StringJoiner sj = new StringJoiner(",");IntStream.range(1,10).forEach(i->sj.add(i+""));System.out.println(sj.toString());// 构造参数,分隔符、前缀、后缀StringJoiner sj3 = new StringJoiner(",", "p ", " s");IntStream.range(20, 30).forEach(i->sj3.add(i+""));System.out.println(sj3.toString());StringJoiner sj4 = new StringJoiner(".","pp ", " ss");IntStream.range(30,40).forEach(i->sj4.add(i+""));// 合并,第一个sj3的前缀后缀为准System.out.println(sj3.merge(sj4));
// 结果1,2,3,4,5,6,7,8,9p 20,21,22,23,24,25,26,27,28,29 sp 20,21,22,23,24,25,26,27,28,29,30.31.32.33.34.35.36.37.38.39 s

其它功能:

  • setEmptyValue, 默认情况下的emptyValue是前缀加后缀, 用户可自定义emptyValue
  • merge(StringJoiner other),合并另外一个joiner
  • length, 当前长度,为空看emptyValue的长度

底层源码:

// 成员变量// 前缀private final String prefix;// 分隔符private final String delimiter;// 后缀private final String suffix;// 字符串值,基于StringBuilder实现private StringBuilder value;// 空值,也就是未append字符串的初始值private String emptyValue;// 构造函数public StringJoiner(CharSequence delimiter,                        CharSequence prefix,                        CharSequence suffix) {
.... // make defensive copies of arguments this.prefix = prefix.toString(); this.delimiter = delimiter.toString(); this.suffix = suffix.toString(); // 构造时就直接将emptyValue拼接好了,默认前缀和后缀 this.emptyValue = this.prefix + this.suffix;}// 添加元素public StringJoiner add(CharSequence newElement) {
prepareBuilder().append(newElement); return this;}// append添加字符串底层代码,前缀和分隔符处理private StringBuilder prepareBuilder() {
// 从构造函数和类变量的声明可以看出,没有添加元素前stringbuilder是没有初始化的 if (value != null) {
// 已经有元素存在的情况下,添加元素前先将分隔符添加进去 value.append(delimiter); } else {
// 没有元素存在的情况下先把前缀加进去 value = new StringBuilder().append(prefix); } return value;}
// toString方法public String toString() {
if (value == null) {
// 这里如果没有自定义空值就是默认前缀+后缀 return emptyValue; } else {
// 为什么不直接value.toString()+suffix????? if (suffix.equals("")) {
return value.toString(); } else {
int initialLength = value.length(); // 返回完整字符串 String result = value.append(suffix).toString(); // value恢复到没有后缀时的字符串 value.setLength(initialLength); return result; } }}// value去掉后缀,方便后面继续添加字符public void setLength(int newLength) {
if (newLength < 0) throw new StringIndexOutOfBoundsException(newLength); ensureCapacityInternal(newLength); if (count < newLength) {
Arrays.fill(value, count, newLength, '\0'); } count = newLength;}
// 合并方法,前缀后缀保留调用merge一方public StringJoiner merge(StringJoiner other) {
Objects.requireNonNull(other); if (other.value != null) {
final int length = other.value.length(); // 下面这段注释是说避免merge(this)时受影响,为什么? // lock the length so that we can seize the data to be appended // before initiate copying to avoid interference, especially when // merge 'this' StringBuilder builder = prepareBuilder(); // stringBuilder的append方法,添加指定索引范围的字符串,也就是忽略other的前缀,起始prefix.length(),一直到尾部结束,后缀不在value上的 builder.append(other.value, other.prefix.length(), length); } return this;}

参考:

转载地址:http://qviwi.baihongyu.com/

你可能感兴趣的文章
判断二叉树是否有从根节点到叶子节点的节点值之和等于sum的路径
查看>>
反转字符串
查看>>
环形链表
查看>>
删除链表的倒数第N个节点
查看>>
回文链表
查看>>
容器盛水问题
查看>>
滑动窗口最大值
查看>>
win7 文件删除后要刷新后才会消失
查看>>
用ffmpeg转多音轨的mkv文件
查看>>
ubuntu12.04 安装VLC,在root用户下不能使用的问题
查看>>
简单而又完整的Makefile
查看>>
GNU/Linux下如何卸载源码安装的软件
查看>>
ffmpeg 常用 命令随手记
查看>>
av_seek_frame中flags值的意义
查看>>
git 学习笔记
查看>>
C++类中的static的用法
查看>>
vector 释放内存 swap
查看>>
在linux下新增一块硬盘的操作。(包含大于2T的硬盘在linux下挂载操作)
查看>>
在32位系统中使用fseek和lseek或fwrite、write写大文件时,最大只能写2G左右的解决办法
查看>>
整理华为C/C++编码规范
查看>>