标签:创建对象 改变 效率 app 优化 替换 end 修改 更新
摘要:
Java 中的 String类 是我们日常开发中使用最为频繁的一个类,但要想真正掌握的这个类却不是一件容易的事情。笔者为了还原String类的真实全貌,先分为上、下两篇博文来综述Java中的String类。笔者从Java内存模型展开,结合 JDK 中 String类的源码进行深入分析,特别就 String类与享元模式,String常量池,String不可变性,String对象的创建方式,String与正则表达式,String与克隆,String、StringBuffer 和 StringBuilder 的区别等几个方面对其进行详细阐述、总结,力求能够对String类的原理和使用作一个最全面和最准确的介绍。
友情提示:
应CSDN博友建议,笔者将原文《Java String 综述》(“特别长的巨文…哈哈”)分为上下两篇。上篇主要介绍 Java内存模型与常量池、常量与变量、String定义与基础、String的不可变性 和 String对象创建方式五部分内容,烦请各位看官先移步到 《Java String 综述(上篇)》进行阅读,以便更好理解本文内容。下篇(即本文)主要介绍字符串常量池、String,StringBuilder 和 StringBuffer 三个字符串类的联系和区别、String与正则表达式、String与(深)克隆和String总结五部分内容。
版权声明:
本文原创作者:书呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
1、字符串池
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串字面值的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串常量池,每当以字面值形式创建一个字符串时,JVM会首先检查字符串常量池:如果字符串已经存在池中,就返回池中的实例引用;如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突进行共享。 例如:
public class Program
{
public static void main(String[] args)
{
String str1 = "Hello";
String str2 = "Hello";
System.out.print(str1 == str2); // true
}
}
一个初始为空的字符串池,它由类 String 私有地维护。当以字面值形式创建一个字符串时,总是先检查字符串池是否含存在该对象,若存在,则直接返回。此外,通过 new 操作符创建的字符串对象不指向字符串池中的任何对象。
2、 手动入池
一个初始为空的字符串池,它由类 String 私有地维护。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
它遵循以下规则:
对于任意两个字符串 s 和 t ,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true 。
public class TestString
{
public static void main(String args[]){
String str1 = "abc";
String str2 = new String("abc");
String str3 = s2.intern();
System.out.println( str1 == str2 ); //false
System.out.println( str1 == str3 ); //true
}
}
所以,对于 String str1 = “abc”,str1 引用的是常量池(方法区)的对象;而 String str2 = new String(“abc”),str2引用的是堆中的对象,所以内存地址不一样。但是由于内容一样,所以 str1 和 str3 指向同一对象。
3、小结
使用字面值形式创建的字符串与通过 new 创建的字符串一定是不同的,因为二者的存储位置不同:前者在方法区,后者在堆;
我们在使用诸如String str = “abc”;的格式创建字符串对象时,总是想当然地认为,我们创建了String类的对象str。但是 对象可能并没有被创建!唯一可以肯定的是,指向 String 类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为”abc”的String类。
使用字面值形式创建字符串的方式的理念是 享元模式;
JAVA编译器对 string + 基本类型/常量 是当成常量表达式直接求值来优化的;对诸如 “str4 = str2+str3”【StringBuilder 连接,在堆中创建新对象】 或 “str4 = 基本类型/常量 + str2” 或 “str4 = 基本类型/常量 + new String() ” 等在编译期是不能确定的**;
看下面代码来深入理解String:
public static void main(String[] args) {
/**
* 情景一:字符串池
* JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象;
* 并且可以被共享使用,因此它提高了效率。
* 由于String类是final的,它的值一经创建就不可改变。
* 字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
*/
String s1 = "abc";
//↑ 在字符串池创建了一个对象
String s2 = "abc";
//↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象
System.out.println("s1 == s2 : "+(s1==s2));
//↑ true 指向同一个对象,
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
//↑ true 值相等
//↑------------------------------------------------------over
/**
* 情景二:关于new String("")
*
*/
String s3 = new String("abc");
//↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中;
//↑ 还有一个对象引用s3存放在栈中
String s4 = new String("abc");
//↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象
System.out.println("s3 == s4 : "+(s3==s4));
//↑false s3和s4栈区的地址不同,指向堆区的不同地址;
System.out.println("s3.equals(s4) : "+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1 == s3 : "+(s1==s3));
//↑false 存放的地区都不同,一个方法区,一个堆区
System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
//↑true 值相同
//↑------------------------------------------------------over
/**
* 情景三:
* 由于常量的值在编译的时候就被确定(优化)了。
* 在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。
* 这行代码编译后的效果等同于: String str3 = "abcd";
*/
String str1 = "ab" + "cd"; //1个对象
String str11 = "abcd";
System.out.println("str1 = str11 : "+ (str1 == str11));
//↑------------------------------------------------------over
/**
* 情景四:
* 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。
*
* 第三行代码原理(str2+str3):
* 运行期JVM首先会在堆中创建一个StringBuilder类,
* 同时用str2指向的拘留字符串对象完成初始化,
* 然后调用append方法完成对str3所指向的拘留字符串的合并,
* 接着调用StringBuilder的toString()方法在堆中创建一个String对象,
* 最后将刚生成的String对象的堆地址存放在局部变量str3中。
*
* 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。
* str4与str5地址当然不一样了。
*
* 内存中实际上有五个字符串对象:
* 三个拘留字符串对象、一个String对象和一个StringBuilder对象。
*/
String str2 = "ab"; //1个对象
String str3 = "cd"; //1个对象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 : " + (str4==str5)); // false
//↑------------------------------------------------------over
/**
* 情景五:
* JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。
* 运行期的两个string相加,会产生新的对象的,存储在堆(heap)中
*/
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 : "+ (str7 == str67));
//↑str6为变量,在运行期才会被解析。
final String str8 = "b";
String str9 = "a" + str8;
String str89 = "ab";
System.out.println("str9 = str89 : "+ (str9 == str89));
//↑str8为常量变量,编译期会被优化
//↑------------------------------------------------------over
}
1.String 与 StringBuilder
简要的说, String 类型 和 StringBuilder 类型的主要性能区别在于 String 是不可变的对象。 因此,在每次对 String 类型进行改变时,其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。所以,经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。而如果是使用 StringBuilder 类则结果就不一样了,每次结果都会对 StringBuilder 对象本身进行操作,而不是生成新的对象并改变对象引用。所以,在一般情况下,推荐使用 StringBuilder ,特别是字符串对象经常改变的情况下。
而在某些特别情况下,String 对象的字符串拼接可以直接被 JVM 在编译器确定下来。所以,这时在速度上,StringBuilder 不占任何优势。
String S1 = “This is only a” + “ simple” + “ test”; //编译期完成字符串常量的串联,相当于“This is only a simple test”
StringBuffer Sb = new StringBuilder(“This is only a”).append(“simple”).append(“ test”);
对于
String S1 = “This is only a” + “ simple” + “test”;
其实就是:
String S1 = “This is only a simple test”;
需要注意的是,如果是下面的情形,其内部实现是先new一个 StringBuilder,然后调用其 append 方法连接,效率会较低。
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;
因此,在大部分情况下, 在效率方面:StringBuilder > String .
2.StringBuffer 与 StringBuilder
首先,JDK的实现中 StringBuffer 与 StringBuilder 都继承自 AbstractStringBuilder。AbstractStringBuilder的实现原理为:AbstractStringBuilder中采用一个 char数组 来保存需要append的字符串,char数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式,这里是 2 倍。
【
StringBuffer 始于 JDK 1.0
StringBuilder 始于 JDK 1.5
从 JDK 1.5 开始,对含有字符串变量(非字符串字面值)的连接操作(+),JVM 内部采用的是
StringBuilder 来实现的,而之前这个操作是采用 StringBuffer 实现的。
】
Java.lang.StringBuffer 是线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
java.lang.StringBuilder 也是一个可变的字符序列,是 JDK 5.0 新增的。此类提供一个与 StringBuffer 兼容的 API,即:StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。
因此,在单线程下,优先使用 StringBuilder.
对于三者使用的总结:
单线程 操作字符串缓冲区下操作大量数据 StringBuilder , 多线程 操作字符串缓冲区下操作大量数据 StringBuffer;
如果所操作数据不怎么变化(String 是不可变的)或字符串常量粘结(编译期优化为字符串常量)或操作较为简单(不包括循环等),则用 String;否则,用下面二者
例如:
String s = “a” + "b” + "c”;
String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = s1 + s2 + s3;
分析:变量s的创建等价于 String s = “abc”; 由上面例子可知编译器进行了优化,这里只创建了一个对象。由上面的例子也可以知道; s4不能在编译期进行优化,其对象创建相当于:
StringBuffer temp = new StringBuffer();
temp.append(s1).append(s2).append(s3);
String s = temp.toString();
由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因分析,形如这样的代码:
public class Test {
public static void main(String args[]) {
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
}
}
每做一次 + 就产生一个 StringBuilder 对象,然后append后就扔掉。下次循环再到达时重新 new 一个 StringBuilder 对象,然后append 字符串,如此循环直至结束。如果我们直接采用 StringBuilder 对象进行append的话,我们可以节省N - 1次创建和销毁对象的时间。所以,对于在循环中要进行字符串连接的应用,一般都是用StringBulider对象来进行append操作。
正则表达式:用一个字符串来描述一个特征,然后去验证另一个字符串是否符合这个特征。使用正则表达式,我们能够以编程的方式,构造复杂的文本模式,并对输入的字符串进行搜索;
java转义(\) 与 正则表达式转义(\\);
使用 Pattern 与 Matcher 构造功能强大的正则表达式对象.
1、基础
目标:制造一个对象的副本
类型:Shallow Clone ; Deep Clone
Clone & Copy
假设现在有一个Employee对象,Employee tobby = new Employee(“CMTobby”,5000),通常, 我们会有这样的赋值Employee cindyelf=tobby,这个时候只是简单了copy了一下reference,cindyelf和tobby都指向内存中同一个object,这样cindyelf或者tobby的一个操作都可能影响到对方。打个比方,如果我们通过cindyelf.raiseSalary()方法改变了salary域的值,那么tobby通过getSalary()方法得到的就是修改之后的salary域的值,显然这不是我们愿意看到的。我们希望得到tobby的一个精确拷贝,同时两者互不影响,这时候我们就可以使用Clone来满足我们的需求。Employee cindy=tobby.clone(),这时会生成一个新的Employee对象,并且和tobby具有相同的属性值和方法。
Shallow Clone & Deep Clone
Clone是如何完成的呢?Object在对某个对象实施Clone时对其是一无所知的,它仅仅是简单地执行域对域的copy,这就是Shallow Clone。这样,问题就来了咯,以Employee为例,它里面有一个域hireDay不是基本型别的变量,而是一个reference变量,经过Clone之后就会产生一个新的Date型别的reference,它和原始对象中对应的域指向同一个Date对象,这样克隆类就和原始类共享了一部分信息,而这样显然是不利的,过程下图所示:
这个时候我们就需要进行 Deep Clone了,对那些非基本型别的域进行特殊的处理,例如本例中的hireDay。我们可以重新定义Clone方法,对hireDay做特殊处理,如下代码所示:
class Employee implements Cloneable
{
public Object clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone() ; //public class Date implements java.io.Serializable, Cloneable, Comparable<Date>
return cloned;
}
}
因此,Object 在对某个对象实施 Clone 时,对其是一无所知的,它仅仅是简单执行域对域的Copy. 其中,对八种基本类型的克隆是没有问题的,但当对一个对象进行克隆时,只是克隆了它的引用。因此,克隆对象和原始对象共享了同一个对象成员变量,故而提出了深克隆 : 在对整个对象浅克隆后,还需对其引用变量进行克隆,并将其更新到浅克隆对象中去。
2、Clone()方法的保护机制
在Object中Clone()是被申明为protected的,这样做是有一定的道理的,以 Employee 类为例,通过申明为protected,就可以保证只有Employee类(及其子类)里面才能“克隆”Employee对象。
3、Clone()方法的使用
Clone()方法的使用比较简单,注意如下几点即可:
明白 String 在克隆中的特殊性
String 在克隆时只是克隆了它的引用。
奇怪的是,在修改克隆后的 String 对象时,其原来的对象并未改变。原因是:String是在内存中不可以被改变的对象,就比如说在for大量循环中不推荐使用+的方式来拼凑字符串一样,每次使用+都会新分配一块内存,不在原来上修改,原来的没有指向它的引用,会被回收。所以克隆相当于1个String内存空间有两个引用,当修改其中的一个值的时候,会新分配一块内存用来保存新的值,这个引用指向新的内存空间,原来的String因为还存在指向他的引用,所以不会被回收,这样,虽然是复制的引用,但是修改值的时候,并没有改变被复制对象的值。
所以在很多情况下,我们可以把 String 在clone的时候和基本类型做相同的处理。
字符串比较时用的什么方法,内部实现如何?
使用equals方法 : 先比较引用是否相同(是否是同一对象),再检查是否为同一类型(str instanceof String), 最后比较内容是否一致(String 的各个成员变量的值或内容是否相同)。这也同样适用于诸如 Integer 等的八种包装器类。
更多关于字面量的介绍请移步我的博文《Java 原生类型与包装器类型深度剖析》;
更多关于享元模式的介绍请移步我的博文《深入理解享元模式》。
引用
JVM内存模型及垃圾回收算法
深入理解Java:String
java中特殊的String类型
Java中的String为什么是不可变的? – String源码分析
String,StringBuffer与StringBuilder的区别及应用场景
JAVA中的clone方法剖析
java克隆中String的特殊性
Java堆、栈和常量池以及相关String的详细讲解(经典中的经典)
什么是字符串常量池?
java中的堆、栈和常量池
1
1
1
1
标签:创建对象 改变 效率 app 优化 替换 end 修改 更新
原文地址:http://blog.csdn.net/justloveyou_/article/details/60983034