概念
String在java中是对char数组的延伸和封装,它主要有三部分:
char数组
偏移量
String长度
char数组表示String的内容 他是String对象所表示字符串的超集,String的真是内容还需要由偏移量和长度在这个char数组中进行定位和截取
特点
不变性
一个String对象一旦生成就不可改变,作用在于被多线程共享和访问时,省略同步和锁等待的时间,提高性能
针对常量池的优化
当两个String对象拥有相同的值得时候,他们只引用常量池中的同一个拷贝
类final的定义
String类作为final类不可以有任何子类,这是对系统安全性的保护,使用final定义 ,有助于虚拟机内联所有final方法,提高效率
subString()方法的内存泄漏
public String subString(int beginIndex,int endIndex)
这个方法在jdk1.6中存在严重的内存泄漏,查看源代码:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
这个方法的最后返回了一个包私有的构造函数
// Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
这种把原来字符串中char数组完全复制到被截取字符创中的做法,虽然可以高效快速实现字符串共享,但是当原来字符串很大,我们需要截取的字符串很小时,子字符串虽然看上去只有那么几个,但是他包含了原来字符串所有的内容,极大的浪费了空间,这就是典型的以空间换时间。
通过一个例子可以亲自试验一下:
public class SubStringTest {
static class HugeStr{
private String str = new String(new char[100000]);
public String getSubString(int begin,int end){
return str.substring(begin, end);
}
}
static class ImprovedHugeStr{
private String str = new String(new char[100000]);
public String getSubString(int begin,int end){
return new String(str.substring(begin, end));
}
}
public static void main(String[] args) {
List<String> handle = new ArrayList<String>();
for(int i = 0;i < 100000;i ++){
// HugeStr h = new HugeStr();
ImprovedHugeStr h = new ImprovedHugeStr();
handle.add(h.getSubString(1, 5));
}
System.out.println("end");
}
}
使用HugeStr时会看到内存溢出是由于调用了
// Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
这个构造函数,但实际使用时,应用程序用不到。
使用ImprovedHugeStr没有内存溢出,他没有使用内存泄漏的String构造函数重新生成对象,使得由subString()返回的存在内存泄漏的对象失去了强引用,从而被垃圾回收器回收掉。
我们通过查看调用关系可以看到所有使用了这个构造函数的方法,他们都有可能造成内存泄漏:
字符串的分割
- String类的split()方法
- StringTokenizer类进行分割
自定义更优化的分割方法
对于最原始的split方法
public String[] split(String regex)
我们可以通过自定义正则表达式来规定分割的方法。为了比较这几种字符串分割的优劣,首先,我们先来 构造一个字符串:
String orgStr = null;
StringBuffer sb = new StringBuffer();
for(int i = 0;i < 1000; i ++){
sb.append(i);
sb.append(";");
}
orgStr = sb.toString();
对于split做一万次分割:
long current = System.currentTimeMillis();
for(int i = 0;i < 10000; i ++){
orgStr.split(";");
}
System.out.println(System.currentTimeMillis() - current);
用时:510ms
使用StringTokenizer类:
long current = System.currentTimeMillis();
StringTokenizer st = new StringTokenizer(orgStr,";");
for(int i = 0;i < 10000;i++){
while(st.hasMoreTokens()){
st.nextToken();
}
st = new StringTokenizer(orgStr,";");
}
System.out.println(System.currentTimeMillis() - current);
耗時:460ms
在StringTokenizer類中,通過hasMoreTokens()可以判斷是否還有子串需要分割,nextToken()能夠獲取下一個分割的子字符串,我們及時構造了一萬次StringTokenizer對象,仍然比split方法要快。
第三種,需要用到兩個String類的方法indexOf()和subString(),下面是我們自定義的方法:
long current = System.currentTimeMillis();
String tmp = orgStr;
for(int i = 0;i < 10000;i ++){
while(true){
String splitStr = null;
int j = tmp.indexOf(';');//找到分隔符的位置
if( j < 0){ //沒有分隔符存在
break;
}
splitStr = tmp.substring(0,j);//找到分隔符,截取子字符串
tmp = tmp.substring(j+1);//剩下待處理字符串
}
tmp = orgStr;
}
System.out.println(System.currentTimeMillis() - current);
這種方法耗時才172ms,明顯更快,綜上,split性能最差,能有StringTokenizer就不要用split,當時間性能很重要的時候,可以選用第三種。
高效的charAt()
这个方法也具有很高的效率,他采用了数组的随机地址访问:
public char charAt(int index) {
if ((index < 0) || (index >= count)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index + offset];
}
它能够根据索引直接找到对应的字符。
我们经常会判断一个字符串的开头或者结尾是一个什么样的字符串,通常的做法是使用以下两个方法:
public boolean startsWith(String prefix);
public boolean endsWith(String suffix);
即使他们是JAVA内置的函数,其效率也没有charAt方法效率高,以下是我们自己封装的一个判断方法
public static boolean endWith(String str){
int len = str.length();
if(str.charAt(len - 1) == 'a'
&& str.charAt(len - 2) == 'b'
&& str.charAt(len - 3) == 'c'){
return true;
}
return false;
}
public static boolean startWith(String str){
if(str.charAt(0) == 'a'
&& str.charAt(1) == 'b'
&& str.charAt(2) == 'c'){
return true;
}
return false;
}
适应这两个方法时,耗时7ms,使用startsWith和endsWith 耗时16ms,所以当性能成为影响系统的主要因素的时候,最好选用charAt。