常量池简单认识及相关问题

    技术2026-03-08  7

    常量池分类

    常量池大体可以分为以下两类:

    静态常量池 存在于class文件中 (可使用javap -verbose进行使用)运行时常量池 class文件被加载进内存之后,常量池保存在了方法区,就称为运行时常量池

    字符串常量池

    String a = "abc"; String b = new String("abc"); System.out.println(a == b); // false

    变量a的“abc”作为字面量开始存储在class文件中,在运行时转存至方法区。变量b是一个对象,对象存储在堆中。

    所以可以看到这两个字符串不是在同一个地方存储的。

    范例

    String s1 = "Hello"; String s2 = "Hello"; String s3 = "Hel" + "lo"; String s4 = "Hel" + new String("lo"); String s5 = new String("Hello"); String s6 = s5.intern(); String s7 = "H"; String s8 = "ello"; String s9 = s7 + s8; System.out.println(s1 == s2); // 1. true System.out.println(s1 == s3); // 2. true System.out.println(s1 == s4); // 3. false System.out.println(s1 == s9); // 4. false System.out.println(s4 == s5); // 5. false System.out.println(s1 == s6); // 6. true

    分析:

    1. s1 和 s2 都指向方法区常量池中的 "Hello"

    2. s3 做 + 的时候,会进行优化,自动生成 Hello 赋值给 s3

    3. s1 是常量池中的字符串,s4是存放对象的堆中的字符串,做 + 的时候会动态调用StringBuilder.append()进行字符串拼接,最后返回一个String对象

    4. s9 是 两个String类型的对象进行拼接,在JAVA9中,会使用StringBuilder进行拼接,返回一个新的String对象

    5. s4 和 s5是两个不同的对象,这里比较的是内存地址

    6. intern方法首先会在常量池中查找是否存在一份equal相等的字符串,如果有的话就返回该字符串的引用,没有的话就将它添加到字符串常量池中,所以存在于class中的常量池并非是固定不变的,也可以使用intern方法加入新的常量值

    注意: s5 和 s6是不相等的

    实例

    1. 常量拼接

    public class FinalTest { // 如果a b 是final类型 结果为true 反之为false public static final String a = "123"; public static final String b = "456"; public static void main(String[] args) { String c = "123456"; String d = a + b; System.out.println(c == d); } }

    对以上代码进行javac编译,并使用javap 进行反编译,编译结果如下:

    Compiled from "FinalTest.java" public class com.cqut.epidemic.dto.FinalTest { public static final java.lang.String a; public static final java.lang.String b; public com.cqut.epidemic.dto.FinalTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String 123456 将常量值从常量池中推送至栈 2: astore_1 // 将栈顶引用型数值存入第二个本地变量 3: ldc #2 // String 123456 将常量值从常量池中推送至栈 5: astore_2 // 将栈顶引用型数值存入第三个本地变量 6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 9: aload_1 // 将第二个引用类型本地变量推送至栈顶 10: aload_2 // 将第三个引用类型本地变量推送至栈顶 11: if_acmpne 18 // 比较栈顶两引用型数值,当结果不相等时跳转 14: iconst_1 // 将int型(1)推送至栈顶 15: goto 19 // 无条件跳转 18: iconst_0 // 将int型(0)推送至栈顶 19: invokevirtual #5 // Method java/io/PrintStream.println:(Z)V 22: return }

    运行结果 : true

    从编译结果中可以看出,final类型的常量已经在编译中被确认下来了,自动执行了+号,把它们拼接起来,并放入了常量池中,所以在main方法中比较的时候,就会直接从常量池中取出来进行比较。

    2. static静态代码块

    public class FinalTest { public static final String a; public static final String b; static { a = "123"; b = "456"; } public static void main(String[] args) { String c = "123456"; String d = a + b; System.out.println(c == d); } }

    编译结果如下:

    Compiled from "FinalTest.java" public class com.cqut.epidemic.dto.FinalTest { public static final java.lang.String a; public static final java.lang.String b; public com.cqut.epidemic.dto.FinalTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String 123456 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: getstatic #5 // Field a:Ljava/lang/String; 13: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 16: getstatic #7 // Field b:Ljava/lang/String; 19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: astore_2 26: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 29: aload_1 30: aload_2 31: if_acmpne 38 34: iconst_1 35: goto 39 38: iconst_0 39: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 42: return static {}; Code: 0: ldc #11 // String 123 2: putstatic #5 // Field a:Ljava/lang/String; 5: ldc #12 // String 456 7: putstatic #7 // Field b:Ljava/lang/String; 10: return }

    运行结果 : false

    从编译的代码可以看到,在方法中通过使用StringBuilder对a、b两个String类型进行append,最后调用StringBuilder.toString方法返回String类型对象,最后对结果进行比较。String类型对象是在堆中,而“123456”在方法区常量池中,所以两者不同。

    这样做的原因是因为,final类型常量在编译期间就已经确定了a和b的值,而在这段代码中,static在编译器是不执行的,所以a和b的值是未知的。并且static代码块在初始化的时候被执行,而初始化属于类加载的一部分,属于运行期。

    包装类的常量池技术

    JAVA对数值型数据有自动装箱和自动拆箱,自动装箱常见的就是valueOf(),自动拆箱是intValue()。

    如果直接将一个基本数据类型的值赋给Integer对象,则会发生自动装箱,其原理就是通过调用Integer类的valueOf(),将int类型的值包装到一个对象中。

    在这两个方法的源码中有实现一种特殊的缓存技术(除了Long和Double这两个包装类没有实现这个缓存技术)

    // 返回一个表示指定的int 值的 Integer实例 // 如果不需要新的 Integer 实例,则通常应优先使用该方法,而不是构造方法 Integer(int) // 因为该方法有可能通过缓存经常请求的值而显著提高空间和时间性能 public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }

    IntegerCache是Integer中的静态内部类,其中有个Cache[],这个就是Integer的常量池,常量池的大小为一个字节(-128 ~ 127) 

    注意: 这里的代码是在static块中执行的,也就是类初始化的时候执行的,所以这里说的常量池指的是运行时常量池

    简单示例

    Integer i1 = 40; Integer i2 = 40; Double i3 = 40.0; Double i4 = 40.0; System.out.println("i1=i2 " + (i1 == i2)); // true System.out.println("i3=i4 " + (i3 == i4)); // false

    解释 : 

    1. == 在不出现算数运算符的情况下是不会自动拆箱的,i1 和 i2 在赋值的时候都会调用 valueOf() ,所以 实际上两者是同一块内存地址,==比较的也是内存地址比较

    2. 因为Double类型没有缓存,所以这是两个不同的对象,地址是不同的

    复杂实例 1

    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)); // 1. true System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); // 2. true System.out.println("i1=i4 " + (i1 == i4)); // 3. false System.out.println("i4=i5 " + (i4 == i5)); // 4. false System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); // 5. true System.out.println("40=i5+i6 " + (40 == i5 + i6)); // 6. true

    注意点 : 当出现运算符的时候,Integer不能直接用来计算,所以会进行一次拆箱

    1. 此种情况和上面的情况一样

    2. 运算符自动拆箱成基本数值类型,这里是数值类型的比较

    3. i1 是缓存数组中的值,i4是堆中的对象,这里是内存地址的比较

    4. i4 和 i5 是两个不同的对象,其内存地址不同

    5. 运算符自动拆箱成基本数值类型

    6. 运算符自动拆箱成基本数值类型

    注意: equals() 比较的时候不会处理数据之间的转型,比如Double类型和Integer类型

    复杂示例2

        如果数值超过了 -128 ~ 127这个范围,所有的数值都将成为新对象

    Integer i1 = 400; Integer i2 = 400; Integer i3 = 0; Integer i4 = new Integer(400); Integer i5 = new Integer(400); Integer i6 = new Integer(0); Integer i7 = 1; Integer i8 = 2; Integer i9 = 3; System.out.println("i1=i2 " + (i1 == i2)); // 1. false System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); // 2. true System.out.println("i1=i4 " + (i1 == i4)); // 3. false System.out.println("i4=i5 " + (i4 == i5)); // 4. false System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); // 5. true System.out.println("400=i5+i6 " + (400 == i5 + i6)); // 6. true

    相关面试题

    提问下面的输出结果是多少?

    package test; public class Test { public static void main(String[] args) { Integer i1 = 127; Integer i2 = 127; System.err.println(i1 == i2); // true i1 = 128; i2 = 128; System.err.println(i1 == i2); // false } }

    解答: 

    JVM会自动维护八种基本类型的常量池,int常量池中初始化-128 ~ 127的范围,所以当为127的时候,在自动装箱过程中是取自常量池中的数值,而当128的时候,需要重新创建Integer对象,所以地址不同。

    提问:

    如果new出来两个值相等的Integer对象,且值范围在 -128 ~ 127之间,再通过 “==” 去比较会怎样?

    public class Test2 { public static void main(String[] args) { Integer i1 = 6; Integer i2 = 6; System.out.println((i1==i2)); //true Integer i3 = new Integer(6); Integer i4 = new Integer(6); Integer i5 = new Integer(128); System.out.println((6==i3)); // true System.out.println((128==i5)); // true System.out.println((i5==128)); // true System.out.println((i3==i4)+" "+i3.hashCode()+" "+i4.hashCode()); // false 6 6 } }

    结果说明:

    a. 当数值范围为 -128 ~ 127时,如果对对象直接赋值,为true;如果通过new进行赋值,则为false。因为直接赋值会触发valueOf(),此时就会使用IntegerCache

    b. 当数值不在这个范围时,无论通过哪种方式,通过 == 比较,其结果为false

    c. 当一个Integer对象直接与一个int基本数据类型通过 == 比较时,Integer类型会调用intValue()进行拆箱,此时就是基础数值的比较

    d. Integer对象的hash值为数值本身(查看源码: Integer重写了hashcode方法,返回值为value,即Integer对象的数值)

    提问: 为什么byte的范围是 -128 ~ 127?

    解答: 一个字节占 8 bit,由于计算机只能识别二进制,即0和1。所以规定第一位是符号位,1表示负数,0表示正数,涉及到补码的知识

     

    参考链接: 

    https://blog.csdn.net/qq_41376740/article/details/80338158

    https://blog.csdn.net/BeauXie/article/details/53013946?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

    Processed: 0.014, SQL: 9