Java字符串优化处理

字符串是软件开发中最为重要的对象之一。而且它在内存中占据了很大的空间块。因此如何高效的处理字符串,必将是提高系统整体性能的关键。

字符串对象及其特点

String对象是Java语言中重要的数据类型,但它并不是Java的基本数据类型,在Java语言中,String对象可以认为是char数组的延伸和进一步封装。它主要有3部分组成:char数组,偏移量和string的长度。char数组表示String的内容,它是String对象所表示字符串的超集。String的真实内容还需要偏移量和长度在这个char数组表示Stirng内存,它是String对象所表示字符串的超集。String的真实内容还需要由偏移量和长度在这个char数组中进行定位和截取。

在Java语言中,Java的设计者对String对象进行了大量的优化,其主要表现在三个方面

1.不变性;

2.针对常量池的优化;

3.类的final定义

Java字符串优化处理_第1张图片Java字符串优化处理_第2张图片

1.不变性

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

2.针对常量池的优化

针对常量池的优化是指:当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

	String str1="123";
		String str2="123";
		String str3=new String("123");
		System.out.println(str1==str2);  //true
		System.out.println(str1==str3); //false
		System.out.println(str2==str3.intern()); //true

str1和str2引用了相同的地址,虽然str3开辟了一块新的内存地址,但是str3在常量池中的位置和str1是一样的。

Java字符串优化处理_第3张图片

3.类的final定义

final类型定义也是String对象的重要特点,作为final类的String对象在系统中不可能有任何子类,这是对系统安全性的保护。

subString()方法的内存泄漏

截取字符串是字符串操作中最常用的操作之一。在Java中,String提供了两个截取字符串的方法;注意是两个不是两种;

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
 public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

我们看在方法的最好,都返回了一个新建的String对象。查看该String的构造函数:

String(int offset, int count, char value[]) {
  this.value = value;
  this.offset = offset;
  this.count = count;
}

当我们读完上述的代码,我们应该会豁然开朗,原来是这个样子啊!

当我们调用字符串a的substring得到字符串b,其实这个操作,无非就是调整了一下b的offset和count,用到的内容还是a之前的value字符数组,并没有重新创建新的专属于b的内容字符数组。

举个和上面重现代码相关的例子,比如我们有一个1G的字符串a,我们使用substring(0,2)得到了一个只有两个字符的字符串b,如果b的生命周期要长于a或者手动设置a为null,当垃圾回收进行后,a被回收掉,b没有回收掉,那么这1G的内存占用依旧存在,因为b持有这1G大小的字符数组的引用。

看到这里,大家应该可以明白上面的代码为什么出现内存溢出了。

共享内容字符数组

其实substring中生成的字符串与原字符串共享内容数组是一个很棒的设计,这样避免了每次进行substring重新进行字符数组复制。正如其文档说明的,共享内容字符数组为了就是速度。但是对于本例中的问题,共享内容字符数组显得有点蹩脚。

如何解决

对于之前比较不常见的1G字符串只截取2个字符的情况可以使用下面的代码,这样的话,就不会持有1G字符串的内容数组引用了。
String littleString = new String(largeString.substring(0,2));

Java 7 实现

在Java 7 中substring的实现抛弃了之前的内容字符数组共享的机制,对于子字符串(自身除外)采用了数组复制实现单个字符串持有自己的应该拥有的内容。

public String(char value[], int offset, int count) {
    if (offset < 0) {
          throw new StringIndexOutOfBoundsException(offset);
    }
    if (count < 0) {
      throw new StringIndexOutOfBoundsException(count);
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
      throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

真的是内存泄露么

我们知道了substring某些情况下可能引起内存问题,但是这个叫做内存泄露么?

其实个人认为这个不应该算为内存泄露,使用substring生成的字符串b固然会持有原有字符串a的内容数组引用,但是当a和b都被回收之后,该字符数组的内容也是可以被垃圾回收掉的。

字符串分割和查找

字符串分割和查找也是字符串处理中最常用的方法之一。字符串分割将一个原始字符串,根据某个分隔符,切割成一组小字符串。string对象的split()方法便实现了化这个功能:
对正则表达式的支持,使得split()函数本身具有强大的功能,但是它的性能却比较差;String.split()方法使用简单,功能强大,但是在系统中频繁使用这个方法是不可取的;

效率更高的StringTokenizer类分割字符串

 StringTokenizer类是JDK中提供的专门用来处理字符串分割字串的工具类
  public StringTokenizer(String str, String delim, boolean returnDelims) {
        currentPosition = 0;
        newPosition = -1;
        delimsChanged = false;
        this.str = str;
        maxPosition = str.length();
        delimiters = delim;
        retDelims = returnDelims;
        setMaxDelimCodePoint();
    }

其中str参数是要分割处理的字符串,delim是分割符号。当一个StringTokenizer对象生成后。通过它的nextToken()方法便可以得到下一个分割的字符串。

更加优化的字符串分割方式

我们可以自己动手完成字符串分割的算法。为了完成这个算法,我们需要使用String的两个方法indexOf()和subString()。因为subString()是采用了时间换空间的技术,所以它的执行速度相对会很快,只要处理好内存溢出问题,可以大胆使用

public static List splits(String str, String sign) {
		List list = new ArrayList<>();
		String tmp = str;
		while (true) {
			String spl = null;
			int j = tmp.indexOf(sign); // 找到分隔符的位置
			if(j>0)
			spl = tmp.substring(0, j);
			tmp = tmp.substring(j + 1); // 剩下需要处理的字符串
			if (j < 0) { // 没有分隔符存在
				list.add(tmp);
				break;
			}  
			if(j>0){
				list.add(spl);
			}
		}
		return list;
	}

高效率的charAt()方法;

在上例中提到,indexOf()方法具有很高的效率,适合高频率地调用。和indexOf()方法相反,string对象还提供了一个charAt()方法,他返回给定字符串中位置再index的字符,它的功能和indexOf()正好相反,但是效率却很高;

String、StringBuffer、StringBuilder区别

StringBuffer、StringBuilder和String一样,也用来代表字符串。String类是不可变类,任何对String的改变都 会引发新的String对象的生成;StringBuffer则是可变类,任何对它所指代的字符串的改变都不会产生新的对象。既然可变和不可变都有了,为何还有一个StringBuilder呢?相信初期的你,在进行append时,一般都会选择StringBuffer吧!

先说一下集合的故事,HashTable是线程安全的,很多方法都是synchronized方法,而HashMap不是线程安全的,但其在单线程程序中的性能比HashTable要高。StringBuffer和StringBuilder类的区别也是如此,他们的原理和操作基本相同,区别在于StringBufferd支持并发操作,线性安全的,适 合多线程中使用。StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用。新引入的StringBuilder类不是线程安全的,但其在单线程中的性能比StringBuffer高。

public class StringTest {  
  
    public static String BASEINFO = "Mr.Y";  
    public static final int COUNT = 2000000;  
  
    /** 
     * 执行一项String赋值测试 
     */  
    public static void doStringTest() {  
  
        String str = new String(BASEINFO);  
        long starttime = System.currentTimeMillis();  
        for (int i = 0; i < COUNT / 100; i++) {  
            str = str + "miss";  
        }  
        long endtime = System.currentTimeMillis();  
        System.out.println((endtime - starttime)  
                + " millis has costed when used String.");  
    }  
  
    /** 
     * 执行一项StringBuffer赋值测试 
     */  
    public static void doStringBufferTest() {  
  
        StringBuffer sb = new StringBuffer(BASEINFO);  
        long starttime = System.currentTimeMillis();  
        for (int i = 0; i < COUNT; i++) {  
            sb = sb.append("miss");  
        }  
        long endtime = System.currentTimeMillis();  
        System.out.println((endtime - starttime)  
                + " millis has costed when used StringBuffer.");  
    }  
  
    /** 
     * 执行一项StringBuilder赋值测试 
     */  
    public static void doStringBuilderTest() {  
  
        StringBuilder sb = new StringBuilder(BASEINFO);  
        long starttime = System.currentTimeMillis();  
        for (int i = 0; i < COUNT; i++) {  
            sb = sb.append("miss");  
        }  
        long endtime = System.currentTimeMillis();  
        System.out.println((endtime - starttime)  
                + " millis has costed when used StringBuilder.");  
    }  
  
    /** 
     * 测试StringBuffer遍历赋值结果 
     *  
     * @param mlist 
     */  
    public static void doStringBufferListTest(List mlist) {  
        StringBuffer sb = new StringBuffer();  
        long starttime = System.currentTimeMillis();  
        for (String string : mlist) {  
            sb.append(string);  
        }  
        long endtime = System.currentTimeMillis();  
        System.out.println(sb.toString() + "buffer cost:"  
                + (endtime - starttime) + " millis");  
    }  
  
    /** 
     * 测试StringBuilder迭代赋值结果 
     *  
     * @param mlist 
     */  
    public static void doStringBuilderListTest(List mlist) {  
        StringBuilder sb = new StringBuilder();  
        long starttime = System.currentTimeMillis();  
        for (Iterator iterator = mlist.iterator(); iterator.hasNext();) {  
            sb.append(iterator.next());  
        }  
  
        long endtime = System.currentTimeMillis();  
        System.out.println(sb.toString() + "builder cost:"  
                + (endtime - starttime) + " millis");  
    }  
  
    public static void main(String[] args) {  
        doStringTest();  
        doStringBufferTest();  
        doStringBuilderTest();  
  
        List list = new ArrayList();  
        list.add(" I ");  
        list.add(" like ");  
        list.add(" BeiJing ");  
        list.add(" tian ");  
        list.add(" an ");  
        list.add(" men ");  
        list.add(" . ");  
  
        doStringBufferListTest(list);  
        doStringBuilderListTest(list);  
    }  
  
}

看一下执行结果:

2711 millis has costed when used String.
211 millis has costed when used StringBuffer.
141 millis has costed when used StringBuilder.
 I  like  BeiJing  tian  an  men  . buffer cost:1 millis
 I  like  BeiJing  tian  an  men  . builder cost:0 millis


从上面的结果可以看出,不考虑多线程,采用String对象时(我把Count/100),执行时间比其他两个都要高,而采用StringBuffer对象和采用StringBuilder对象的差别也比较明显。由此可见,如果我们的程序是在单线程下运行,或者是不必考虑到线程同步问题,我们应该优先使用StringBuilder类;如果要保证线程安全,自然是StringBuffer

后面List的测试结果可以看出,除了对多线程的支持不一样外,这两个类的使用方式和结果几乎没有任何差别,


StringBuffer常用方法

(由于StringBuffer和StringBuilder在使用上几乎一样,所以只写一个,以下部分内容网络各处收集,不再标注出处)

StringBuffer s = new StringBuffer();

这样初始化出的StringBuffer对象是一个空的对象,

 StringBuffer sb1=new StringBuffer(512);
分配了长度512字节的字符缓冲区。 

StringBuffer sb2=new StringBuffer(“how are you?”)

创建带有内容的StringBuffer对象,在字符缓冲区中存放字符串“how are you?”


 a、append方法
public StringBuffer append(boolean b)
该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接,调用该方法以后,StringBuffer对象的内容也发生改 变,例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.append(true);
则对象sb的值将变成”abctrue”

使用该方法进行字符串的连接,将比String更加节约内容,经常应用于数据库SQL语句的连接。


 b、deleteCharAt方法
public StringBuffer deleteCharAt(int index)
该方法的作用是删除指定位置的字符,然后将剩余的内容形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“KMing”);
sb. deleteCharAt(1);
该代码的作用删除字符串对象sb中索引值为1的字符,也就是删除第二个字符,剩余的内容组成一个新的字符串。所以对象sb的值变 为”King”。
还存在一个功能类似的delete方法:
public StringBuffer delete(int start,int end)
该方法的作用是删除指定区间以内的所有字符,包含start,不包含end索引值的区间。例如:
StringBuffer sb = new StringBuffer(“TestString”);
sb. delete (1,4);
该代码的作用是删除索引值1(包括)到索引值4(不包括)之间的所有字符,剩余的字符形成新的字符串。则对象sb的值是”TString”。 


 c、insert方法
public StringBuffer insert(int offset, boolean b),
该方法的作用是在StringBuffer对象中插入内容,然后形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“TestString”);
sb.insert(4,false);
该示例代码的作用是在对象sb的索引值4的位置插入false值,形成新的字符串,则执行以后对象sb的值是”TestfalseString”。 


 d、reverse方法
public StringBuffer reverse()
该方法的作用是将StringBuffer对象中的内容反转,然后形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.reverse();
经过反转以后,对象sb中的内容将变为”cba”。 


 e、setCharAt方法
public void setCharAt(int index, char ch)该方法的作用是修改对象中索引值为index位置的字符为新的字符ch。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.setCharAt(1,’D’);
则对象sb的值将变成”aDc”。 


 f、trimToSize方法
public void trimToSize()
该方法的作用是将StringBuffer对象的中存储空间缩小到和字符串长度一样的长度,减少空间的浪费,和String的trim()是一样的作用,不在举例。


 g、length方法
该方法的作用是获取字符串长度 ,不用再说了吧。


 h、setlength方法
该方法的作用是设置字符串缓冲区大小。
StringBuffer sb=new StringBuffer();
sb.setlength(100);
如果用小于当前字符串长度的值调用setlength()方法,则新长度后面的字符将丢失。 


 i、sb.capacity方法
该方法的作用是获取字符串的容量。
StringBuffer sb=new StringBuffer(“string”);
int i=sb.capacity(); 


 j、ensureCapacity方法
该方法的作用是重新设置字符串容量的大小。
StringBuffer sb=new StringBuffer();
sb.ensureCapacity(32); //预先设置sb的容量为32 


 k、getChars方法
该方法的作用是将字符串的子字符串复制给数组。
getChars(int start,int end,char chars[],int charStart); 

StringBuffer sb = new StringBuffer("I love You");
int begin = 0;
int end = 5;
//注意ch字符数组的长度一定要大于等于begin到end之间字符的长度
//小于的话会报ArrayIndexOutOfBoundsException
//如果大于的话,大于的字符会以空格补齐
char[] ch  = new char[end-begin];
sb.getChars(begin, end, ch, 0);
System.out.println(ch);

结果:I lov

注明以上内容转载自http://blog.csdn.net/mad1989/article/details/26389541

这里再添加一点就是无论StringBuilder或者StringBuffer,在初始化时都可以设置一个容量参数,在不指定容量参数时,默认是16个字节。此参数指定了他们的初始化大小,在追加字符串时,如果超过实际char数组长度,则需要进行扩容。如果能够预先评估StringBuilder的大小,将能够有效的节省这些操作,从而提高系统性能。


你可能感兴趣的:(Java复习总结)