【Java学习笔记系列】Java常量池、常量池的应用以及装拆箱特性总结

Java常量池以及装箱&拆箱特性的总结


这里我们来总结一下Java常量池的一些基本知识以及深入解析JDK5引入的装箱&拆箱特性。首先我们要了解什么是常量池,常量池的应用,再去深入分析装箱&拆箱特性

Java常量池


什么是常量:
常量可分为两种:

  • 字面常量(也称为字面量,直接量,直接常量)
    (字面常量是指在程序中无需预先定义就可使用的数字、字符、boolen值、字符串等。简单的说,就是确定值的本身。如 10,2L,2.3f,3.5,“hello”,’a’,true、false、null 等等。)
  • 符号常量
    (符号常量是指在程序中用标识符预先定义的,其值在程序中不可改变的量。如 final int a = 5;)

既final int a = 10; a就是符号常量,10就是字面常量。在编译期间,在表达式中的符号常量会被编译器优化,直接使用其的
替换掉,如

//源码
final int a = 5;
int c = a + 5
//编译后的代码
final int a = 5;
int c = 10;

符号常量分类:
符号常量可以分为两种:、

  • static符号常量 (static final int a,要使用可能引用类的加载,毕竟不需要通过对象来调用)
  • 非static符号常量 (final int a ,要使用必然引用类的加载,因为必须通过对象来调用)

根据编译器的不同,static符号常量又可以分为两种:

  • 编译时常量
    (public static final int a = 10就是一个编译时常量,在编译后的符号中找不到a,所有对a的引用都被替换成了20。在编译时就可以确定值,使用它不会引用所属类的加载)
  • 运行时常量
    (public static final int b = “hello”.length()就是一个运行时常量,因为”hello”.length()的值不是一个编译期间能确定的值,需要在运行期间才能确定。它的值要被确定必然会引用类的加载)

既常量被赋予的值是一个确定的值时,就会在编译期间被优化。如果是一个编译期间无法确定的值,需要在运行期间加载类来获取该值时,这种常量则是运行期的常量。

什么是常量池:
常量,我们都了解了。那什么是一个常量池呢?我们可以通俗的理解是用于存放常量信息的地方。

常量池的分类:
Java中的常量池也分三种,就如《深入理解Java虚拟机》所说:

  • 静态常量池 (Class文件常量池,Constant Pool Table)
  • 运行时常量池 (动态常量池,Runtime Constant Pool)
  • 字符串常量池 (全局的字符串常量池,String Pool)

静态常量池:
我们知道Java源代码被编译器编译后,会产生很多的Class文件,而Class文件中有一块用于存放编译后产生的字面量和符号引用的地方。如下代码块,这就是一个静态常量池,也可以如书中所说叫Class文件常量池

Constant pool:
   #1 = Methodref          #4.#16         //  java/lang/Object."<init>":()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          //  "<init>":()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包装类型在常量池的应用


java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。当然还有没有基本类型的String字符串类型也都应用了常量池技术。我们大致可以分为4类

  • Byte,Short,Integer,Long,Character
    存在范围限制,既字面量值处于-128~127期间才使用常量池技术(具体类型的范围可以存在稍许不同),不在这个范围内不适用
  • Boolean
    Boolean常量池只存在两个对象,true和false
  • String
    不存在范围限制
  • Double,Float
    浮点数没有应用常量池技术
Byte,Short,Integer,Long,Character类型测试

存在范围限制

//测试代码
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);
}

我们可以分别看到:

  • Character的参数只要小于等于127,就返回常量池中的对象引用,如果大于127则返回一个新实例化的对象
  • Integer的参数则是判断是否在-128~127区间内,是则返回常量池中的对象引用,不是而返回一个新实例化的对象
Boolean类型测试
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类型对常量池的应用与上面讨论的类型几乎一致,只是应用范围稍有不同。我们来看一下BooleanvalueOf方法

 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类型不属于谁的包装类,它也没有对应的基本类型。它就是一个字符串类型。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;

这是拆箱还是装箱?其实也很简单,对象是不能用来相加减的。所以必须是先拆箱成基本类型,运算完之后的得到基本类型的计算结果,在装箱成包装类


参考资料:

Java常量池理解和经典总结
java常量池概念,String,Integer等包装类对常量池的应用
java基础(八) 深入解析常量池与装拆箱机制
Java中几种常量池的区分
JVM-String常量池与运行时常量池
javap -c命令详解
深入剖析Java中的装箱和拆箱
Java8内存模型—永久代(PermGen)和元空间(Metaspace)

在此感谢参考过的网站、博客的作者,非常感谢!!

阅读更多
版权声明:本文为博主原创文章,转载须注明原创链接,标注原创作者名:SnailMann https://blog.csdn.net/SnailMann/article/details/80318324
个人分类: Java
上一篇【Java虚拟机学习笔记】《深入理解Java虚拟机》之第二章 - Java内存模型以及内存溢出异常
下一篇【Java学习笔记系列】深入学习数组与相关知识点总结
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭