目录
String str = new String("abc")创建了多少个对象?
String、StringBuffer与StringBuilder
这是个人见过关于String相关知识最好的文章强烈推荐阅读:https://www.cnblogs.com/xiaoxi/p/6036701.html
String的相关原理
- String是通过char数组来保存数据的,并且String类是final类(在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法);
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** 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;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
public String() {
this.value = "".value;
}
}
- String使用private final char value[]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable)。所以对String对象的任何改变都不影响到原对象,相关的任何change操作(sub、concat、replace等)都会生成新的对象。
- new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)
- 字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。
String的常量池与比较
常量池的认识与理解
java中的常量池分为静态常量池与运行时常量池,所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
几种常量的比较
-
编译期就确定的字面量比较
@Test
//编译是就确定了,字符被加在常量池
public void test1(){
String s1 = "liu1";
String s2 = "liu1";
//true 这两个变量引用的是常量池中的“liu1",因此为true
System.out.println(s1==s2);
String s3 = "liu"+"1";
//在编译器做了优化直接将两个字符串转换为一个字符串,避免了运行时的性能消耗
System.out.println(s1==s3);
String s4 = "liu"+1;
//这里的1也可以理解为字面量,是不需要引用地址得到的,在编译的时候就可以被编译器优化得到
System.out.println(s1==s4);
}
-
运行期才能确定的对象类型比较
@Test
public void test2(){
String s1 = "liu1";
String s2 = new String("liu1");
//s1是在常量池中,而s2是存在于堆中的对象,再创建这个对象的时候,jvm会先去常量池中查看“liu1”
//的字符串是否存在,如果“liu1”存在再直接用这个字符串来创建这个对象,如果没有的话
System.out.println(s1==s2);
String s3 = new String("liu1");
//s2、s3字段都是在运行过程中创建在堆中的对象,每创建的一个对象都是新的所以这两个对象的比较是不相等的
System.out.println(s2==s3);
String s4 = "liu";
String s5 = s4+1;
//这里s4在编译器无法确定所以他与1的相加是形成的s5是在运行时期生成的对象,通常情况下在编译器可以确定
//的字段他们相加的时候JVM会进行相应的优化,如果相加的中有引用类型的,那么在编译期不能确定所以只有在
//运行的时候才能确定,关于引用的情况有一个例外那就是对象呗final修饰
System.out.println(s5==s1);
final String s6 = "li";
String s7 = s6+"u1";
//对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节
//码流中。所以s7对象也是在编译阶段就确定的,将s6+"u1"优化为liu1,而这个常量在常量池中本来就已经
//有了所以他们的比较为true
System.out.println(s7==s1);
final String s8 = getS8();
String s9 = s8+"u1";
//这里的s8虽然被final修饰但是因为要通过方法才能获取所以他的值在编译时候也是不能确定的,他们比较的
//结果为false
System.out.println(s9==s1);
final String s10 = getS10();
String s11 = s10+"u1";
//false
System.out.println(s11==s1);
}
private String getS8(){
return "li";
}
static String getS10(){
return "li";
}
个人总结:String的创建可以分为两种形式通过“”、new,通过引号创建的对象存储在常量池中这样的形式下不一定创建对象,通过new关键字创建的对象存储在堆中这样的情况下一定创建对象。由于JVM优化的存在,通过+拼接的字符串,在编译器可能被优化。通常是+两边的值如果在编译期是固定的就可以被优化成拼接后的字符串,存储在变量池中。如果两边的值在编译时是不确定的(引用、通过方法获取),那么他们就不会优化通常是在运行的时候进行操作形成相应的对象存储在堆中。这样的情况有一个例外那就是被final修饰的变量。这其中原因有关JVM对类的加载机制。
-
关于String的+操作:
- String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(通过反编译工具jd-gui也可以方便的直接看出);
- 接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。
也就是说String c = "xx" + "yy " + a + "zz" + "mm" + b; 实质上的实现过程是:
String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();
由此得出结论:当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。
-
Sting+操作引起的性能问题
每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。 如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行append操作。
@Test
public void test4(){
String s1 = null;
int count = 100000;
Long beginTime1 = new Date().getTime();
for (int i = 0; i <count ; i++) {
s1+="a";
}
System.out.println("+运行的时间为:"+(new Date().getTime()-beginTime1));
StringBuilder s2 = new StringBuilder();
Long beginTime2 = new Date().getTime();
for (int i = 0; i <count ; i++) {
s2.append("a");
}
System.out.println("StringBuilder运行的时间为:"+(new Date().getTime()-beginTime2));
}
在测试的时候随着count的增加两者运行的时间差距更加明显。
-
String str = new String("abc")创建了多少个对象?
该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。
个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。
关于String.intern()
intern方法使用:一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。存在于.class文件中的常量池,在运行期间被jvm装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,java查找常量池中是否有相同unicode的字符串常量,如果有,则返回其引用,如果没有,则在常量池中增加一个unicode等于str的字符串并返回它的引用。
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
@Test
public void test3(){
String s1 = "liu1";
String s2 = new String("liu1").intern();
System.out.println(s1==s2);
String s3 = new String("liu").intern();
String s4 = new String("liu").intern();
//通过s3与s4的对比我们可以看出通过String().intern()创建的对象最终是存储在变量池中的
//他也证明了变量池的可扩展性
System.out.println(s3==s4);
}
equals和==
(1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。
(2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。
(3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。
String、StringBuffer与StringBuilder
- 可变与不可变:String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变)。
- 是否多线程安全:String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是非线程安全的。
- String、StringBuilder、StringBuffer三者的执行效率:
- StringBuilder > StringBuffer > String 当然这个是相对的,不一定在所有情况下都是这样。比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:
- 当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
- 当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。