聊聊Java的常量池

什么是常量和常量值

常量是指在程序的整个运行过程中值保持不变的量。在这里要注意常量和常量值是不同的概念,常量值是常量的具体和直观的表现形式,常量是形式化的表现。通常在程序中既可以直接使用常量值,也可以使用常量。

通常也有把常量值和常量统称为字面量

常量值

常量值又称为字面常量,它是通过数据直接表示的,因此有很多种数据类型,像整型和字符串型等。下面一一介绍这些常量值。

整型常量值

Java 的整型常量值主要有如下 3 种形式。

  • 十进制数形式:如 54、-67、0。
  • 八进制数形式:Java 中的八进制常数的表示以 0 开头,如 0125 表示十进制数 85,-013 表示十进制数 11。
  • 十六进制数形式:Java 中的十六进制常数的表示以 0x 或 0X 开头,如 0x100 表示十进制数 256,-0x16 表示十进制数 -22。

整型(int)常量默认在内存中占 32 位,是具有整数类型的值,当运算过程中所需值超过 32 位长度时,可以把它表示为长整型(long)数值。长整型类型则要在数字后面加 L 或 1, 如 697L,表示一个长整型数,它在内存中占 64 位。

实型常量值

Java 的实型常量值主要有如下两种形式。

  • 十进制数形式:由数字和小数点组成,且必须有小数点,如 12.34、-98.0。
  • 科学记数法形式:如 1.75e5 或 32&E3,其中 e 或 E 之前必须有数字,且 e 或 E 之后的数字必须为整数。

Java 实型常量默认在内存中占 64 位,是具有双精度型(double)的值。如果考虑到需要节省运行时的系统资源,而运算时的数据值取值范围并不大且运算精度要求不太高的情况,可以把它表示为单精度型(float)的数值。

单精度型数值一般要在该常数后面加 F 或 f,如 69.7f,表示一个 float 型实数,它在内存中占 32 位(取决于系统的版本高低)。

布尔型常量值

Java 的布尔型常量只有两个值,即 false(假)和 true(真)。

字符型和字符串常量值

Java 的字符型常量值是用单引号引起来的一个字符,如 ‘e’、E’。需要注意的是,Java 字符串常量值中的单引号和双引号不可混用。双引号用来表示字符串,像 “11”、“d” 等都是表示单个字符的字符串。

除了以上所述形式的字符常量值之外,Java 还允许使用一种特殊形式的字符常量值来表示一些难以用一般字符表示的字符,这种特殊形式的字符是以开头的字符序列,称为转义字符。

常量

常量不同于常量值,它可以在程序中用符号来代替常量值使用,因此在使用前必须先定义。

Java 语言使用 final 关键字来定义一个常量,其语法如下所示:

final dataType variableName

其中,final 是定义常量的关键字,dataType 指明常量的数据类型,variableName 是变量的名称。

例如,以下语句使用 final 关键字声明常量。

final int COUNT = 10;
final float HEIGHT = 10.2f;

在定义常量时,需要注意如下内容:

  • 在定义常量时就需要对该常量进行初始化。
  • final 关键字不仅可以用来修饰基本数据类型的常量,还可以用来修饰对象的引用或者方法。
  • 为了与变量区别,常量取名一般都用大写字符。

当常量被设定后,一般情况下不允许再进行更改,如果更改其值将提示错误。例如,以下语句定义常量 AGE 并赋予初值,如果更改 AGE 的值,那么在编译时将提示错误。

final int AGE = 10;
AGE = 11;

常量池

常量池在Java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是JVM的一块特殊的内存空间。

常量池形态

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

静态常量池

所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

运行时常量池

而运行时常量池,则是JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

常量池的好处

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

  • 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  • 节省运行时间:比较字符串时,== 比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

常量池的位置

这个问题的回答众说纷纭,个人觉得一个比较靠谱的回答:

  • Java8之前,常量池是存放在堆中的,常量池就相当于是在永久代中,所以永久代存放在堆中。
  • Java8之后,取消了整个永久代区域,取而代之的是元空间。常量池就不存放在堆中了,而是存放在方法区里面,与堆栈是并列关系。永久代也就不存放在堆中了。

原文:JVM中常量池存放在哪里

通过例子了解常量池

网络上流行的常量池例子

@Test
public void constantPoolTest() {
    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);  // true
    System.out.println(s1 == s3);  // true
    System.out.println(s1 == s4);  // false
    System.out.println(s1 == s9);  // false
    System.out.println(s4 == s5);  // false
    System.out.println(s1 == s6);  // true
}

首先说明一点,在Java 中对象直接使用==操作符,比较的是两个对象的引用地址,并不是比较内容,比较内容请用equals()。

  • s1 == s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。
  • s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = “Hel” + "lo"在class文件中被优化成String s3 = “Hello”,所以s1 == s3成立。
  • s1 == s4当然不相等,s4虽然也是拼接出来的,但new String(“lo”)这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同。
  • s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,所以不做优化,等到运行时,s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。
  • s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。
  • s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。

特例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,它们是同一个对象"); // print   
    } else {   
        System.out.println("s不等于t,它们不是同一个对象");   
    }   
} 

A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。
也就是说:String s = A + B; 等同于:String s = “ab” + “cd”;

特例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,它们不是同一个对象"); // print   
    }   
} 

A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。

至此,我们可以得出三个非常重要的结论:

  • 必须要关注编译期的行为,才能更好的理解常量池。
  • 运行时常量池中的常量,基本来源于各个class文件中的常量池。
  • 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则JVM不会自动添加常量到常量池。

以上所讲仅涉及字符串常量池,实际上还有整型常量池、浮点型常量池。Java中包装类的大部分都实现了常量池技术,如Byte,Short,Integer,Long,Character,Boolean。

你可能感兴趣:

参考:

猜你喜欢

转载自blog.csdn.net/lrh329678260/article/details/85315553