一,String类
1,String类的特点(由源码分析)
public
final
class
String
implements
java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private
final
char
value[];
/** The offset is the first index of the storage that is used. */
private
final
int
offset;
/** The count is the number of characters in the String. */
private
final
int
count;
/** Cache the hash code for the string */
private
int
hash;
// Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private
static
final
long
serialVersionUID = -6849794470754667710L;
}
由上可知:
(1)String类被final修饰,所以String类不能被继承,也是线程安全的
(2)
private
final
char
value[];
//用于存储字符串
private int hash; // 缓存String的hash值
(3)String类的方法实现:(源码)
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操作不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
2,String的创建存储以及比较
字符串的两种创建方式:String str="fuheiuG";(存储在常量池中) String str=new string("fgaueiug");(new对象存储在堆中)
JVM内存机制中,在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。
因此在上述代码中,String str1 = "hello world";和String str3 = "hello world"; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。
总所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。
例如:public class StringCompare {
public static void A() {
String str1 = "java";
String str2 = "java";
System.out.println(str1 == str2); //true 原因很简单,String对象被放进常量池里了,再次出现“java”字符串的时候,JVM很兴奋地把str2的引用也指向了 “java”对象,它认为自己节省了内存开销
}
public static void B() {
String str1 = new String("java");
String str2 = new String("java");
System.out.println(str1 == str2); //false new是在堆中创建了两个不同的对象。
}
public static void D() {
String s1 = "java";
String s2 = new String("java");
System.out.println(s1 == s2.); //false, ==比较引用,因为对象在不同的位置创建。所以对象不可能相同。
}
public static void D2() {
String s1 = "java";
String s2 = new String("java");
System.out.println(s1 == s2.intern()); //true, 使用intern方法可以通过==比较对象的内容,实质上intern方法运行时会在常量池中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用,如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。 intern()方法主要可以用来避免创建内容重复的String对象,值得我们重视。
public static void C() {
String str1 = "java";
String str2 = "blog";
String s = str1 + str2;
System.out.println(s == "javablog"); //false str1和str2和Javablog在编译时刻就已经放在常量池中了,二s是在运行期间在堆中创建,所以s引用不可能指向常量池中的对象。
}
public static void C2() {
String str1 = "javablog";
String str2 = "java"+"blog"; //在编译时被优化成String str2 = "javablog";
System.out.println(str1 == str2); //true
}
public static void E() {
String str1 = "java";
String str2 = new String("java");
System.out.println(str1.equals(str2)); //true 无论在常量池还是堆中的对象,用equals()方法比较的就是内容,就这么简单!
}
StringBuffer的insert方法:
1 2 3 4 5 6 |
|
二, String vs StringBuilder vs StringBuffer
1,三者之间的区别:
StringBuilder 是可变的,因此可以在创建以后修改内部的值.(适用于单线程下在字符缓冲区进行大量操作的情况)
StringBuffer 是同步的,因此是线程安全的,但效率相对更低.(适用多线程下在字符缓冲区进行大量操作的情况)
String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行 创建和回收的操作,所以速度要比String快很多。
在运行速度方面:StringBuilder > StringBuffer > String,所以在不考虑线程安全的情况下(即单线程的情况下)应使用StringBuilder。。多线程的情况下应使用StringBuffer。
例如:
4 5 6 7 8 9 |
|
这句 string += "hello";的过程相当于将原有的string变量指向的对象内容取出与"hello"作字符串相加操作再存进另一个新的String对象当中,再让string变量指向新生成的对象。
string+="hello"的操作事实上会自动被JVM优化成StringBuilder进行操作。
1 2 3 4 5 6 7 8 9 |
|
这里new操作只进行了一次,也就是说只生成了一个对象,append操作是在原有对象的基础上进行的。因此这段代码所占的资源要比上面小得多。
2,字符串拼的实质:
StringBuilder的append() 工作原理, StringBuilder是调用了其父类的append方法的,我们来看源码:
public StringBuilder append(String str) { super.append(str); return this; }
public AbstractStringBuilder append(String str) { if (str == null) { str = "null"; } int len = str.length(); if (len == 0) { return this; } int newCount = count + len;//统计拼接后字符串长度 if (newCount > value.length) { expandCapacity(newCount);//如果拼接结果大于所用的char[],则扩容 } //getChars将拼接字符串赋值到char[]中 str.getChars(0, len, value, count); count = newCount;//更新char[]所保存的字符串长度 return this; }
可知,用StringBuilder拼接字符串时,其实是在底层创建了一个char[]数组,然后通过char[]把要拼接的字符串添加到char[]而已。最后通过toString()生成最终拼接结果时就是通过 return new String(char[]) 实现的。
从上面(其实应该说是五种,+ 分为字符串常量的拼接和变量的拼接两种)的字符串拼接来看,除了字符串常量的拼接是返回拘留字符串的地址外,其他四种(str1+str2、str1.concat(str2)、builder.append(str1).append(str2)、buffer.append(str1).append(str2))都是使用了StringBuilder,或者说是StringBuilder的父类的拼接方法来做的——创建一个char数组,把需要拼接的内容先存进char数组,最后通过char数组创建新的String对象返回。
造成三者性能差别的主要原因是:
用String的 + 累加拼接字符串变量时,每拼接一个就会创建一个StringBuilder,并通过append()方法拼接,然后返回一个新的String对象。然后再继续拼接下一个变量。这样就会导致重复创建StringBuilder对象,性能低下。用 concat() 累计拼接时,则每两个字符串拼接都会创建一个 char[] 进行内容拼接并返回一个新的String对象作为结果,重复调用concat()会导致重复创建char[]和新String对象,性能低下。
StringBuilder在调用toString()之前都不会创建拼接结果,并且底层的char数组会自动扩容,一直到拼接字符串全部存入char数组后,调用toString()时才创建新的String对象并返回,这样就避免了重复创建,效率提高。
StringBuffer则因为使用了syncrhoized对append()进行了加锁,所以导致性能稍微低于StringBuilder
注意:
(1)String str = new String("abc")创建了多少个对象?
什么叫“创建了”?什么时候创建了什么?
人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念 该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。
解释:一个是字符串字面量"abc"所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"abc"相同的实例
或者是:在运行时涉及几个String实例?(不涉及编译时期)是一个
(2)StringBuffer常见的方法:,StringBuilder的方法相同。
1,public StringBuffer append(String s) 将指定的字符串追加到此字符序列。
2,public StringBuffer reverse() 将此字符序列用其反转形式取代。
3,public delete(int start, int end) 移除子字符串中的字符(可以删除一个字符,也可以不删除一个字符,也可以是字符串)
4,public deleteCharAt(int i) 删除某一特定位置的字符,也可以用方法三完成。
5,public insert(int offset, String str) 将 string参数的字符串表示形式插入此序列中,这里的参数可以是任何类型的。int,double,数组...... ...跟append方法一样后面的参数可以是任何类型的数据。
6,replace(int start, int end, String str) 使用给定 String
中的字符替换字符串中的某段字符。
7,void setCharAt(int index, char ch) 修改某一特定位置的字符。注意参数只能是chat。
8,void setLength(int newLength) 设置字符序列的长度。