String性能提升10倍的几个方法!(源码+原理分析)上

简介: String性能提升10倍的几个方法!(源码+原理分析)

String 类型是我们使用最频繁的数据类型,没有之一。那么提高 String 的运行效率,无疑是提升程序性能的最佳手段。


我们本文将从 String 的源码入手,一步步带你实现字符串优化的小目标。不但教你如何有效的使用字符串,还为你揭晓这背后的深层次原因


本文涉及的知识点,如下图所示:


image.png


在看如何优化 String 之前,我们先来了解一下 String 的特性,毕竟知己知彼,才能百战不殆。


字符串的特性


想要了解 String 的特性就必须从它的源码入手,如下所示:


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


从他的源码我们可以看出,String 类以及它的 value[] 属性都被 final 修饰了,其中 value[] 是实现字符串存储的最终结构,而  final 则表示“最后的、最终的”。


我们知道,被 final 修饰的类是不能被继承的,也就是说此类将不能拥有子类,而被 final 修饰的变量即为常量,它的值是不能被改变的。这也就说当 String 一旦被创建之后,就不能被修改了


String 为什么不能被修改?


String 的类和属性 value[] 都被定义为 final 了,这样做的好处有以下三点:


  1. 安全性:当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题,所以迫使 String 设计为 final 类的一个重要原因就是出于安全考虑;


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


  1. 节约内存:String 的不可变性是它实现字符串常量池的基础,字符串常量池指的是字符串在创建时,先去“常量池”查找是否有此“字符串”,如果有,则不会开辟新空间创作字符串,而是直接把常量池中的引用返回给此对象,这样就能更加节省空间。例如,通常情况下 String 创建有两种方式,直接赋值的方式,如 String str="Java";另一种是 new 形式的创建,如 String str = new String("Java")。当代码中使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。String str = new String("Java") 这种方式,首先在编译类文件时,“Java”常量字符串将会放入到常量结构中,在类加载时,“Java”将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的“Java”字符串,在堆内存中创建一个 String 对象,最后 str 将引用 String 对象。


1.不要直接+=字符串


通过上面的内容,我们知道了 String 类是不可变的,那么在使用 String 时就不能频繁的 += 字符串了。


优化前代码


public static String doAdd() {
    String result = "";
    for (int i = 0; i < 10000; i++) {
        result += (" i:" + i);
    }
    return result;
}


有人可能会问,我的业务需求是这样的,那我该如何实现?


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


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


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


那我们就用 StringBuilder 来实现字符串的拼加,优化后代码


public static String doAppend() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++) {
        sb.append(" i:" + i);
    }
    return sb.toString();
}


我们通过代码测试一下,两个方法之间的性能差别:


public class StringTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            // String
            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 < 10000; i++) {
            result += ("Java中文社群:" + i);
        }
        return result;
    }
    public static String doAppend() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append("Java中文社群:" + i);
        }
        return sb.toString();
    }
}


以上程序的执行结果如下:


String 拼加,执行时间:429

StringBuilder 拼加,执行时间:1

String 拼加,执行时间:325

StringBuilder 拼加,执行时间:1

String 拼加,执行时间:287

StringBuilder 拼加,执行时间:1

String 拼加,执行时间:265

StringBuilder 拼加,执行时间:1

String 拼加,执行时间:249

StringBuilder 拼加,执行时间:1


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


注意:此性能测试的结果与循环的次数有关,也就是说循环的次数越多,他们性能相除的结果也越大。


接下来,我们要思考一个问题:为什么 StringBuilder.append() 方法比 += 的性能高?而且拼接的次数越多性能的差距也越大?


当我们打开 StringBuilder 的源码,就可以发现其中的“小秘密”了,StringBuilder 父类 AbstractStringBuilder 的实现源码如下:


abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] 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);
        count += len;
        return this;
    }
    // 忽略其他信息...
}


而 StringBuilder 使用了父类提供的 char[] 作为自己值的实际存储单元,每次在拼加时会修改 char[] 数组,StringBuilder toString() 源码如下:


@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}


综合以上源码可以看出:StringBuilder 使用了 char[] 作为实际存储单元,每次在拼加时只需要修改 char[] 数组即可,只是在 toString() 时创建了一个字符串;而 String 一旦创建之后就不能被修改,因此在每次拼加时,都需要重新创建新的字符串,所以 StringBuilder.append() 的性能就会比字符串的 += 性能高很多

相关文章
|
4月前
|
Kubernetes jenkins 持续交付
从代码到k8s部署应有尽有系列-java源码之String详解
本文详细介绍了一个基于 `gitlab + jenkins + harbor + k8s` 的自动化部署环境搭建流程。其中,`gitlab` 用于代码托管和 CI,`jenkins` 负责 CD 发布,`harbor` 作为镜像仓库,而 `k8s` 则用于运行服务。文章具体介绍了每项工具的部署步骤,并提供了详细的配置信息和示例代码。此外,还特别指出中间件(如 MySQL、Redis 等)应部署在 K8s 之外,以确保服务稳定性和独立性。通过本文,读者可以学习如何在本地环境中搭建一套完整的自动化部署系统。
73 0
|
20天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
45 5
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
65 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
1月前
|
JavaScript 前端开发 开发者
|
3月前
|
JavaScript 前端开发 API
javaScript中常用的String方法以及注意点总结
本文总结了JavaScript中常用的String对象的方法及其注意事项,包括大小写转换、字符获取、子字符串截取、字符串拼接、去除空格、替换、分割以及查找字符串中字符的索引等操作。提供了每种方法的使用示例代码,帮助理解它们的具体用法和差异。
44 2
|
4月前
|
JavaScript 算法 前端开发
JS算法必备之String常用操作方法
这篇文章详细介绍了JavaScript中字符串的基本操作,包括创建字符串、访问特定字符、字符串的拼接、位置查找、大小写转换、模式匹配、以及字符串的迭代和格式化等方法。
JS算法必备之String常用操作方法
|
4月前
|
XML Java API
List与String相互转化方法汇总
本文汇总了List与String相互转化的多种方法,包括使用`String.join()`、`StringBuilder`、Java 8的Stream API、Apache Commons Lang3的`StringUtils.join()`以及Guava的`Joiner.on()`方法实现List转String;同时介绍了使用`split()`方法、正则表达式、Apache Commons Lang3的`StringUtils.split()`及Guava的`Splitter.on()`方法实现String转List。
143 1
List与String相互转化方法汇总
|
3月前
|
Java 索引
java基础扫盲-String类常用的方法
java基础扫盲-String类常用的方法
|
3月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
44 0
java基础(13)String类
|
20天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2