String字符串

String

   首先我们要明确,String并不是基本数据类型,而是一个对象,并且是不可变的对象。查看源码就会发现

1
2
3
4
   public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

       private final char value[];
}

String类为final型的(当然也不可被继承),而且通过查看JDK文档会发现几乎每一个修改String对象的操作,实际上都是创建了一个全新的String对象。字符串为对象,那么在初始化之前,它的值为null,到这里就有必要提下””、null、new String()三者的区别。null 表示string还没有new ,也就是说对象的引用还没有创建,也没有分配内存空间给他,而””、new String()则说明了已经new了,只不过内部为空,但是它创建了对象的引用,是需要分配内存空间的。打个比方:一个空玻璃杯,你不能说它里面什么都没有,因为里面有空气,当然也可以把它弄成真空,null与” “、new String()的区别就象真空与空气一样。在字符串中存在一个非常特殊的地方,那就是字符串池。每当我们创建一个字符串对象时,首先就会检查字符串池中是否存在面值相等的字符串,如果有,则不再创建,直接放回字符串池中对该对象的引用,若没有则创建然后放入到字符串池中并且返回新建对象的引用。这个机制是非常有用的,因为可以提高效率,减少了内存空间的占用。所以在使用字符串的过程中,推荐使用直接赋值(即String s=”aa”),除非有必要才会新建一个String对象(即String s = new String(”aa”))。

equals()方法

超类Object中有这个equals()方法,该方法主要用于比较两个对象是否相等。该方法的源码如下:

1
2
3
public boolean equals(Object obj) {  
return (this == obj);
}

我们知道所有的对象都拥有标识(内存地址)和状态(数据),同时“==”比较两个对象的的内存地址,所以说使用Object的equals()方法是比较两个对象的内存地址是否相等,即若object1.equals(object2)为true,则表示equals1和equals2实际上是引用同一个对象。虽然有时候Object的equals()方法可以满足我们一些基本的要求,但是我们必须要清楚我们很大部分时间都是进行两个对象的比较,这个时候Object的equals()方法就不可以了,实际上JDk中,String、Math等封装类都对equals()方法进行了重写。下面是String的equals()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {//如果两个对象的内存地址相同,则内容肯定相同,返回true.
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {//两个数组一一比较,String内部实现还是数组
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

对于这个代码段:if (v1[i] != v2[i])return false;我们可以非常清晰的看到String的equals()方法是进行内容比较,而不是引用比较。至于其他的封装类都差不多。
在Java规范中,它对equals()方法的使用必须要遵循如下几个规则:
equals 方法在非空对象引用上实现相等关系:
1、自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
2、对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
3、传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
4、一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
5、 对于任何非空引用值 x,x.equals(null) 都应返回 false。
对于上面几个规则,我们在使用的过程中最好遵守,否则会出现意想不到的错误。
在java中进行比较,我们需要根据比较的类型来选择合适的比较方式:
1) 对象域,使用equals方法 。
2) 类型安全的枚举,使用equals或== 。
3) 可能为null的对象域 : 使用 == 和 equals 。
4) 数组域 : 使用 Arrays.equals 。
5) 除float和double外的原始数据类型 : 使用 == 。
6) float类型: 使用Float.foatToIntBits转换成int类型,然后使用==。
7) double类型: 使用Double.doubleToLongBit转换成long类型,然后使用==。
至于6)、7)为什么需要进行转换,我们可以参考他们相应封装类的equals()方法,下面的是Float类的:

1
2
3
4
public boolean equals(Object obj) {  
return (obj instanceof Float)
&& (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
}

StringBuffer

  StringBuffer和String一样都是用来存储字符串的,只不过由于他们内部的实现方式不同,导致他们所使用的范围不同,对于StringBuffer而言,他在处理字符串时,若是对其进行修改操作,它并不会产生一个新的字符串对象,所以说在内存使用方面它是优于String的。
其实在使用方法,StringBuffer的许多方法和String类都差不多,所表示的功能几乎一模一样,只不过在修改时StringBuffer都是修改自身,而String类则是产生一个新的对象,这是他们之间最大的区别。
同时StringBuffer是不能使用=进行初始化的,它必须要产生StringBuffer实例,也就是说你必须通过它的构造方法进行初始化。
在StringBuffer的使用方面,它更加侧重于对字符串的变化,例如追加、修改、删除,相对应的方法:
1、append():追加指定内容到当前StringBuffer对象的末尾,类似于字符串的连接,这里StringBuffer对象的内容会发生改变。
2、insert:该类方法主要是在StringBuffer对象中插入内容。
3、delete:该类方法主要用于移除StringBuffer对象中的内容。

StringBuilder

   StringBuilder也是一个可变的字符串对象,他与StringBuffer不同之处就在于它是线程不安全的,基于这点,它的速度一般都比StringBuffer快。与StringBuffer一样,StringBuider的主要操作也是append与insert方法。这两个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符添加或插入到字符串生成器中。
上面只是简单的介绍了String、StringBuffer、StringBuilder,其实对于这三者我们应该更加侧重于他们只见到的区别,只有理清楚他们之间的区别才能够更好的使用他们。

什么时候使用哪个

对于这三者使用的场景做如下概括(参考:《编写搞质量代码:改善java程序的151个建议》):
1、String:在字符串不经常变化的场景中可以使用String类,如:常量的声明、少量的变量运算等。
2、StringBuffer:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。
3、StringBuilder:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,如SQL语句的拼装、JSON封装等(貌似这两个我也是使用|StringBuffer)

字符串拼接方式

对于字符串而言我们经常是要对其进行拼装处理的,在java中提高了三种拼装的方法:+、concat()以及append()方法。这三者之间存在什么区别呢?先看如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  public class StringTest {  

/**
* @desc 使用+、concat()、append()方法循环10W次
* @author chenssy
* @data 2013-11-16
* @param args
* @return void
*/

public static void main(String[] args) {
//+
long start_01 = System.currentTimeMillis();
String a = "a";
for(int i = 0 ; i < 100000 ; i++){
a += "b";
}
long end_01 = System.currentTimeMillis();
System.out.println(" + 所消耗的时间:" + (end_01 - start_01) + "毫米");

//concat()
long start_02 = System.currentTimeMillis();
String c = "c";
for(int i = 0 ; i < 100000 ; i++){
c = c.concat("d");
}
long end_02 = System.currentTimeMillis();
System.out.println("concat所消耗的时间:" + (end_02 - start_02) + "毫米");

//append
long start_03 = System.currentTimeMillis();
StringBuffer e = new StringBuffer("e");
for(int i = 0 ; i < 100000 ; i++){
e.append("d");
}
long end_03 = System.currentTimeMillis();
System.out.println("append所消耗的时间:" + (end_03 - start_03) + "毫米");
}
}

------------
Output:
+ 所消耗的时间:19080毫米
concat所消耗的时间:9089毫米
append所消耗的时间:10毫米

从上面的运行结果可以看出,append()速度最快,concat()次之,+最慢。原因请看下面分解:

+方式拼接字符串

在前面我们知道编译器对+进行了优化,它是使用StringBuilder的append()方法来进行处理的,我们知道StringBuilder的速度比StringBuffer的速度更加快,但是为何运行速度还是那样呢?主要是因为编译器使用append()方法追加后要同toString()转换成String字符串,也就说 str +=”b”等同于
str = new StringBuilder(str).append(“b”).toString();
它变慢的关键原因就在于new StringBuilder()和toString(),这里可是创建了10W个StringBuilder对象,而且每次还需要将其转换成String,速度能不慢么?

concat()方法拼接字符串

1
2
3
4
5
6
7
8
9
10
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);
}

这是concat()的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的:return new String(buf,true);这同样也创建了10W个字符串对象,这是它变慢的根本原因。

append()方法拼接字符串

1
2
3
4
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}

StringBuffer的append()方法是直接使用父类AbstractStringBuilder的append()方法,该方法的源码如下:

1
2
3
4
5
6
7
8
9
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);//扩容
str.getChars(0, len, value, count);//把字符串str全部字符复制到value数组中,从count下标开始
count += len;//添加后的字符串长度
return this;//返回本类,也就是String
}

与concat()方法相似,它也是进行字符数组处理的,加长,然后拷贝,但是请注意它最后是返回并没有返回一个新串,而是返回本身,也就说这这个10W次的循环过程中,它并没有产生新的字符串对象。
通过上面的分析,我们需要在合适的场所选择合适的字符串拼接方式,但是并不一定就要选择append()和concat()方法,原因在于+根据符合我们的编程习惯,只有到了使用append()和concat()方法确实是可以对我们系统的效率起到比较大的帮助,才会考虑,同时鄙人也真的没有怎么用过concat()方法。

传送门

热评文章