这里我们来总结一下Java常量池的一些基本知识以及深入解析JDK5引入的装箱&拆箱特性。首先我们要了解什么是常量池,常量池的应用,再去深入分析装箱&拆箱特性
什么是常量:
常量可分为两种:
既final int a = 10; a就是符号常量,10就是字面常量。在编译期间,在表达式中的符号常量会被编译器优化,直接使用其的
值替换掉,如
//源码
final int a = 5;
int c = a + 5
//编译后的代码
final int a = 5;
int c = 10;
符号常量分类:
符号常量可以分为两种:、
根据编译器的不同,static符号常量又可以分为两种:
既常量被赋予的值是一个确定的值时,就会在编译期间被优化。如果是一个编译期间无法确定的值,需要在运行期间加载类来获取该值时,这种常量则是运行期的常量。
什么是常量池:
常量,我们都了解了。那什么是一个常量池呢?我们可以通俗的理解是用于存放常量信息的地方。
常量池的分类:
Java中的常量池也分三种,就如《深入理解Java虚拟机》所说:
静态常量池:
我们知道Java
源代码被编译器编译后,会产生很多的Class
文件,而Class
文件中有一块用于存放编译后产生的字面量和符号引用的地方。如下代码块,这就是一个静态常量池,也可以如书中所说叫Class文件常量池
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."":()V
#2 = Methodref #3.#17 // X.bar:()V
#3 = Class #18 // X
#4 = Class #19 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 LX;
#12 = Utf8 foo
#13 = Utf8 bar
#14 = Utf8 SourceFile
#15 = Utf8 X.java
#16 = NameAndType #5:#6 // "":()V
#17 = NameAndType #13:#6 // bar:()V
#18 = Utf8 X
#19 = Utf8 java/lang/Object
(代码出处 - R大解释符号引用和直接引用时的Class文件代码)
运行时常量池:
而运行时常量池呢?运行时常量池是虚拟机内存方法区的一部分(jdk1.6)。用于存放静态常量池内的字面量,符号引用和翻译后的直接引用等,还有存放运行期间产生的一些常量(Stirng.intern()
等)。所以运行时常量池是每个类都有一个。
字符串常量池
字符常量池不同于运行时常量池,是全局享有的,意识是所有类的字符串常量的引号都存放在这里。过程是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例(jdk.1.7),然后将该字符串对象的地址存到字符串常量池中(字符串常量池中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。
在HotSpot VM
里实现的字符串常量池功能的是一个StringTable
类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable
在每个HotSpot VM
的实例只有一份,被所有的类共享。
静态常量池和运行时常量池的关系:
静态常量池(Constant Pool Table)用于存放编译期间生成的各种字面量和符号引用,然后在类加载的时候被放进运行时常量池(Runtime Constant Pool)中存储。
注意:
jdk1.6运行时常量池和字符串常量池在永久代中,1.7常量池都移入了堆,1.8字符串常量池依然在堆,运行时常量池听说移入元空间。
java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。当然还有没有基本类型的String字符串类型也都应用了常量池技术。我们大致可以分为4类
存在范围限制
//测试代码
public static void main(String[] args) {
//byte类型本身的范围就是-128~127之间,其实可以说是无范围限制
Byte b1 = 1;
Byte b2 = 1;
Byte b3 = new Byte((byte) 1);
System.out.println(b1 == b2); //true
System.out.println(b1 == b3); //false
Character c1 = 'a';
Character c2 = 'a';
Character c3 = new Character('a');
System.out.println(c1 == c2); //true
System.out.println(c1 == c3); //false
//我们重点说Integer类型
Integer i1 = 127;
Integer i2 = 127;
Integer i3 = new Integer(127);
System.out.println(i1 == i2); //true
System.out.println(i1 == i3); //false
Integer i4 = 128;
Integer i5 = 128;
System.out.println(i4 == i5); //false
}
由上我们可以看到,当包装类型Byte
,Character
,Integer
的不同变量被直接赋予相同字面量时(排除范围的情况),我们用==去比较同类型的两个变量的引用是否相等,返回的是true,但去跟new处理的对象比较时,返回的是false。所以我们可以知道当包装类型变量被直接赋于相同字面量时,它们所指向的地址是相同的。
但当字面量值不在包装类型特定范围内时,返回的会是false。我们来看下面两段代码
源代码:
Byte b1 = 1;
Byte b2 = 1;
Character c1 = 'a';
Character c2 = 'a';
Integer i1 = 127;
Integer i2 = 127;
编译后生成的代码:
Byte b1 = Byte.valueOf();
Byte b2 = Byte.valueOf((byte)1);
Character c1 = Character.valueOf('a');
Character c2 = Character.valueOf('a');
Integer i1 = Integer.valueOf(127);
Integer i2 = Integer.valueOf(127);
我们可以看到所有直接赋值字面量的操作在编译之后,实际都是通过包装类型的valueOf
方法去初始化的,然后我们再看一下valueOf
方法的源码
//Character类型的valueOf方法
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
//Integer类型的valueOf方法
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) //low is -128 ,high = 127
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
我们可以分别看到:
127
,就返回常量池中的对象引用,如果大于127
则返回一个新实例化的对象-128~127
区间内,是则返回常量池中的对象引用,不是而返回一个新实例化的对象public static void main(String[] args) {
Boolean a = true;
Boolean b = true;
Boolean c = new Boolean(true);
System.out.println(a == b); //true
System.out.println(a == c); //false
}
Boolean
类型对常量池的应用与上面讨论的类型几乎一致,只是应用范围稍有不同。我们来看一下Boolean
的valueOf
方法
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
可以看出,Boolean
类型不存在限制范围,只要是字面量赋值,通过valueOf
方法去初始化的,都只返回常量池中的两个对象的引用的其中一个(TRUE
常量和FALSE
常量)。TRUE
常量和FALSE
常量的初始化在加载Boolean类时进行,整个程序运行期间,只被加载一次。
String类与上面的类型有些不同,String类型不属于谁的包装类,它也没有对应的基本类型。它就是一个字符串类型。String的字面量用"“来表示。”"的本身就代表着一个空字符串的对象。("".toString()
)
测试代码:
public class StringPool {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1 == s2); //true
System.out.println(s1 == s3); //false
}
}
String的valueOf方法:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
由上,我们可以看出String类型也应用了常量池技术,但是String字符串类型与其他的包装类型的应用稍有不同。"abc"不是一个基本类型数据,而是一个对象,是一个存储在String常量池中驻留字符串对象。测试代码如下
public class Demo {
String str1 = "abc";
String str2 = new String("abc");
String str3 = new String("123");
}
命令行输入javap -verbose Demo.cass
查看Class字节码代码如图:
以上代码的加载流程可以分为两个步骤
首先这段代码被编译之后,"abc"
,"123"
的信息会被生成在Class
文件的静态常量池中。在程序运行期间,当Demo
类被JVM加载时,静态常量池的"abc"
,"123"
对象的值会被String常量池所遍历寻找(可能别的类加载时也有"abc"
或"123"
,已经创建过了),如果已经含有值为"abc"
,"123"
的对象则什么都不做,直接返回字符串常量池中对象的引用。如果不存在,则生成值分别为"abc"
和"123"
的对象并在String常量池中存储,再返回对象的引用。
因为有遇到了new
关键字,所以会拷贝String
常量池中值为"123"
,"abc"
的对象到堆中,相当于在堆中生成一个一模一样的新对象,并返回堆中新对象的地址。
"abc"
,"123"
这些对象称为驻留字符串,有些题目会问String str = new (“123”)这句话创建了几个对象,这是要看具体情况具体分析,有可能一个(堆),也有可能两个(常量池和堆)
注意:
以上的说法为了更容易理解,都是简单的描述。实质上不同的虚拟机或是同样的虚拟机不同的版本的实现都有些许不同,比如在jdk1.7之后,字符串常量池已经移出了方法区,且字符串常量池中实质存储的只是对象的引用,“abc”,"123"字符串对象的实例是存储在堆中。移入堆中。上面测试中的说法是将字符串常量存储的引用和堆的实例对象合并成一个常量池的说法。
不过不要混淆的是,该创建两个对象还是两个,因为如果是new的情况下,遍历字符串常量池没有找到对应的对象,会在堆中生成该对象,字符串常量池存储该对象的引用。再copy这个对象到堆里生成一个一模一样的新对象,返回这个新对象在堆中的地址,就是在堆中会有两个对象生成。
Java中几种常量池的区分
装箱&拆箱特性是JDK5的时候引入的特性,目的是简易开发步骤。我们这里就来讲一下什么是装箱?什么又是拆箱?这个机制是怎么去实现的?
什么是装箱&拆箱?这个机制是怎么实现的?
Java为8种基本数据类型都提供了对应的包装器类型,在jdk5
之前,包装类型和基本类型是不能直接赋值的,因为包装类型的本质就是一个类,这个过程就相当将一个基本类型的数据赋值给一个对象,这是有问题的,因为基本类型和对象是不对等的,正确的方式应该是将基本类型值赋值给对象中的某个变量。但是为了更加的简洁,简易开发步骤,所以从jdk5
开始,可以直接将基本类型的数据赋值给对应的包装类对象。
简单点讲,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
测试代码:
public class Box {
public static void main(String[] args) {
int a = 10; //正常赋值
Integer i = 10; //装箱,将基本类型数据赋值给包装类对象
int n = i; //拆箱,将包装类型对象赋值给基本类型变量
}
}
反编译的代码:
public class Box
{
public static void main(String[] args)
{
int a = 10;
Integer i = Integer.valueOf(10);
int n = i.intValue();
}
}
我们可以看到,整个装箱和拆箱的过程是由编译器帮我们实现的。
装箱&拆箱特性实现原理:
装箱过程是由包装类的valueOf()方法去实现
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
拆箱过程是由包装类的xxxValue()方法实现的
private final int value; //实际Integer的值就存储在value变量中,private,只允许内部访问
public int intValue() {
return value;
}
其他的包装类型也类似,大家可以去试试,下面我举个可能会出现困惑的例子
Integer x = 1;
Integer y = 2;
Integer z = x+y;
这是拆箱还是装箱?其实也很简单,对象是不能用来相加减的。所以必须是先拆箱成基本类型,运算完之后的得到基本类型的计算结果,在装箱成包装类
public static void main(String[] args) {
String a = "a";
String b = "b";
String c = "ab";
System.out.println(("a" + "b") == c); // true
System.out.println((a + b) == c); // false
}
"a" + "b"
在编译时会被直接优化成"ab"
, 所以它的地址是跟变量c相等的,因为地址都在字符串常量池中a + b
的形式,那么就不同了,编译器的优化,是通过StringBuilder.append
实现的,所以就相当于a.append(b).toString()
, 最终会new出一个新对象,所以就不是常量池中的引用了,自然是就是false 0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
12: ldc #4 // String ab
14: aload_3
15: if_acmpne 22
18: iconst_1
19: goto 23
22: iconst_0
23: invokevirtual #6 // Method java/io/PrintStream.println:(Z)V
26: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
29: new #7 // class java/lang/StringBuilder
32: dup
33: invokespecial #8 // Method java/lang/StringBuilder."":()V
36: aload_1
37: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
40: aload_2
41: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
44: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
47: aload_3
48: if_acmpne 55
51: iconst_1
52: goto 56
55: iconst_0
56: invokevirtual #6 // Method java/io/PrintStream.println:(Z)V
59: return
在此感谢参考过的网站、博客的作者,非常感谢!!