字符串那些事

1.概述

这周就要刷字符串的题目了,上周在阿里笔试的时候由于自己临场的傻缺,在没有使用IDE的情况下忘记了StringBuffer的api调用,所以这里将字符串方面的内容做一个小总结

2.String

我们都知道String类的底层是一个final类型的char数组,这样婶的:

 /** The value is used for character storage. */
    private final char value[];

final表明这个字符串一旦被创建之后,只能进行一次赋值,之后便不能修改,如果我们之后对字符串对象进行修改,那么都会在字符串常量池中生成一个新的字符串对象,并返回新的引用,所以我们在经常变动字符串内容的时候尽量不要使用String,这样会产生很多没有被引用的字符串对象,可能会提前触发JVM GC,导致系统性能下降


还需要知道的就是String类重写了Object类的equals方法,Object类默认的equals方法比较的是两个引用指向的地址是否相等,而String类重写为比较两个字符串的字面值是否相等,这一点在之前的==和equals 中已经详细说过,这样做的好处就是字符串常量池可以保证字符串的唯一,不必创建很多相同的字符串,而且两个字符串变量之间进行比较时可以直接使用==,比较指向的字符串是否为字符串常量池中同一个,但是这种比较只限于使用类似于String s = "abc"这样进行赋值的字符串,如果要是这样赋值的话:String s = new String("abc"),生成的字符串是在堆空间中的,要使用equals进行比较

3.StringBuffer

在上周的笔试题中我就是采用这个类进行频繁的字符串拼接和比较,但是翻车了,这一次仔细总结一下:

使用StringBuffer stringBuffer = new StringBuffer();创建好一个StringBuffer对象之后,**默认情况下会创建一个长度为16的char数组,**我们来看源码,一共有四个构造方法,分别是:

  • 默认长度为16
  public StringBuffer() {
        super(16);
    }
  • 指定数组大小
 public StringBuffer(int capacity) {
        super(capacity);
    }
  • 将指定的字符串str传进去,并设置初始数组长度为str.length() + 16
public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }
  • 将实现CharSequence接口的子类传进去,并设置初始长度为seq.length() + 16
 public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

这里继续往下探索,那么CharSequence是啥玩意呢,我们来看这个接口的实现类都有哪些:

image

可以看到,和我们目前接触最为密切的只有java.lang包下面的抽象类AbstractStringBuilder,那么AbstractStringBuilder又是做什么的?

使用ctrl + alt + B可以查看实现当前接口的所有子类

我们来看这个抽象类的子类:

image

没错,就是我们正在讲的StrinngBuffer和StringBuilder,所以这个构造方法就可以理解为:将初始数组长度设置为AbstractStringBuilder子类的长度加上16,然后再将AbstractStringBuilder的子类使用append(seq);方法追加进去


在上面的四个构造方法中,我们可以看到里面都调用了super()方法,我们在IDEA中继续跟踪super()方法:

 /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

可以看到直接调用了父类AbstractStringBuilder的构造方法,上面的四个构造方法创建数组的过程同时通过调用父类中的构造方法来完成的。


  • capacity():返回当前数组的容量
@Override
    public synchronized int capacity() {
        return value.length;
    }
  • length():返回当前数组实际字符元素的个数
   public int length() {
        return count;
    }

算了算了,其他常用的方法实在太多了,这里就不一一列举了,需要的时候就去查看一下api文档,以后记住笔试时候能使用本地IDE的情况下就要用,别装逼显自己能耐,以后不要再写stringBuffer.get(),stringBuffer.size()这种沙雕代码了🙂


最后要说的就是线程安全性,在上面的capacity()代码中我们可以看到使用了synchronized关键字,说明这个方法是同步的,可以保证线程安全,在多线程情况下一定要使用这个

3.StringBuilder

和上面的StringBuffer一样,他也是继承于AbstractStringBuilder抽象类,就是一对双胞胎,只不过他是线程不安全的

image

在方法上和上面的StringBuffer几乎一样,至少我还没发现其中的不同之处,可能还是用的比较少

构造方法:

image

我觉得在平时做题过程中使用StringBuilder就足够了,因为编写的都是单线程程序,不会出现线程安全问题,减少了同步方法的使用还可以提高效率,但是在生产环境下,如果不确定具体的使用场景会不会涉及到多线程的情况,一定要使用StringBuffer,这样比较谨慎

4.字符串的连接方式

1.+

在String类的加号运算中实际就是调用StringBuffer底层的append()方法来实现的,例如这个操作:

String s1 = "a";
String s2 = s1 + "b";

就等效于下面这个代码:

String a = "a";
StringBuilder sb = new StringBuilder();
sb.append(a).append("b");
String str = sb.toString();

但并不是说直接用“+”号拼接就可以达到StringBuilder的效率了,因为用“+”号每拼接一次都会新建一个StringBuilder对象,并且最后toString()方法还会生成一个String对象。在循环拼接十万次的时候,就会生成十万个StringBuilder对象,十万个String对象,这简直就是噩梦。


还有一个特殊情况:

String s1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);

在这种情况下,使用String的加号方法是比使用StringBuffer更加好的,这是因为JVM进行了编译优化,将第一条语句直接合并为:

String s1 = “This is only a simple test”;

如果字符串常量中没有的话,只需要创建一个字符串对象就好了,要是已经有那就更好了,而StringBuffe还要进行对象的创建以及三次append操作

但是这种情况只是发生在字面量字符串进行连接的情况, 只要后面要进行连接的内容出现了字符串引用变量,那么直接打回原样,就不进行编译优化了,而是采用上面那种浪费空间和效率的方式

2.String的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);
    }

这个方法就是创建一个足够大的char数组空间,然后将原来的字符串和要拼接的字符串全部放到char数组中,最后创建一个String对象并返回,还是开辟了新的空间

3.StringBuilder/StringBuffer的append()

这两个类实现append的方法都是调用父类AbstractStringBuilder的append方法,只不过StringBuffer是的append方法加了sychronized关键字,因此是线程安全的。append代码如下,他主要也是利用char数组保存字符,类似于ArrayList底层的数组,也是通过ensureCapacityInternal方法来保证数组容量可用还有扩容。

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

他扩容的方法的代码如下,可见,当容量不够的时候,数组容量右移1位(也就是翻倍)再加2,以前的jdk貌似是直接写成int newCapacity = (value.length * 2) + 2,后来优化成了右移,可见,右移的效率还是比直接乘更高的

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

4.性能分析

1.循环拼接字符串
通过实验我们发现,在循环拼接同一个字符串的时候,他们效率的按快慢排序是
StringBulider > StringBuffer >> String.concat > “+”。
StringBulider比StringBuffer更快这个容易理解,因为StringBuffer的方法是sychronized修饰的,同步的时候会损耗掉一些性能。StringBulider和String.concat的区别,主要在扩容上,String.concat是需要多少扩多少,而StringBulider是每次翻两倍,指数级扩容。在10万次拼接中,String.concat需要扩容10万次,而StringBuilder只需要扩容log100000次(大约17次),除此之外,concat每次都会生成一个新的String对象,而StringBuilder则不必,那StringBuilder如此快就不难解释了。至于直接用“+”号连接,之前已经说了,它会产生大量StringBuilder和String对象,当然就最慢了。

2.大量字符串拼接
在只拼接少量字符串的情况下的时候,他们效率的按快慢排序是
String.concat > StringBulider > StringBuffer > “+”。
为什么在拼接少量字符串的时候,String.concat就比StringBuilder快了呢,原因大致有两点,一是StringBuilder的调用栈更深,二是StringBuilder产生的垃圾对象更多,并且它重写的toString方法为了做到不可变性采用了“保护性拷贝”,因此效率不如concat。
详细原因分析参考:concat和StringBuilder性能分析
保护性拷贝见:保护性拷贝
当拼接的字符串少的时候,concat因为上述优势略快,但是当一次性拼接字符串多的时候,StringBuilder的扩容更少优势便会开始展现出来,例如一次拼接8个字符串,concat的效率就已经明显不如StringBuilder了,如下图。
一次拼接8个字符串

结论
从以上分析我们可以得出以下几点结论
1.无论如何直接用“+”号连接字符串都是最慢的
2.在拼接少数字符串(不超过4个)的时候,concat效率是最高的
3.多个字符串拼接的时候,StringBuilder/StringBuffer的效率是碾压的
4.在不需要考虑线程安全问题的时候,使用StringBuilder的效率比StringBuffer更高

参考文章:

https://blog.csdn.net/u012722531/article/details/79055989?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://hadoo666.top/archives/字符串那些事md