final修饰的变量就是常量?final修饰局部变量在栈还是堆还是常量池中?

7,814 阅读8分钟

概念

什么是常量?
对于这个问题,可能很多人都可以脱口而出 : 用final修饰的变量是常量 ,或者是在编译时期定义好的字符串。(字符串常量)

但是这种说法是不严谨的,因为准确来说 : 常量是用final修饰的成员变量!常量在类编译时期载入类的常量池中。

即final修饰的成员变量(实例变量)和静态变量(静态变量也只能是用static修饰的成员变量),那么用final修饰的局部变量(方法内)我们也可以称之为不可变变量。(存储在栈中)

常量池

Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。

  • 静态常量池 : *.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。(编译时期)

  • 运行时常量池 : jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。(运行时期)
    补充 : 运行时常量池中的常量,基本来源于各个class文件中的常量池。(即每个class文件都有对应的常量池)

常量池的好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。

(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等

双等号==的含义
基本数据类型之间应用双等号,比较的是他们的数值。
复合数据类型(类)之间应用双等号,比较的是他们在内存中的存放地址。(引用地址)

        String hello="helloMoto";   
        String hello2="helloMoto";  

例如我们定义hello和hello2,并且字符串常量池中没有存在”helloMoto”这个字符串常量。
那么首先会在字符串常量池中创建”helloMoto”字符串对象,hello指向字符串常量池中”helloMoto”字符串对象。
第一行代码,hello2首先会去常量池中寻找是否有”helloMoto”,发现已经存在,就直接指向该字符串常量池中”helloMoto”字符串对象。(String对象探索)

Class类文件中的常量池

  • 魔数 : 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来表示身份识别。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意的改动。Class文件的魔数有很浪漫的气息,值为0x CAFEBABE这也是java是咖啡图标和商标名的原因之一。

  • 版本号 : 紧接着4个魔数字节后面存储的是Class文件的版本号:第5和6个字节是次版本号,第7和第8个字节是主版本号。

  • 常量池 : 接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。(Class类文件中的常量池在类未加载到内存中可以称为静态常量池) 。入口处用2个字节标识常量池常量数量。



我们使用十六进制编辑器WinHex打开Class文件

public class Test2 {

    public static void main(String[] args) {

        String hello="helloMoto";   
    }

}

这里写图片描述

常量池中存放了各种类型的常量,他们都有自己的类型,并且都有自己的存储规范,本文只关注字符串常量,字符串常量以01开头(1个字节),接着用2个字节记录字符串长度,然后就是字符串实际内容。


这里写图片描述

常量池

常量池主要用于存放两大类常量: 字面量和符号引用量

字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值(成员变量)等。

符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名

  • 字段名称和描述符

  • 方法名称和描述符

运行时常量池

在Class类文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用如不过不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。 这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于CLass文件常量池(静态常量池)的另外一个重要特征是 具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

包装类常量池(对象池)

java中基本类型的包装类的大部分都实现了常量池技术,
Byte,Short,Integer,Long,Character,BooleanFloat,Double

          Integer i1 = 127;
          Integer i2 = 127;
          System.out.println(i1==i2);//true


          Integer i3 = 128;
          Integer i4 = 128;
          System.out.println(i3==i4);//false

对于上面2段代码不同结果我们可以追溯Integer源码

//Integer

    public static Integer valueOf(int i) {
        if (i >= -128 && i <= 127)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

可以看到如果值位于[-128,127]区域中,会使用IntegerCache类缓存数据,类似于字符串常量池。
所以如果赋的值超出这个区域, 便会创建一个新的Integer对象。(好处是平时如果频繁的使用Integer,并且数值在[-128,127]中,便不会重复创建新的Integer对象)



但是DoubleFloat这两个基本数据类型的包装类就没有对应常量池(对象池)的实现。

//Double


    public static Double valueOf(double d) {
        return new Double(d);
    }

Java中装箱和拆箱

基本数据类型 包装类
int(4字节) Integer
byte(1字节) Byte
short(2字节) Short
long(8字节)) Long
float(4字节) Float
double(8字节) Double
char(2字节) Character
boolean(1字节) Boolean

赋值时

  • 装箱
    如果要生成一个数值为10的Integer对象,只需要这样:
Integer i = 10;

这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。

  • 拆箱
Integer i = 10;  //装箱
int n = i;   //拆箱

简单一点说,装箱就是 自动将基本数据类型转换为包装器类型;拆箱就是 自动将包装器类型转换为基本数据类型。

方法调用时

public class Test2 {

    public static void main(String[] args) {

        int result = print(5);//int值 5 转换成对应的Integer对象(装箱)
    }

    private static int print(Integer a) {//接收Integer对象作为参数
        System.out.println("a==" + a);
        return a;//返回int 类型,Integer自动拆箱转为int类型。
    }
}

//a==5

方法运算时

public class Test2 {

    public static void main(String[] args) {

        Integer sum = 0;
        for (int i = 1000; i < 5000; i++) {
        //自动拆箱为int类型才能运算
        //运算结果再自动装箱为Integer类型
            sum += i;
        }
    }

}

上面的代码sum+=i可以看成sum = sum + i,但是+这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下

int result = sum.intValue() + i;
Integer sum = new Integer(result);

由于我们这里声明的sum为Integer类型,在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。

参考

梦工厂的简书
深入剖析Java中的装箱和拆箱
技术小黑屋