Java常量池总结 一、基本概念介绍 什么是常量 用final修饰的变量即为常量(成员变量、局部变量、静态变量,只要被final给修饰过的)。 什么是常量池 先说静态常量池,来看看class文件中都包含了哪些内容。 其中前四个字节为魔数(确定一个class文件能否被JVM解析),后四个字节为存储版本号,其中前两个字节为次版本号,后两个为主版本号,最后就是静态常量池,其中又包含字面量,和符号引用,字面量指的就是一些字符串和声明为final类型的常量,符号引用指的是字符串在常量池的引用,包括类和接口的全限定名(如:com/test/Test),方法以及属性的名称和描述符。 以上就是class文件的内容,运行时常量池为方法区的一部分,上述的class文件中除了有class的类信息,方法字段信息,超类以及接口信息,还有静态常量池,在方法区中他会被加载到一块叫做运行时常量池的地方。
二、主角运行时常量池 运行时常量池用于存放class文件中的常量池内容,也就是常量和方法以及字段信息,但这些都是编译期就有的内容,显然的,他的作用不仅于此。 并非只有编译期class文件中的常量池的内容才能入住运行时常量池,在运行时,同样也可以通过String类的intern方法,可以显式地将新的值放进常量池。
运行时常量池的好处: 1、节省内存,如字符串,可以无需频繁地新建内存或者销毁内存,直接在常量池共享一个字符串;
2、节省操作时间,通过==比较字符串是否相同效率显然是优于equals方法的,如果是常量池中同一个内容的引用,就无需进行equals比较了。
三、运行时常量池的作用 Java共有八种基础数据类型,都有各自的封装类,基本上都有对常量池的利用。 其中除了Float和Double,其他的六种,byte,short,int,long,boolean,char大多对常量池有利用。
在-128到127这个区间的数,会先使用常量池中的缓存内容,大于这个区间则需要新建一个对象,源码如下:
//Integer 缓存代码 :
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
再拿Integer做个例子: Integer i1 = 40;
Integer i2 = 40;
System.out.println(i1==i2);//输出TRUE
由于40<127,所以两者都是指向了常量池中的缓存,故,输出true。 注:如果显式地去new一个Integer对象,就会直接在堆中新建一个对象了。
以下为更为丰富的一个例子:
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
结果:i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true 总结:只要使用+或者显式地写一个等号赋值,默认的,都会去常量池中找内容。
四、String类和常量池的关系 String有两种创建方式,一种是直接等号赋值,一种是new String("***")。 两者还是有本质上的区别的。
第一种是在常量池中创建对象,一种是在堆中创建对象。
使用+也可以生成String对象,如果加号中有String对象的引用而不是引号文本,则生成的String对象不会被加入常量池,如果加号中全部是引号文本,生成的对象会被加入常量池。
也有特例:
1、特例1:
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
s等于t,它们是同一个对象
由于在编译期A和B就已经固定值了,s=A+B,效果等同于s="ab"+"cd"。 2、特例2:
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
s不等于t,它们不是同一个对象
由于A和B的值,并不是在编译期就固定。
说说创建了几个对象的问题:String s1 = new String("xyz"); **创建了几个对象? ** 考虑类加载阶段和实际执行时。 (1)类加载对一个类只会进行一次。"xyz"在类加载时就已经创建并驻留了(如果该类被加载之前已经有"xyz"字符串被驻留过则不需要重复创建用于驻留的"xyz"实例)。驻留的字符串是放在全局共享的字符串常量池中的。 (2)在这段代码后续被运行的时候,"xyz"字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1 持有。 这条语句创建了2个对象
Intern方法和更加丰富的String例子 运行期可以通过intern方法将String对象放入运行时常量池中,看以下的几个例子: public class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.println((hello == "Hello") + " ");
System.out.println((Other.hello == hello) + " ");
System.out.println((other.Other.hello == hello) + " ");
System.out.println((hello == ("Hel"+"lo")) + " ");
System.out.println((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
package other;
public class Other { public static String hello = "Hello"; }
结果:true true true true false true```
在同包同类下,引用自同一String对象.
在同包不同类下,引用自同一String对象.
在不同包不同类下,依然引用自同一String对象.
在编译成.class时能够识别为同一字符串的,自动优化成常量,引用自同一String对象.
在运行时创建的字符串具有独立的内存地址,所以不引用自同一String对象.
五、不同JDK版本下的intern方法 简言之,在JDK1.6中,当常量池中没有对象,intern方法会直接在常量池创建对象,并返回常量池的引用。 而在JDK1.7以后,intern如果在常量池中发现了字符串,还是和JDK1.6一样处理,不同的是,当常量池中没有字符串的时候,就不会再去把堆中的字符串拷贝到常量池中了,而是将堆的引用拷贝到常量池。
注:JDK1.6以前的常量池在方法区,而JDK1.6以后的常量池在堆上。
可以看这个链接:传送门
|