一. 相关概念
什么是常量
用 final 修饰的成员变量表示常量, 值一旦给定就无法改变!
final 修饰的变量有三种: 静态变量实例变量和局部变量, 分别表示三种类型的常量
Class 文件中的常量池
在 Class 文件结构中, 最头的 4 个字节用于存储魔数 Magic Number, 用于确定一个文件是否能被 JVM 接受, 再接着 4 个字节用于存储版本号, 前 2 个字节存储次版本号, 后 2 个存储主版本号, 再接着是用于存放常量的常量池, 由于常量的数量是不固定的, 所以常量池的入口放置一个 U2 类型的数据 (constant_pool_count) 存储常量池容量计数值
常量池主要用于存放两大类常量: 字面量 (Literal) 和符号引用量(Symbolic References), 字面量相当于 Java 语言层面常量的概念, 如文本字符串, 声明为 final 的常量值等, 符号引用则属于编译原理方面的概念, 包括了如下三种类型的常量:
类和接口的全限定名
字段名称和描述符
方法名称和描述符
方法区中的运行时常量池
运行时常量池是方法区的一部分
CLass 文件中除了有类的版本字段方法接口等描述信息外, 还有一项信息是常量池, 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后进入方法区的运行时常量池中存放
运行时常量池相对于 CLass 文件常量池的另外一个重要特征是具备动态性, Java 语言并不要求常量一定只有编译期才能产生, 也就是并非预置入 CLass 文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入池中, 这种特性被开发人员利用比较多的就是 String 类的 intern()方法
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能, 其实现了对象的共享
例如字符串常量池, 在编译阶段就把所有的字符串文字放到一个常量池中
(1)节省内存空间: 常量池中所有相同的字符串常量被合并, 只占用一个空间
(2)节省运行时间: 比较字符串时,== 比 equals()快对于两个引用变量, 只用 == 判断引用是否相等, 也就可以判断实际值是否相等
双等号 == 的含义
基本数据类型之间应用双等号, 比较的是他们的数值
复合数据类型 (类) 之间应用双等号, 比较的是他们在内存中的存放地址
二. 8 种基本类型的包装类和常量池
java 中基本类型的包装类的大部分都实现了常量池技术, 即 Byte,Short,Integer,Long,Character,Boolean;
- Integer i1 = 40;
- Integer i2 = 40;
- System.out.println(i1==i2);// 输出 TRUE
这 5 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据, 但是超出此范围仍然会去创建新的对象
- //Integer 缓存代码 :
- public static Integer valueOf(int i) {
- assert IntegerCache.high>= 127;
- if (i>= IntegerCache.low && i <= IntegerCache.high)
- return IntegerCache.cache[i + (-IntegerCache.low)];
- return new Integer(i);
- }
- Integer i1 = 400;
- Integer i2 = 400;
- System.out.println(i1==i2);// 输出 false
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术
- Double i1=1.2;
- Double i2=1.2;
- System.out.println(i1==i2);// 输出 false
应用常量池的场景
(1)Integer i1=40;Java 在编译的时候会直接将代码封装成
Integer i1=Integer.valueOf(40);
, 从而使用常量池中的对象
- (2)
- Integer i1 = new Integer(40);
这种情况下会创建新的对象
- Integer i1 = 40;
- Integer i2 = new Integer(40);
- System.out.println(i1==i2);// 输出 false
Integer 比较更丰富的一个例子
- 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));
- System.out.println("i1=i2+i3" + (i1 == i2 + i3));
- System.out.println("i1=i4" + (i1 == i4));
- System.out.println("i4=i5" + (i4 == i5));
- System.out.println("i4=i5+i6" + (i4 == i5 + i6));
- System.out.println("40=i5+i6" + (40 == i5 + i6));
- i1=i2 true
- i1=i2+i3 true
- i1=i4 false
- i4=i5 false
- i4=i5+i6 true
- 40=i5+i6 true
解释: 语句 i4 == i5 + i6, 因为 + 这个操作符不适用于 Integer 对象, 首先 i5 和 i6 进行自动拆箱操作, 进行数值相加, 即 i4 == 40 然后 Integer 对象无法与数值进行直接比较, 所以 i4 自动拆箱转为 int 值 40, 最终这条语句转为 40 == 40 进行数值比较
Java 中的自动装箱与拆箱
三. String 类和常量池
String 对象创建方式
- String str1 = "abcd";
- String str2 = new String("abcd");
- System.out.println(str1==str2);//false
这两种不同的创建方法是有差别的, 第一种方式是在常量池中拿对象, 第二种方式是直接在堆内存空间创建一个新的对象
只要使用 new 方法, 便需要创建新的对象
连接表达式 +
(1)只有使用引号包含文本的方式创建的 String 对象之间使用 + 连接产生的新对象才会被加入字符串池中
(2)对于所有包含 new 方式新建对象 (包括 null) 的 + 连接表达式, 它所产生的新对象都不会被加入字符串池中
- String str1 = "str";
- String str2 = "ing";
- String str3 = "str" + "ing";
- String str4 = str1 + str2;
- System.out.println(str3 == str4);//false
- String str5 = "string";
- System.out.println(str3 == str5);//true
特例 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, 它们是同一个对象");
- } else {
- System.out.println("s 不等于 t, 它们不是同一个对象");
- }
- }
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, 它们不是同一个对象");
- }
- }
s 不等于 t, 它们不是同一个对象
A 和 B 虽然被定义为常量, 但是它们都没有马上被赋值在运算出 s 的值之前, 他们何时被赋值, 以及被赋予什么样的值, 都是个变数因此 A 和 B 在被赋值之前, 性质类似于一个变量那么 s 就不能在编译期被确定, 而只能在运行时被创建了
String s1 = new String("xyz");
创建了几个对象?
考虑类加载阶段和实际执行时
(1)类加载对一个类只会进行一次 xyz 在类加载时就已经创建并驻留了 (如果该类被加载之前已经有 xyz 字符串被驻留过则不需要重复创建用于驻留的 xyz 实例) 驻留的字符串是放在全局共享的字符串常量池中的
(2)在这段代码后续被运行的时候, xyz 字面量对应的 String 实例已经固定了, 不会再被重复创建所以这段代码将常量池中的对象复制一份放到 heap 中, 并且把 heap 中的这个对象的引用交给 s1 持有
这条语句创建了 2 个对象
java.lang.String.intern()
运行时常量池相对于 CLass 文件常量池的另外一个重要特征是具备动态性, Java 语言并不要求常量一定只有编译期才能产生, 也就是并非预置入 CLass 文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入池中, 这种特性被开发人员利用比较多的就是 String 类的 intern()方法
String 的 intern()方法会查找在常量池中是否存在一份 equal 相等的字符串, 如果有则返回该字符串的引用, 如果没有则添加自己的字符串进入常量池
- public static void main(String[] args) {
- String s1 = new String("计算机");
- String s2 = s1.intern();
- String s3 = "计算机";
- System.out.println("s1 == s2?" + (s1 == s2));
- System.out.println("s3 == s2?" + (s3 == s2));
- }
- s1 == s2? false
- s3 == s2? true
字符串比较更丰富的一个例子
- public class Test {
- public static void main(String[] args) {
- String hello = "Hello", lo = "lo";
- System.out.println((hello == "Hello") + " ");
- System.out.println((Other.hello == hello) + " ");
- System.out.println((other.Other.hello == hello) + " ");
- System.out.println((hello == ("Hel"+"lo")) + " ");
- System.out.println((hello == ("Hel"+lo)) + " ");
- System.out.println(hello == ("Hel"+lo).intern());
- }
- }
- class Other { static String hello = "Hello"; }
- package other;
- public class Other { public static String hello = "Hello"; }
- true true true true false true
在同包同类下, 引用自同一 String 对象.
在同包不同类下, 引用自同一 String 对象.
在不同包不同类下, 依然引用自同一 String 对象.
在编译成. class 时能够识别为同一字符串的, 自动优化成常量, 引用自同一 String 对象.
在运行时创建的字符串具有独立的内存地址, 所以不引用自同一 String 对象.
来源: http://www.codeceo.com/java-const-conclude.html