1.字符串的创建
常量池
Java中的常量池分为两种形态:静态常量池和运行时常量池。
**静态常量池:*即.class文件中的常量池,class文件中的常量池不仅仅包括字符串,还包括类、方法的信息,占用class文件绝大部分空间。
**运行时常量池:**JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中。
我们常说的常量池,就是指方法区中的运行时常量池。
String a="Hello World";
此种创建方式下,编译器首先检查常量池中是否含有此字符串常量,没有则创建,然后JVM会开辟一块堆内存存放一个隐式对象指向该字符串常量,接着在栈空间里创建a变量,a变量指向该块堆内存首地址。
String a = new String("Hello World");
此种创建方式下,直接在堆内存new一个对象,并在常量池中创建“Hello World”字符串,该对象指向常量池中的“Hello World”字符串,然后创建a变量,并将a变量指向该块堆内存首地址。
2.字符串的特性
String是一个不可变的字符序列
String a1 = new String("Hello");
String a2 = new String("World");
a1 += a2;
此段代码执行时,首先在内存中分配一块空间,它的大小为a1和a2空间大小之和,然后将a1和a2的内容复制到该内存空间相应位置,最后将变量a1指向该内存空间。
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
........
}
从String源码中分析可知,String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。并且String类是通过char数组来存储字符串。
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
分析String类中的substring方法可以发现,该方法的操作并不改变原有字符串的值,而是返回了一个新的字符串对象,这也与String类是一个不可改变的字符序列相吻合。
3.字符串的比较
“==”比较的是两个字符串地址是否相同,而.equals()方法比较的是两个字符串内容是否相同。
情况一:
String s1 = new String("java");
String s2 = new String("java");
System.out.println(s1==s2); //false
System.out.println(s1.equals(s2)); //true
此段代码创建的是两个对象,他们有不同的堆空间,因此“==”为false;但内容相同,因此.equals()为true。
情况二:
String s1 = new String("java");
String s2 = s1;
System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
此段代码中变量s1,s2指向的为同一堆空间,并且字符串内容相同,因此均为true。
情况三:
String s1 = "java";
String s2 = "java";
System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
此段代码中,字符串创建方式为隐式创建,即,将String视为基本数据类型,因此,若字符串值相同,则s1,s2指向同一对象;若不同,则指向不同对象。
情况四:
String s0="helloworld";
String s1="helloworld";
String s2="hello"+"world";
System.out.println(s0==s1); //true 可以看出s0跟s1是指向同一个对象
System.out.println(s0==s2); //true 可以看出s0跟s2是指向同一个对象
此段代码中的s0和s1中的"helloworld”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而"hello”和"world”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中"helloworld”的一个引用。
情况五:
String s0="helloworld";
String s1=new String("helloworld");
String s2="hello" + new String("world");
String s3="aaa";
String s4=s0+s3;
System.out.println( s0==s1 ); //false
System.out.println( s0==s2 ); //false
System.out.println( s1==s2 ); //false
System.out.println( s4=="helloworldaaa" ); //false
用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
s0还是常量池中"helloworld”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象"helloworld”的引用,s2因为有后半部分new String(”world”)所以也无法在编译期确定,所以也是一个新创建对象"helloworld”的引用。
String s4=s0+s3是运行时才可知的,而"helloworldaaa"是在编译时就存放在常量池中的。在之前说过,String的所有函数都是返回的一个新字符串而不改变原字符串,因此+运算符会在堆中建立来两个String对象,这两个对象的值分别是"helloworld"和"aaa",也就是说从字符串池中复制这两个值,接着在堆中创建两个对象,然后再在栈空间建立变量s4,将 "helloworldaaa"的堆地址赋给s4。
情况六:
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); // true
s1字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + s1和"a" + "b"效果是一样的。故上面程序的结果为true。
情况七:
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); // false
private static String getS1() {
return "b";
}
这里虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
4.String、StringBuffer、StringBuilder的区别
(1)可变与不可变:String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变)。
(2)是否多线程安全:String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是非线程安全的。
5.关于String str = new String(“abc”)创建了多少个对象?
网上流传的以及一些面试书籍上都说是2个对象,这种说法是片面的。
首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建2个对象么?毫无疑问不可能,用javap -c反编译即可得到JVM执行的字节码内容:
很显然,new只调用了一次,也就是说只创建了一个对象。而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成 String str = new String(“abc”)涉及到几个String对象?
合理的解释是2个。