Java常量池

  一.相关知识

  1.何为常量

  第一种常量:是一个值,我们将这个值本身称为常量。比如:

整型常量:1024
实型常量:1.024
字符常量:'g' 'c' 'w'
字符串常量:"gcw"
逻辑常量:true false

   例如,我们可以将数字1024称为一个int类型的常量。
  第二种常量:不可变的变量,我们也称为常量。被关键字final修饰的变量,其值就不可以改变。可能它本身是个变量,但是被final修饰后,我们就可以认为它是个常量。比如:

final int i=1024;

   2.常量池

  常量池分为两种:静态常量池和运行时常量池。
  (1)静态常量池也就是Class字节码文件中的常量池。我们举一个简单的例子,下面是一个HelloWorld的源文件和Class文件。
  源文件:

public class HelloWorld{
    public static void main(String args[]){
        System.out.println("hello world");
    }
}

   class文件:

  我们对class文件中的标识符予以一一分析。

  ①魔数
  魔数是class字节码文件中的前四个字节:ca fe ba be(漱壕)。它的唯一作用是确定这个文件是否可以被JVM接受。很多文件存储标准中都使用魔数来进行身份识别。
  ②版本号
  第5和第6个字节是次版本号,第7个和第8 个是主版本号。这里的第7和第8位是0034,即:0x0034。0x0034转为10进制是52。Java的版本是从45开始的然而从1.0 到1.1 是45.0到45.3, 之后就是1.2 对应46, 1.3 对应47 … 1.6 对应50,这里的1.6.0_24对应的是52,就是0x0034;
  ③常量池的入口
  由于常量池中的常量的数量不是固定的,所以常量池的入口需要放置一项u2类型的数据,代表常量池的容量计数值。这里的常量池容量计数值是从1开始的。如图常量池的容量:0x001d(29)。所以共有29个常量。
  ④常量池
  常量池中主要存放两类常量:字面量和符号引用。字面量是比较接近Java语言层面的常量概念,也就是我们提到的常量。符号引用则属于编译原理的方面的概念,包括三类常量:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符。

  (2)运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外还有一项信息是常量池,它用于存放编译期生成的字面量和符号应用,这部分内容将在类加载后进入方法区的时候存到运行时常量池中。运行时常量池还有个更重要的的特征:动态性。Java要求,编译期的常量池的内容可以进入运行时常量池,运行时产生的常量也可以放入池中。常用的是String类的intern()方法【当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。 】。

  3.常量池的好处
  常量池是为了避免频繁地创建和销毁对象而影响系统性能,其实现了对象的共享。
  例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
  (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  (2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

  4.equals和==的区别
   Java中的数据类型分两种:基本数据类型和引用数据类型。
  (1)基本数据类型共8种:byte short int long char float double boolean。
  对于基本数据类型的比较,都是用==来比较两者的值是不是相等。
  (2)引用数据类型。
  一般情况下,equals和==是一样的,都是比较的两者的地址值是不是一样。但是也有特殊情况,比如,我们都知道所有类都是继承自Object基类,Object中的equals方法中是使用==来实现的,即比较的是两者的地址值。但是,Object的子类可以重写equals方法,比如Date、String、Integer等类都是重写了equals()方法,比较的是值是否相等。例如,在String类的equals()源码中,先比较是不是指向同一个地址,如果不是再比较两者是不是值相等。这个时候,equals和==所表达的含义显然就不一样了。  

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = count;
            if (n == anotherString.count) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = offset;
                int j = anotherString.offset;
                while (n-- != 0) {
                    if (v1[i++] != v2[j++])
                        return false;
                }
                return true;
            }
        }
        return false;
    }

   二.8种基本数据类型

  8种基本数据类型都有自己的包装类,其中有6个包装类(Byte,Short,Integer,Long,Character,Boolean)实现了常量池技术。举例来说,通过查看Integer的源码会发现,它有个内部静态类IntegerCache,这个内部静态类进行了缓存,范围是[-128,127],只要是这个范围内的数字都会缓存到里面,从而做成常量池进行管理。我们来看一个实例:

package com.itszt.test5;
/**
 * Integer常量池
 */
public class IntegerTest {
    public static void main(String[] args) {
        Integer i1=10;
        Integer i2=10;//在[-128-127之间,存入常量池]
        System.out.println("i1 ==i2 ---> " + (i1==i2));

        Integer i3=1000;
        Integer i4=1000;//超出常量池范围,各自创建新的对象
        System.out.println("i3 ==i4 ---> " + (i3==i4));
    }
}

   控制台打印结果:

i1 ==i2 ---> true
i3 ==i4 ---> false

   上述代码中,第一次把i1的值缓存进去了,当创建i2的时候,它其实是指向了第一次缓存进去的那个10,所以i1和i2指向了同一个地址;由于i3和i4均超出了常量池范围,故在堆内存中重新创建了两个对象,它们在堆内存中的地址不相等。

  如果使用new关键字,意味着在堆内存中开辟了新的一块内存孔家。每次new一个对象都是在堆内存中开辟一块新的空间,所以每一个new出的对象的地址都不一样。

  Float和Double没有实现常量池。代码演示如下:

        Float f1=10.0f;
        Float f2=10.0f;
        System.out.println("f1 =f2 ---> " + (f1==f2));

        Double d1=12.0;//默认为double类型
        Double d2=12.0d;
        System.out.println("d1 =d2 ---> " + (d1==d2));    

   上述代码在main()主函数中执行后,控制台打印如下:

f1 = f2 ---> false
d1 =d2 ---> false

   三.String类

  1.new String都是在堆上创建字符串对象。

   

  2.当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。

  

  3.通过字面量赋值创建字符串(如:String str=”twm”)时,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。

  4.常量字符串的“+”操作,编译阶段直接会合成为一个字符串。如string str=”JA”+”VA”,在编译阶段会直接合并成语句String str=”JAVA”,于是会去常量池中查找是否存在”JAVA”,从而进行创建或引用。

  5.对于final字段,编译期直接进行了常量替换(而对于非final字段则是在运行期进行赋值处理的)。
  final String str1=”ja”;
  final String str2=”va”;
  String str3=str1+str2;
  在编译时,直接替换成了String str3=”ja”+”va”,然后再次替换成String str3=”JAVA”。

  6.常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用stringBuilder.append()在堆内存上创建新的对象。

  7.在JDK 1.7后,intern()方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池中找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。

  

  四.常见试题解答:

Q:下列程序的输出结果:
String s1= “abc”;
String s2= “abc”;
System.out.println(s1==s2);
A:true,均指向常量池中对象。

Q:下列程序的输出结果:
String s1=new String(“abc”);
String s2=new String(“abc”);
System.out.println(s1==s2);
A:false,两个引用指向堆中的不同对象。

Q:下列程序的输出结果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因为s2+s3实际上是使用StringBuilder.append来完成,
        会生成不同的对象。

Q:下列程序的输出结果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因为final变量在编译后会直接替换成对应的值,
    所以实际上等于s4=”a”+”bc”,而这种情况下,
    编译器会直接合并为s4=”abc”,所以最终s1==s4。

Q:下列程序的输出结果:
String s = new String(“abc”);
String s1 = “abc”;
String s2 = new String(“abc”);
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false,false,true。 
posted @ 2018-04-22 20:39  奔跑在梦想的道路上  阅读(2445)  评论(0编辑  收藏  举报