Jeffrey的博客 Jeffrey的博客

左脑编程,右脑写诗

目录
改善Java程序的N个建议(三)
/    

改善Java程序的N个建议(三)

今天带来的都是几条和String字符串操作有关的建议,细品、细品。

建议54:正确使用String、StringBuffer、StringBuilder

Java的CharSequence接口有三个实现类与字符串有关:String、StringBuffer、StringBuilder。
String是一个不可变量,也就是当他创建之后就会中内存中永久存在且不能修改,即使通过String自身的方法产生的也是一个新的字符串。

String str = "hello";
String str1 = str.substring(1);

str字符串通过substring方法重新生成了一个str1字符串其值为“ello”,那有没有可能不创建对象返回自己呢?str.substring(0)就不会产生新对象,JVM会从字符串池只能够返回str的引用。

StringBuffer和String一样中内存中保存的都是一个有序的字符序列,不同点是StringBuffer对象的值是可变的,例如:

StringBuffer sb = new StringBuffer("hello");
sb.append(" world");

上面的代码sb的值一直在变化,经过append后变为了“hello world”,那这个和String类通过“+”连接字符串有什么区别呢?

当然有区别,通过String加号连接的字符串,字符串变量指向了新的引用地址,而StringBuffer则不会变更其引用地址。

StringBuilder和StringBuffer基本相同,不同点是,StringBuffer是线程安全的,而StringBuilder是线程不安全的,所以可以看出String类的操作要远慢于StringBuffer和StringBuilder。

弄清楚了三者的原理,再来看看他们的使用场景:

  • String类使用场景:在字符串不经常变化的时候使用,例如声明常量、少量变量等
  • StringBuffer类使用场景:频繁进行字符串的运算,如拼接、替换、删除等,并且运行在多线程环境中,例如XML解析、HTTP参数解析和封装等
  • StringBuilder类使用场景:频繁进行字符串的运算,如拼接、替换、删除等,并且运行在单线程环境中,例如SQL语句的拼装、JSON封装等

建议56:自由选择字符串拼接方法

对于字符串等拼接一般有三种方法:加号、concat方法、StringBuffer或StringBuilder的append方法,那这三者具体有什么区别呢?来看看下面的例子:

str += "a";	// 加号连接
str = str.concat("a");	// concat方法连接

分别用这三种方法做字符串拼接,循环10W次后,检查其执行时间:

public class Proposal_56 {
	public static void doWithAdd() {
		String str = "a";
		for (int i = 0; i < 100000; i++) {
			str += "c";
		}
	}

	public static void doWithConcat() {
		String str = "a";
		for (int i = 0; i < 100000; i++) {
			str = str.concat("c");
		}
	}

	public static void doWithStringBuilder() {
		StringBuilder sb = new StringBuilder("a");
		for (int i = 0; i < 100000; i++) {
			sb.append("c");
		}
	}

	public static void main(String[] args) {
		long startTime = System.currentTimeMillis();
		doWithAdd();
		long endTime = System.currentTimeMillis();
		System.out.println("doWithAdd运行时间:" + (endTime - startTime) + "ms");

		startTime = System.currentTimeMillis();
		doWithConcat();
		endTime = System.currentTimeMillis();
		System.out.println("doWithConcat运行时间:" + (endTime - startTime) + "ms");

		startTime = System.currentTimeMillis();
		doWithStringBuilder();
		endTime = System.currentTimeMillis();
		System.out.println("doWithStringBuilder运行时间:" + (endTime - startTime) + "ms");
	}
}

结果如下:
image.png

1.加号拼接字符串:
编译器对字符串使用加号做了优化,它会使用StringBuilder的append方法进行追加,其效果和下面的代码相同:

str = new StringBuilder(str).append("c").toString();

那按道理,不应该也和StringBuilder的效率一样吗,为什么用加号花了4372ms,而StringBuilder只花了2ms,原因很简答,一它每次循环都会创建一个StringBuilder对象,循环10W次就是10W个对象,二是每次执行完毕调用toString方法,转换成字符串也需要消耗时间。

2.concat方法拼接字符串:
先来看一下concat方法的源码:

public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
}

整体看上去就是一个数组拷贝,虽然这内存中的处理是原子操作,速度非常快,但是注意看最后的return,每次concat方法都会创建一个新的String对象,这就是concat方法慢下来的原因,循环10W次,同样创建来10W个对象。

3.append方法拼接字符串:
同样也先看一下append的源码:

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
}

整个append方法都中做字符数组处理,加长,然后数组拷贝,这些都是基本的数据操作,没有新建任何对象,所以速度也就快来。

这三种拼接字符串的方法,功能相同,性能各不相同,但并不表示我们一定要使用StringBuilder,这是因为“+”非常符合我们但编程习惯,便于阅读,在大多数情况用加号即可,只有在系统性能临界的时候才考虑concat或append方法。

建议57:推荐在复杂字符串操作中使用正则表达式

在日常字符串的操作中经常会用到诸如追加、合并、替换、倒叙、分割等操作,而且Java也为我们提供了append、replace、reverse、split等方法,但是更多的时候,我们还是需要借助正则表达式完成复杂的处理,下面这个例子,统计一篇文章中的英语单词数量,代码如下:

public class Proposal_57 {
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		while (scan.hasNext()) {
			String str = scan.nextLine();
			int wordsCount = str.split(" ").length;
			System.out.println(str + " 单词数:" + wordsCount);
		}
	}
}

返回结果如下:
image.png

我们发现除了第一条正确外其他都错了,第二条没有考虑用户输入都连续空格,第三条没有考虑连续都单词,第四条没有把连写符“'”考虑进去。那该如何处理呢?我们考虑使用正则表达式:

public class Proposal_57 {
	public static void main(String[] args) {
		Scanner scan = new Scanner(System.in);
		while (scan.hasNext()) {
			String str = scan.nextLine();
//			int wordsCount = str.split(" ").length;
			Pattern pattern = Pattern.compile("\\b\\w+\\b");
			Matcher matcher = pattern.matcher(str);
			int wordsCount = 0;
			while (matcher.find()) {
				wordsCount++;
			}
			System.out.println(str + " 单词数:" + wordsCount);
		}
	}
}

改成上述代码之后,得到了下面都结果:
image.png

此时所有的结果都正确,\b表示单词边界,\w表示数字或者字符,这样匹配出来的都将会是有效都代码。正则表达式都字符串匹配可以应用在很多场合,比如常见的服务器日志分析等。


标题:改善Java程序的N个建议(三)
作者:Jeffrey