前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >字符串优化处理

字符串优化处理

原创
作者头像
七七分享
修改2019-05-20 10:33:07
7030
修改2019-05-20 10:33:07
举报
文章被收录于专栏:RelaxHeart网聊聊Java

原文:https://www.relaxheart.cn/to/master/blog?uuid=87

String对象及特点

String对象时Java语言中重要的数据类型,但它并不是Java的基本数据类型。在C语言中对字符串的处理通常做法时使用char[],但这这种方式的弊端很明显,数组本身无法封装字符串的操作所需的基本方法。如下图所示,Java的String类型基本实现由3部分组成:char数组、偏移量和String的长度。

String.png
String.png

这里有一个点需要提一下:String的真实长度还需要由偏移量和长度在这个char数组中进行定位和截取,因为待会会提到String.subString()方法导致内存泄漏的问题,它的根因就在这,先提前抛出来。

在Java语言中,Java的设计者对String对象进行了大量的优化,其主要表现在一下三点:

(1)不变性;

(2)针对常量池的优化;

(3)类的final定义

不变性

不变性是指String对象一旦生成,则不能再对它进行改变。String的这个特性可以泛化成不变模式,即一个对象的状态被创建之后就不再发生变化。不变模式的主要作用在于当一个对象需要被多线程共享,并且频繁访问时,可以省略同步和锁等待的时间,从而大幅提高系统性能。

针对常量池的优化

当两个String对象拥有相同 的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个计数可以大幅度节省内存空间。

代码语言:txt
复制
    public static void main(String[] args) {
        String str1 = "aaa";
        String str2 = "aaa";
        String str3 = new String("aaa");

        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
        System.out.println(str1 == str3.intern());
    }

运行结果:

代码语言:txt
复制
true
false
true

Process finished with exit code 0

上述代码显示str1和str2引用了相同的地址,但是str3却重新开辟了一块内存空间。但即便如此,str3在常量池中的位置和str1还是一样的,也就是说,虽然str3单独占用了堆空间,但是它所指向的实体和str1完全一样。代码中最后一行的intern()方法返回的是String对象在常量池的引用.

String内存分配方式.png
String内存分配方式.png
类的final定义

除以上2点,final类型定义也是String对象的重要特点,作为final类的String对象在系统中不可能有任何子类,这是对系统安全行的保护。同时,在JDK1.5版本之前的任何环境使用final定义,有助于帮助JVM寻找机会,内联所有的final方法,从而提高系统环境。但是这种优化在JDK1.5之后,效果并不明显了。

subString()方法的内存泄露

截取字符串是字符串操作中最常用的操作之一,在Java中,String类提供了两个截取字符串的方法:

代码语言:txt
复制
public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)

以第2个为例,它返回源字符串中的以beginIndex开始,endIndex为止的字符串。然而,这个方法在JDK中的实现存在严重的内存泄露问题。查看此方法的源码:

代码语言:txt
复制

public String subString(int beginIndex, int endIndex){

代码语言:txt
复制
    if (beginIndex < 0){
代码语言:txt
复制
        throw new StringIndexOutOfBoundsException(beginIndex);
代码语言:txt
复制
    }
代码语言:txt
复制
    if (endIndex > count){
代码语言:txt
复制
        throw new StringIndexOutOfBoundsException(beginIndex);
代码语言:txt
复制
    }
代码语言:txt
复制
    if (beginIndex > endIndex){
代码语言:txt
复制
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
代码语言:txt
复制
    }
代码语言:txt
复制
    return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value);
代码语言:txt
复制
}
代码语言:txt
复制
在方法的最后一行,返回一个新建的String对象。查看String的构造函数:
```java

// Package private constructor which shares value array for speed.

String(int offset, int count, char[] value){

代码语言:txt
复制
    this.value = value;
代码语言:txt
复制
    this.offset = offset;
代码语言:txt
复制
    this.count = count;
代码语言:txt
复制
}
代码语言:txt
复制
源码注释中说明,这是一个包作用域的构造函数,其目的是为了能高效且快速的共享String内的char数组对象。但在这种通过偏移量来截取字符串的方法中,String的原生内容value数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符串长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了响应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了运算速度却浪费了大量的内存空间。
(String的这个构造函数使用了以空间换时间的策略,浪费了内存空间,却提高了字符串的生成速度。)
以下以一个实例来说明该方法的弊端:
```java

public class StringTest {

代码语言:txt
复制
public static void main(String[] args) {
代码语言:txt
复制
    List<String> handler = new ArrayList<>();
代码语言:txt
复制
    for (int i = 0; i < 10000; i++){
代码语言:txt
复制
        HugeStr h = new HugeStr();
代码语言:txt
复制
        //ImprovedHugeStr imp = new ImprovedHugeStr();
代码语言:txt
复制
        handler.add(h.getSubString(1, 5));
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
static class HugeStr{
代码语言:txt
复制
    // 一个很长的String
代码语言:txt
复制
    private String str = new String(new char[10000000]);
代码语言:txt
复制
    // 截取字符串,有溢出
代码语言:txt
复制
    public String getSubString(int begin, int end){
代码语言:txt
复制
        return str.substring(begin, end);
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
static class ImprovedHugeStr{
代码语言:txt
复制
    private String str = new String(new char[10000000]);
代码语言:txt
复制
    // 截取子字符串,并重新生成
代码语言:txt
复制
    public String getSubString(int begin, int end){
代码语言:txt
复制
        return new String(str.substring(begin, end));
代码语言:txt
复制
    }
代码语言:txt
复制
}

}

代码语言:txt
复制
HugeStr.getSubString()在不到1000次的时候就出现了内存溢出。
而ImprovedHugeStr能够很好的工作的关键是因为它使用没有内存泄漏的String构造函数重新生成String对象,使得由subString()方法返回的,存在内存泄漏问题的String对象失去所有的强引用,从而被垃圾回收器识别为垃圾对象进行回收,保证了系统内存的稳定。

#### 字符串分割和查找
-------------------------------------------
字符串分割和查找也是字符串处理中最常用的方法之一。字符串分割将一个原始字符串,根据某个分隔符,切割成一组小字符串。String对象的split()方法便实现了这个功能:
```java

public String[] split(String regex)

代码语言:txt
复制
以上代码是split()函数的原型,它提供了非常强大的字符串分割功能。传入参数可以是一个正则表达式,从而进行复杂逻辑的字符串分割。
比如字符串“a;b,c:d”,分别使用了分号、逗号、冒号分隔了各个字符串,这时,如果需要将这些分隔符去掉,只保留字母内容,只需要使用正则表达式即可:
```java

String[] array = "a;b,c:d".split(";|,|:]");

代码语言:txt
复制
对正则表达式的支持,使split()函数本身具有强大的功能,强当地使用,可以起到事半功倍的效果。但是,就简单的字符串分割功能而言,它的性能表现却不尽人意。

##### 最原始的字符串分割
使用以下代码生成一个String对象,并存放在str变量中。
```java

String str = null;

代码语言:txt
复制
    StringBuffer sb = new StringBuffer();
代码语言:txt
复制
    for (int i = 0; i < 1000; i++){
代码语言:txt
复制
        sb.append(i);
代码语言:txt
复制
        sb.append(";");
代码语言:txt
复制
    }
代码语言:txt
复制
    str = sb.toString();
代码语言:txt
复制
然后,使用split根据";"对字符串进行分割:

long start = System.currentTimeMillis();

代码语言:txt
复制
    for (int i = 0; i < 10000; i++){
代码语言:txt
复制
        str.split(";");
代码语言:txt
复制
    }
代码语言:txt
复制
    System.out.println("对str进行10000次分割用时cost:" + (System.currentTimeMillis() - start));
代码语言:txt
复制
执行结果:
```java

对str进行10000次分割用时cost:462

代码语言:txt
复制
在我的计算机上执行上述分割,共用时462毫秒。是否有更快的发方法完成类似的简单字符串分割呢?来看一个StringTokenizer类。

##### 使用效率更高的StringTokenizer类分割字符串
StringTokenizer类时JDK中提供的专门用来处理字符串分割字串的工具类。它的典型构造函数如下:
```java

public StringTokenizer(String str, String delim)

代码语言:txt
复制
其中str参数是要分割处理的字符串,delim是分割符号。当一个StringTokenizer对象生成后,可以通过nextToken()方法便可以得到下一个分割的字符串。通过hasMoreTokens()方法可以得到是否有更多的子字符串需要处理。使用StringTokenizer完成上述例子中的分割任务:
```java

long start2 = System.currentTimeMillis();

代码语言:txt
复制
    StringTokenizer stringTokenizer = new StringTokenizer(str, ";");
代码语言:txt
复制
    for (int i = 0; i < 10000; i++){
代码语言:txt
复制
        while (stringTokenizer.hasMoreTokens()) {
代码语言:txt
复制
            stringTokenizer.nextToken();
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
    stringTokenizer = new StringTokenizer(str, ";");
代码语言:txt
复制
    System.out.println("StringTokenizer对str进行10000次分割用时cost:" + (System.currentTimeMillis() - start2));
代码语言:txt
复制
运行结果:
```java

StringTokenizer对str进行10000次分割用时cost:13

Process finished with exit code 0

代码语言:txt
复制
同样在我的计算机上,使用StringTokenizer分割用时只有13毫秒。即使在这段代码中StringTokenizer对象被不断创建并销毁,但其效率仍然明显高于split()方法。

##### 更优化字符串分割方法
字符串分割是否还能有继续优化的余地呢?有的!那就是自己手动完成字符串分割的算法。为了完成这个算法,我们需要使用String的两个方法indexOf()和subString().在前面我们提到过,subString()是采用时间换取空间技术,因此它的执行速度相对会很快,只要处理好内存溢出问题,便可大胆使用。
而indexOf()函数是一个执行速度非常快的方法,它的原型如下:
```java

public int indexOf(int ch)

代码语言:txt
复制
它返回指定字符在String对象中的位置。
下面我们开始完成自定义分割算法,并同样对str对象进行处理。方法同样执行1万次。
```java

long start = System.currentTimeMillis();

代码语言:txt
复制
    for (int i = 0; i < 2; i++){
代码语言:txt
复制
        while (true){
代码语言:txt
复制
            String splitStr = null;
代码语言:txt
复制
            // 找分割符的位置
代码语言:txt
复制
            int j = str.indexOf(";");
代码语言:txt
复制
            // 没有分隔符存在
代码语言:txt
复制
            if (j<0)
代码语言:txt
复制
                break;
代码语言:txt
复制
            // 找到分隔符,截取字符串
代码语言:txt
复制
            splitStr = str.substring(0, j);
代码语言:txt
复制
            // 剩下需要处理的字符串
代码语言:txt
复制
            str = str.substring(j+1);
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
    System.out.println("自定义分割算法对str进行10000次分割用时cost:" + (System.currentTimeMillis() - start));
代码语言:txt
复制
执行结果:
```java

自定义分割算法对str进行10000次分割用时cost:2

Process finished with exit code 0

代码语言:txt
复制
看到我们自定义的分割算法耗时只用了2毫秒。这也说明了indexOf()和subString()执行速度非常之快,很适合作为高频函数使用。

#### 高效率的charAt()方法
-----------------------------------------------
在上述例子中indexOf()方法是用于检索字符串中index位置的字符,它具有很高的效率,String还提供了一个同样高效,但是作用相反的方法,通过位置检索字符:charAt(),其原型如下:
```java

public char charAt(int index)

代码语言:txt
复制
在软件开发过程中,经常会遇到这样的问题:判断一个字符串的开始和结束子串是否等于某个字串。如:判断字符串str,是否以“Java”开头,以“JDK”结尾。
判断以什么开头:
```java

public boolean startsWith(String prefix)

代码语言:txt
复制
判断以什么结尾:
```java

public boolean startsWith(String prefix)

代码语言:txt
复制
但即使是这样的内置函数,其效率也是远远低于charAt()方法的。来验证下:
```java

public static void main(String[] args) {

代码语言:txt
复制
    // 初始化一个大字符串
代码语言:txt
复制
    String str = null;
代码语言:txt
复制
    StringBuffer sb = new StringBuffer();
代码语言:txt
复制
    for (int i = 0; i < 100000; i++){
代码语言:txt
复制
        sb.append(i);
代码语言:txt
复制
    }
代码语言:txt
复制
    str = sb.toString();
代码语言:txt
复制
    // 使用startWith() & endWith()
代码语言:txt
复制
    long start1 = System.currentTimeMillis();
代码语言:txt
复制
    for (int i=0; i <100000; i++){
代码语言:txt
复制
        str.startsWith("Java");
代码语言:txt
复制
        str.endsWith("Jdk");
代码语言:txt
复制
    }
代码语言:txt
复制
    System.out.println("使用startWith() & endWith()执行100000用时cost:" + (System.currentTimeMillis() - start1));
代码语言:txt
复制
    // 使用charWith()方法
代码语言:txt
复制
    long start2 = System.currentTimeMillis();
代码语言:txt
复制
    int len = str.length();
代码语言:txt
复制
    for (int i=0; i <100000; i++){
代码语言:txt
复制
        // charAt()判断是否“Java”开头
代码语言:txt
复制
        if (str.charAt(0) == 'J' && str.charAt(1) == 'a' && str.charAt(2) == 'v' && str.charAt(3) == 'a' );
代码语言:txt
复制
        // charAt()判断是否“Jdk”结尾
代码语言:txt
复制
        if (str.charAt(len-1) == 'J' && str.charAt(len-2) == 'd' && str.charAt(len-3) == 'k');
代码语言:txt
复制
    }
代码语言:txt
复制
    System.out.println("使用charWith()执行100000用时cost:" + (System.currentTimeMillis() - start2));
代码语言:txt
复制
}
代码语言:txt
复制
执行结果:
```java

使用startWith() & endWith()执行100000用时cost:13

使用charWith()执行100000用时cost:6

Process finished with exit code 0

代码语言:txt
复制

可以看出来,依然是charAt()的效率更高些。

总结

不论选择哪一种实现,对功能来说都是可以满足的。但是如果需要考虑性能问题,那么就需要我们开学人员在我们自己的业务场景下选择更优的实现方案。

更多个人学习笔记


https://www.relaxheart.cn/to/master/blog

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • String对象及特点
    • 不变性
      • 针对常量池的优化
        • 类的final定义
        • subString()方法的内存泄露
        • 总结
        相关产品与服务
        对象存储
        对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档