字符串优化
Posted x-zhi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字符串优化相关的知识,希望对你有一定的参考价值。
C#字符串优化学习总结
内存区域
我们知道一个由C/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack): 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) : 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static):全局变量和静态变量的存储都是在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域, 程序结束后有系统释放 。
4、常量区:常量、字符串常量池就是放在这里的, 程序结束后由系统释放 。
5、程序代码区:存放函数体的二进制代码。
C#常量池
C#也有自己的常量池,也就是我们所称的暂存池(string intern pool),C#的字符串常量池不在堆中也不在栈中,是独立的内存空间管理,在内存的常量区,由CLR(Common Language Runtime)维护这段内存。
其中,我们定义的例如string a = "HelloWorld";
,"HelloWorld"
这个我们定义的字面量就存储在常量区中。如果再定义一个string b = "HelloWorld"
,这时候CLR就会去字符串常量池中找,如果存在相同内容的字符串对象的引用,则将这个引用返回。否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。
关于常量池的理解:
-
常量池由CLR来维护,其中的所有字符串对象的值都不相同。
-
只有编译阶段的文本字符常量会被自动添加到常量池。
-
运行时期动态创建的字符串不会被加入到常量池中。
-
string.Intern()
可以把动态创建的字符串加入到常量池中。
即使这个动态创建的字符串和常量池中的某个字符串的值相等,引用也不会相等。
即使是动态创建的两个字符串的值相等,他们的引用依然不相等。(charArray.ToString()特例)
字符串内存优化的核心原则有三个:
1、复用字符串,减少字符串数量
2、降低不可复用字符串的占用的内存
3、降低运行时产生的GC字符串内存
关于string
拼接和StringBuilder
拼接
1、在处理字符串时:string
是只可读不可写的,在进行字符串拼接时,往往是创建一个string
对象,然后栈中的内存指向堆中的新内存,在创建对象时需要分配内存空间,之前的内存则会产生GC。而StringBuilder
是存在于System.Text
命名空间下的在原来的内存中修改,不需要分配内存空间。
2、从内存优化方面来说,虽然StringBuilder
在拼接后仍需要调用ToString()
将拼接后的内容转换成不可写的字符串,但是相比较下来,频繁的字符串操作StringBuilder
更好。
3、从功能上来说string
仍然比StringBuilder
更强。
4、string
主要用于公共API,通用性好,读取性能高,占用内存小。
5、StringBuilder
主要用于拼接string
,修改性能好。
6、string
是不可变的,所以天然线程同步。
7、StringBuilder
可变,非线程同步。
如果是处理字符串的话,用string
中的方法每次都需要创建一个新的字符串对象并且分配新的内存地址,而StringBuilder
是在原来的内存里对字符串进行修改,所以在字符串处理方面还是建议用StringBuilder
这样比较节约内存。但是string
类的方法和功能仍然还是比StringBuilder
类要强。
关于string+int
string
之所以可以与int
相加,根本上是调用了Concat
方法。首先int
转object
需要装箱,然后Concat
内部调用了所有object
的ToString
方法,然后再new一个字符串返回。而Concat
方法其实接受的是object
类型的对象,这也就是说,string
在与int
相加的时候,会造成装箱操作。而ToString()
会产生28B的GC,装箱会产生20B的GC,所以在拼接时,显式的调用ToString()
可以规避掉装箱的过程,使用string+int.ToString()
会比string+int
更加节省性能
【字符串性能相关的操作】
1.创建空字符串用用string s = string.Empty
,而不是string s = ""
2.高频字符串拼接用stringbuilder
,或者字符串格式化string.Format()
而string.Format()
实际上就是利用stringbuilder
去实现的
3.ToUpper
、ToLower
这类方法均会重新生成字符串,看看是否可以避免使用
4.true判断时,用"value" == string
是最快的;false判断时,用"value".Equals(string)
是最快的
Java性能优化之String字符串优化
字符串是软件开发中最重要的对象之一。通常,字符串对象在内存中是占据了最大的空间块,因此如何高效地处理字符串,必将是提高整体性能的关键所在。
1.字符串对象及其特点
Java中八大基本数据类型没有String类型,因为String类型是Java对char数组的进一步封装。
String类的实现主要由三部分组成:char数组,offset偏移量,String的长度。
String类型有三个基本特点:
不变性
不变性是指String对象一旦生成,则不能再对它进行改变。
不变性的作用在于当一个对象需要被多线程共享,并且频繁访问时,可以省略同步和锁等待的时间,从而大幅提高系统性能。
针对常量池的优化
当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。
类的final定义
作为final类的String对象在系统中不能有任何子类,这是对系统安全性的保护!
1.1 subString()方法的内存泄漏
关于这一点,在JDK的1.7及以后就已经解决了!
在1.7之前,subString()方法截取字符串只是移动了偏移量,截取之后的字符串实际上还是原来的大小。
现在,当使用subString()方法截取字符串时会把截取后的字符串拷贝到新对象。
1.2 字符串分割与查找
1、原始的String.split()
String.split()方法使用简单,功能强大,支持正则表达式,但是,在性能敏感的系统中频繁的使用这个方法是不可取的。
注意 * ^ : | . ?这些符号记得转义
2、使用效率更高的StringTokenizer类分割字符串
StringTokenizer类是JDK中提供的专门用来处理字符串分割的工具类。构造方法:
public StringTokenizer(String str, String delim, boolean returnDelims)
其中str是要分割的字符串,delim是分割符,returnDelims是否返回分隔符,默认false。
String s = "a;b;c";
StringTokenizer stringTokenizer = new StringTokenizer(s, ";", false);
System.out.println(stringTokenizer.countTokens());
while (stringTokenizer.hasMoreTokens()) {
System.out.println(stringTokenizer.nextToken());
}
3、最优化的字符串分割方式
indexOf()方法是一个执行速度非常快的方法,subString()是采用了时间换空间技术,因此速度相对快。
public static List<String> mySplit(String str, String delim){
List<String> stringList = new ArrayList<>();
while(true) {
int k = str.indexOf(delim);
if (k < 0){
stringList.add(str);
break;
}
String s = str.substring(0, k);
stringList.add(s);
str = str.substring(k+1);
}
return stringList;
}
4、三种分割方法的对比与选择
split()方法功能强大,但是效率最差;
StringTokenizer性能优于split方法,能用StringTokenizer就没必要用split();
自己实现的分割算法性能最好,但代码的可读性和系统的可维护性最差,只有当系统性能成为主要矛盾时,才推荐使用该方法。
5、高效率的charAt方法
charAt(int index) 返回指定索引处的 char 值。功能和indexOf()相反,效率却一样高。
6、字符串前后辍判断
public boolean startsWith(String prefix)
测试此字符串是否以指定的前缀开始
public boolean endsWith(String suffix)
测试此字符串是否以指定的后缀结束
这两个Java内置函数效率远远低于charAt()方法。单元测试:
@Test
public void test(){
String str = "hello";
if (str.charAt(0)=='h'&&str.charAt(1)=='e'){
System.out.println(true);
}
if (str.startsWith("he")){
System.out.println(true);
}
}
1.3 StringBuffer和StringBuilder
1、String常量的累加操作
String s = "123"+"456"+"789";
虽然从理论上说字符串的累加的效率并不高,但该语句执行耗时为0;反编译代码后,我们发现代码是
String s = "123456789";
显然,是Java在编译时做了充分的优化。因此,并没有想象中那样生成大量的String实例。
对于静态字符串的连接操作,Java在编译时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
2、String变量累加的操作
String str = "hello";
str+="word";
str+="!!!";
我们利用“+=”改变字符串内容的值,实际上字符串根本没有改变。
当 str+="word"
时,堆内存开辟了word
的内存空间和helloword
的两个内存空间(相当于实例化了两个String对象),并把str的引用指向了helloword
,原来的hello
和word
成为了垃圾被JVM回收。
3、concat() 连接字符串
String的concat()是专门用于字符串连接操作的方法,效率远远高于“+”或者“+=”。
4、StringBuffer和StringBuilder
不用多说了,就是为字符串连接而生的,效率最高。不同的是,StringBuffer几乎对所有的方法都做了同步,StringBuilder并没有做任何同步,效率更高一些。只不过在多线程系统中,StringBuilder无法保证线程安全,不能使用。
5、容量参数
StringBuffer和StringBuilder的是对String的封装,String是对char数组的封装。是数组就有大小,就有不够用的时候,不够用只能扩容,也就是把原来的再复制到新的数组中。合适的容量参数自然能够减少扩容的次数,达到提高效率的目的。
在初始化时,容量参数默认是16个字节。在构造方法中指定容量参数:
public StringBuilder(int capacity)
1.4 附一些实用的方法
判断字符串相等(忽略大小写)
equalsIgnoreCase(String anotherString)
判断是否存在子字符串(返回布尔类型)
contains(CharSequence s)
将指定字符串连接到此字符串的结尾
concat(String str)
使用指定的格式字符串和参数返回一个格式化字符串
format(String?format, Object...?args)
使用默认语言环境的规则将此 String 中的所有字符都转换为小写。
toLowerCase()
使用默认语言环境的规则将此 String 中的所有字符都转换为大写。
toUpperCase()
返回字符串的副本,忽略前导空白和尾部空白。
trim()
使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String replaceAll(String regex, String replacement)
按字典顺序比较两个字符串,不考虑大小写。
int compareToIgnoreCase(String str)
- 如果参数字符串等于此字符串,则返回值 0;
- 如果此字符串小于字符串参数,则返回一个小于 0 的值;
- 如果此字符串大于字符串参数,则返回一个大于 0 的值。
本文参考《Java性能优化》葛一鸣著
码字不易,不吝赐赞!
以上是关于字符串优化的主要内容,如果未能解决你的问题,请参考以下文章