Java面试_String类
一、String类的不可变性
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
}
- String 类为 final类,不可被继承(final类中的中的方法默认为final)。
- String 字符串是通过 char 数组来保存字符串的,而且为 final 不可变。
二、字符串常量池
我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。
Java中的常量池分为两种(静态常量池和运行时常量池):
- 静态常量池 :即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
- 运行时常量池:jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
代码一:
String s1 = "Hello";
String s2 = new String("Hello");
String s3 = new String("Hello");
System.out.println( s1 == s2 ); // false
System.out.println( s2 == s3 ); // false new出的是两个不同的实例
- s1在虚拟机栈中直接存放 “Hello” 字符串在常量池中的引用;
- s2则是new一个String的实例存放于堆中,由String实例指向常量池中的字符串
- 整个过程创建了6个对象
三、String 的创建方法
1. 使用""来创建字符串
- 单独使用""引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;
- 凡是使用""引号创建的字符串的连接,都可以在编译时期确定,并放入静态常量池中,等运行时在加载到运行时常量池中。
2. 使用new关键字创建字符串
- 使用new String("")创建的对象会存储到heap中,是运行期新创建的。
注意:
使用new关键字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!
四、String的 “+” 操作
例1:
- String 在执行 + 操作时,编译时期会进行优化。
- 如果是多个字面量相加,则会在编译时期直接把组合好的字符串放入常量池。
- 如果有字符串常量参与,以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。
代码二:
String s1 = "aa"; // 在编译时期,在常量池中创建 "aa"
String s2 = "bb"; // 在编译时期, 在常量池中创建 "bb"
String s3 = "aa" + "bb"; // 在编译时期,JVM进行优化,在常量池中创建 "aabb"
// 等价于 String s4 = new StringBuilder().append("aa").append("bb").toString();
String s4 = s1 + s2 ;
//等价于 String s5 = new StringBuffer("1122").append("aa").append("33").append("44").append("bb").toString();
String s5 = "11" + "22" + s1 + "33" + "44" + s2;
例2:
代码三:
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”的一个引用。所以我们得出s0==s1==s2。
** 例3:**
代码四:
String s0="helloworld";
String s1=new String("helloworld");
String s2="hello" + new String("world");
System.out.println( s0==s1 ); //false
System.out.println( s0==s2 ); //false
System.out.println( s1==s2 ); //false
分析:
用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
s0还是常量池中"helloworld”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象"helloworld”的引用,s2因为有后半部分new String(”world”)所以也无法在编译期确定,所以也是一个新创建对象"helloworld”的引用。
例4:
代码五:
String str1="abc";
String str2="def";
String str3=str1+str2;
System.out.println(str3=="abcdef"); //false
分析:
因为str3指向堆中的"abcdef"对象,而"abcdef"是字符串池中的对象,所以结果为false。JVM对String str=“abc"对象放在常量池中是在编译时做的,而String str3=str1+str2是在运行时刻才能知道的。new对象也是在运行时才做的。而这段代码总共创建了5个对象,字符串池中两个、堆中三个。+运算符会在堆中建立来两个String对象,这两个对象的值分别是"abc"和"def”,也就是说从字符串池中复制这两个值,然后在堆中创建两个对象,然后再建立对象str3,然后将"abcdef"的堆地址赋给str3。
步骤:
1 栈中开辟一块中间存放引用str1,str1指向池中String常量"abc"。
2 栈中开辟一块中间存放引用str2,str2指向池中String常量"def"。
3 栈中开辟一块中间存放引用str3。
4 str1 + str2通过StringBuilder的最后一步toString()方法还原一个新的String对象"abcdef",因此堆中开辟一块空间存放此对象。
5 引用str3指向堆中(str1 + str2)所还原的新String对象。
6 str3指向的对象在堆中,而常量"abcdef"在池中,输出为false。
例5:编译期优化
代码六
String s0 = "a1";
String s1 = "a" + 1;
System.out.println((s0 == s1)); // true
String s2 = "atrue";
String s3= "a" + "true";
System.out.println((s2 == s3)); // true
String s4 = "a3.4";
String s5 = "a" + 3.4;
System.out.println((s4 == s5)); // true
分析:
在程序编译期,JVM就将常量字符串的"+“连接优化为连接后的值,拿"a” + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。
例6: 编译期无法确定
代码七:
String s0 = "ab";
String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); // false
分析:
JVM对于字符串引用,由于在字符串的"+“连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a” + s1无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s2。所以上面程序的结果也就为false。
例7:编译期无法确定
代码八:
public static void main(String[] args){
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
}
private static String getS1() {
return "b";
}
分析:
这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
五、intern()方法
一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。
存在于.class文件中的常量池,在运行期间被jvm装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,java查找常量池中是否有相同unicode的字符串常量,如果有,则返回其引用,如果没有,则在常量池中增加一个unicode等于str的字符串并返回它的引用。
(因为当new出一个字符串时,如果之前常量池中没有该字符串,则不会在编译时期就在常量池中创建该字符串,就应使用intern()方法)
六、 equals 和 ==
1. ==
- 对于基本数据类型(int、long、short 等等),采用 == 比较则比较的是值是否相等。
- 对于引用类型则比较的是引用是否相等而不是值是否相等。
2. equals
- equals()为Object中的方法,阅读Object类的原码发现,Object 中的equals方法为return this== a;也是通过 ==来进行比较的。
- 所以所有类如果要使用equals进行比较,则应该重写equals方法,而String类重写的equals方法比较的位值。
七、String、StringBuider 和 StringBuffer的区别
https://blog.csdn.net/qq_39541319/article/details/80445197
八、String中的final用法和理解
final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;//此句编译不通过
final StringBuffer a = new StringBuffer("111");
a.append("222");//编译通过
final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。
九、 关于String str = new String(“abc”)创建了多少个对象?
- 只创建了一个对象。这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。
- 该段代码在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
引用博客:(该博客为学习笔记,侵删)
https://blog.csdn.net/sugar_rainbow/article/details/68150249
https://blog.csdn.net/wdsdads/article/details/81262105