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

Updated on in Java with 635 views

今天带来的都是几条和 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

Responses
取消