关于String字符串的性能优化简例

String字符串是我们日常使用频率最为频繁的数据类型之一,以正确高效的方式使用String字符串,是提升程序运行性能的手段之一。下面将从几个示例中给出具体的使用方式。

String字符串的特性

我们先从String的源代码入手,如下所示:

//源码基于 JDK 1.8
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // String 值的实际存储容器,用final修饰
    private final char value[]; 
    public String() {
        this.value = "".value;
    }
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    // 忽略其他信息
}

从以上源码可以看出,String 这个类 以及他的 value[ ]都被 final修饰了,我们知道,被 final修饰的类是不能被继承的,即其不能拥有子类,再者,其值value[ ]也被final修饰了,而被final修饰的变量称为常量,只能赋值一次;这就是说,String一旦被创建之后,就不能再被修改了。

String为什么不能被修改呢?

String的这个类以及他的值value[ ]都被 final 修饰了,这样做的好处有以下几点:

1.避免网络安全问题

如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

2.线程安全

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

3.处理速度快

String 不可变之后就保证的 hash 值的唯一性,这样它就更加高效,并且更适合做 HashMap 的 key- value 缓存。

4.节约内存

String 的不可变性是它实现字符串常量池的基础,字符串常量池指的是字符串在创建时,先去“常量池”查找是否有此“字符串”,如果有,则不会开辟新空间创作字符串,而是直接把常量池中的引用返回给此对象,这样就能更加节省空间。

示例1:字符串少用“+”号进行拼接

通过上面的内容,我们知道了 String 类是不可变的,那么在使用 String 时就不能频繁的用 “+“ 拼接字符串了,因为这样JVM内存中会增加很多无引用的对象,导致垃圾回收频繁,程序性能会受影响。

官方为我们提供了两种字符串拼加的方案:StringBufferStringBuilder,其中 StringBuilder 为非线程安全的,而 StringBuffer 则是线程安全的,StringBuffer 的拼加方法使用了关键字 synchronized 来保证线程的安全,源码如下:

@Override
public synchronized StringBuffer append(CharSequence s) {
    toStringCache = null;
    super.append(s);
    return this;
}

当然也因为使用 synchronized 修饰,所以 StringBuffer 的拼加性能会比 StringBuilder 低。

我们通过代码测试一下,“+”号拼接跟StringBuilder拼接之间的性能差别:

public class StringTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            // 直接“+”号进行拼接
            long st1 = System.currentTimeMillis(); // 开始时间
            doAdd();
            long et1 = System.currentTimeMillis(); // 结束时间
            System.out.println("String 拼加,执行时间:" + (et1 - st1));
            // 使用StringBuilder进行拼接
            long st2 = System.currentTimeMillis(); // 开始时间
            doAppend();
            long et2 = System.currentTimeMillis(); // 结束时间
            System.out.println("StringBuilder 拼加,执行时间:" + (et2 - st2));
            System.out.println();
        }
    }
    public static String doAdd() {
        String result = "";
        for (int i = 0; i < 1000; i++) {
            result += "String";
        }
        return result;
    }
    public static String doAppend() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            sb.append("String");
        }
        return sb.toString();
    }
}

执行结果如下:

String 拼加,执行时间:325
StringBuilder 拼加,执行时间:1

从结果可以看出,优化前后的性能相差很大。

所以为什么 StringBuilder.append() 方法比 += 的性能高?我们打开源码就知道其中的原因,如下所示为StringBuilder 父类 AbstractStringBuilder 的实现源码:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;//可见value值是可变的
    int count;
    @Override
    public AbstractStringBuilder append(CharSequence s, int start, int end) {
        if (s == null)
            s = "null";
        if ((start < 0) || (start > end) || (end > s.length()))
            throw new IndexOutOfBoundsException(
                "start " + start + ", end " + end + ", s.length() "
                + s.length());
        int len = end - start;
        ensureCapacityInternal(count + len);
        for (int i = start, j = count; i < end; i++, j++)
            value[j] = s.charAt(i);//修改value数组
        count += len;
        return this;
    }
    // 忽略其他信息...
}

由上可见,StringBuilder 使用了父类提供的char[ ]作为自己值的实际存储单元,每次在拼加时只需修改char[ ]数组即可,不用额外去创建新的String对象,所以StringBuilder的性能就会高很多。

示例2:善用String.intern() 方法

善用 String.intern() 方法可以有效的节约字符串所占内存,当调用 intern 方法时,如果字符串常量池中已经包含此字符串,则直接返回此字符串的引用,如果不包含此字符串,先将字符串添加到常量池中,再返回此对象的引用。

但是性能效率相比于直接new()创建会有所下降,可以算是一种时间换空间的方法,所以需要考虑具体时间成本以及空间成本对于企业的权重,即要根据实际业务场景需求进行选择使用~

示例3:谨慎使用String.split() 方法

在实际应用场景中,因为 Split 方法大多数情况下使用的是正则表达式,这种分割方式本身没有什么问题,但是由于正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。

为什么回溯问题会引起CPU使用居高不下?首先,Java 正则表达式使用的引擎实现是 NFA(Non deterministic Finite Automaton,不确定型有穷自动机)自动机,这种正则表达式引擎在进行字符匹配时会发生回溯(backtracking),而一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和表达式的复杂度

回溯问题的一个示例:

text = "abbc";
regex = "ab{1,3}c";

上面的这个例子的目的比较简单,匹配以 a 开头,以 c 结尾,中间有 1-3 个 b 字符的字符串。

而NFA 引擎对其解析的过程是这样子的:

  • 首先,读取正则表达式第一个匹配符 a 和 字符串第一个字符 a 比较,匹配上了,于是读取正则表达式第二个字符;
  • 读取正则表达式第二个匹配符 b{1,3} 和字符串的第二个字符 b 比较,匹配上了。但因为 b{1,3} 表示 1-3 个 b 字符串,以及 NFA 自动机的贪婪特性(也就是说要尽可能多地匹配),所以此时并不会再去读取下一个正则表达式的匹配符,而是依旧使用 b{1,3} 和字符串的第三个字符 b 比较,发现还是匹配上了,于是继续使用 b{1,3} 和字符串的第四个字符 c 比较,发现不匹配了,此时就会发生回溯
  • 发生回溯后,我们已经读取的字符串第四个字符 c 将被吐回去,指针回到第三个字符串的位置,之后程序读取正则表达式的下一个操作符 c,然后再读取当前指针的下一个字符 c 进行对比,发现匹配上了,于是读取下一个操作符,然后发现已经结束了。

所以如果正则表达式编写不当,会使NFA引擎频繁发生回溯,最终导致CPU占用过高、消耗时间过多等问题,而且当需要匹配的字符串特别长时候,一个一个字符回溯回去,更会加重性能下降的现象。

所以我们应该慎重使用 Split() 方法,我们可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。如果实在无法满足需求,就在使用 Split() 方法时,对回溯问题加以重视就可以了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值