随手记一下String和常量池的一些知识点。
首先需要大概的了解编译期
和运行期
两个概念。
当编写完JAVA程序之后,首先将java文件编译成class文件,这就是编译期要做的事情,在编译期编译器会对程序进行一些优化。
当class文件编译出来之后,开始运行程序,将class文件动态的加载进JVM,程序就进入了运行期。
常量池(以下名字不准确,是自己叫的):
- 静态常量池:程序编译完成之后,程序中一些不会改变的常量会保存到class文件中的一块地方,这里就是静态常量池
- 动态常量池:程序开始运行的时候,将静态常量池载入了JVM,在JVM的方法区中就产生了一块位置,就是运行期的动态常量池
问题
有一些涉及到String和常量池的问题,基本上就是比较字符串之间是否相等,为啥相等,又为啥不相等,我觉得应该是这样:
String是一个引用数据类型,不是基本数据类型,所以对其创建一个对象应该是在堆
上分配空间的,但是String又是特殊的,它拥有向基本数据类型一样的字面量
(String s = “hello, world” 其中"hello, world"就是字面量),字面量是常量,是要放到常量池中的,这样就可以提高String的复用性,可以多个String指向常量池中的同一个常量。String还拥有+
运算符,不过其作用是连接两个字符串,而不是代数相加。
在编译期
,常量就会被放置到静态常量池中去,等到运行期
的时候加载到内存中去。
看几个例子吧。
// 在堆中创建了一个新的String对象,该对象指向常量池中的 "HelloWorld"
String s1 = new String("HelloWorld");
// 又在堆中创建了一个新的String对象,该对象也指向常量池中的 "HelloWorld"
String s2 = new String("HelloWorld");
// 指向常量池中的 "HelloWorld"
String s3 = "HelloWorld";
// 也指向常量池中的 "HelloWorld"
String s4 = "HelloWorld";
// 指向常量池中的 "Hello"
String s5 = "Hello";
// 指向常量池中的 "World"
String s6 = "World";
System.out.println(s1 == s2); // false s1与s2是两个不同的对象,指向了堆中的两块不同的地址
System.out.println(s3 == s4); // true s2与s4都是字面值常量,指向的是常量池中相同的地址
System.out.println(s1 == s4); // false s1和s4一个地址在堆中,一个地址在常量池中
System.out.println("=========================================");
// false
System.out.println(s1 == (s5 + s6));
// false 如果字符串拼接时如果两边只要有一个是字符串变量,就会在堆中生成新对象,也就是说s3指向常量池,而s5+s6指向堆
System.out.println(s3 == (s5 + s6));
// true 如果是字面量拼接的话其实在编译的时候就已经拼接好了,就是常量池中的HelloWorld
System.out.println(s3 == ("Hello" + "World"));
// false
System.out.println(s3 == (s5 + "World"));
结论是(重要):
1、如果使用new在堆中创建了新的字符串对象,不同的对象在堆上的地址自然是不同的,也就通过==
进行比较自然不相等,因为==
比较引用数据类型是比较其指向的对象是否相同。
2、如果String引用指向的是一个字符串的字面量,那么其指向的地址都会是常量池中的同一块地址,所以地址是相同的,使用==
进行比较也自然是相同的。
3、如同 String s = "Hello" + " World";
这种情况,也就是将字面量使用+
连接,在编译期会被直接优化成String s = "Hello World"
,也就是说和直接使用字面量没有什么区别,s
指向的还是常量池中的"Hello World"。
4、像3中说的那样,不过如果+
的两边存在着变量的话,情况就不一样了,因为字符串是不可变的
,所以两个字符串使用+
进行连接其实是使用StringBuilder
将两个字符串连接起来,然后使用其toString
方法,返回一个新的字符串对象,该对象是在堆上的。
5、如果两个字符串变量都使用final
关键字修饰的话,则说明这两个字符串的引用都不能指向新的值,也就是成为了一个常量,这种情况下这两个字符串常量使用+
运算符连接,与两个字面量常量连接是一样的,都是在编译期
将其放置到常量池中去。
// 再看一下这一个例子
public static final String s1 = "HelloWorld";
public static final String s2 = "HelloWorld";
public static final String s3 = "Hello";
public static final String s4 = "World";
public static final String s5;
public static final String s6;
public static final String s7;
public static final String s8;
public static String s9 = "Hello";
public static String s10 = "World";
static {
s5 = "HelloWorld";
s6 = "HelloWorld";
s7 = "Hello";
s8 = "World";
}
public static void main(String[] args) {
System.out.println(s1 == s2);
System.out.println(s5 == s6);
System.out.println(s1 == s5);
System.out.println("========================");
// true 因为此时s3和s4都是被final修饰且已经被显式赋值,其值已经确定,等同两个字面值相加,在编译期已经完成了字符串拼接
System.out.println(s1 == (s3 + s4));
// false
System.out.println(s1 == (s7 + s8));
// false
System.out.println(s5 == (s7 + s8));
// false
System.out.println(s1 == (s9 + s10));
}
还有就是String类有一个实例方法intern
,会检查常量池中有没有和当前这个字符串内容一样的字符串常量,如果没有的话就将该该字符串加入到常量池中去,并且不管有没有都会返回一个常量池中该字符串的字符串常量。这也是极少数的能够在运行期
将字符串加入到常量池中的方法。