java中的常量池

JVM 的运行时数据区主要包括:堆、栈、方法区、程序计数器等。

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码

更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分

常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。方法引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

常量池有什么用 ?

优点:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

举个栗子: Integer 常量池(缓存池),和字符串常量池

Integer常量池:

我们知道 == 基本数据类型比较的是数值,而引用数据类型比较的是内存地址

public void TestIntegerCache()
{
    public static void main(String[] args)
    {
        
        Integer i1 = new Integer(66);
        Integer i2 = new integer(66);
        Integer i3 = 66;
        Integer i4 = 66;
        Integer i5 = 150;
        Integer i6 = 150;
        System.out.println(i1 == i2);//false
        System.out.println(i3 == i4);//true
        System.out.println(i5 == i6);//false
    }
    
}

i1 和 i2 使用 new 关键字,每 new 一次都会在堆上创建一个对象,所以 i1 == i2 为 false。

i3 == i4 为什么是 true 呢?Integer i3 = 66 实际上有一步装箱的操作,即将 int 型的 66 装箱成 Integer,通过 Integer 的 valueOf 方法。

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

Integer 的 valueOf 方法很简单,它判断变量是否在 IntegerCache 的最小值(-128)和最大值(127)之间,如果在,则返回常量池中的内容,否则 new 一个 Integer 对象。

而 IntegerCache 是 Integer的静态内部类,作用就是将 [-128,127] 之间的数“缓存”在 IntegerCache 类的 cache 数组中,valueOf 方法就是调用常量池的 cache 数组,不过是将 i3、i4 变量引用指向常量池中,没有真正的创建对象。而new Integer(i)则是直接在堆中创建对象。

IntegerCache 类中,包含一个构造方法,三个静态变量:low最小值、high最大值、和Integer数组,还有一个静态代码块。静态代码块的作用就是在 IntegerCache 类加载的时候,对high最大值以及 Integer 数组初始化。也就是说当 IntegerCache 类加载的时候,最大最小值,和 Integer 数组就已经初始化好了。这个 Integer 数组其实就是包含了 -128到127之间的所有值。

IntegerCache 源码

private static class IntegerCache {
        static final int low = -128;//最小值
        static final int high;//最大值
        static final Integer cache[];//缓存数组
 
        //私有化构造方法,不让别人创建它。单例模式的思想
        private IntegerCache() {}
 
        //类加载的时候,执行静态代码块。作用是将-128到127之间的数缓冲在cache[]数组中
        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
 
            cache = new Integer[(high - low) + 1];//初始化cache数组,根据最大最小值确定
            int j = low;
            for(int k = 0; k < cache.length; k++)//遍历将数据放入cache数组中
                cache[k] = new Integer(j++);
 
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
 
    }

而 i5 == i6 为 false,就是因为 150 不在 Integer 常量池的最大最小值之间【-128,127】,从而 new 了一个对象,所以为 false。

再看一段拆箱的代码。

public static void main(String[] args){
       Integer i1 = new Integer(4);
       Integer i2 = new Integer(6);
       Integer i3 = new Integer(10);
       System.out.print(i3 == i1+i2);//true
    }

由于 i1 和 i2 是 Integer 对象,是不能使用+运算符的。首先 i1 和 i2 进行自动拆箱操作,拆箱成int后再进行数值加法运算。i3 也是拆箱后再与之比较数值是否相等的。所以 i3 == i1+i2 其实是比较的 int 型数值是否相等,所以为true。

String常量池:

String 是由 final 修饰的类,是不可以被继承的。通常有两种方式来创建对象。

//1、
String str = new String("abcd");
 
//2、
String str = "abcd";

第一种使用 new 创建的对象,存放在堆中。每次调用都会创建一个新的对象。

第二种先在栈上创建一个 String 类的对象引用变量 str,然后通过符号引用去字符串常量池中找有没有 “abcd”,如果没有,则将“abcd”存放到字符串常量池中,并将栈上的 str 变量引用指向常量池中的“abcd”。如果常量池中已经有“abcd”了,则不会再常量池中创建“abcd”,而是直接将 str 引用指向常量池中的“abcd”。

对于 String 类,equals 方法用于比较字符串内容是否相同; == 号用于比较内存地址是否相同,即是否指向同一个对象。通过代码验证上面理论。

public static void main(String[] args){
       String str1 = "abcd";
       String str2 = "abcd";
       System.out.print(str1 == str2);//true
    }

首先在栈上存放变量引用 str1,然后通过符号引用去常量池中找是否有 abcd,没有,则将 abcd 存储在常量池中,然后将 str1 指向常量池的 abcd。当创建 str2 对象,去常量池中发现已经有 abcd 了,就将 str2 引用直接指向 abcd 。所以str1 == str2,指向同一个内存地址。
 

public static void main(String[] args){
       String str1 = new String("abcd");
       String str2 = new String("abcd");
       System.out.print(str1 == str2);//false
    }

str1 和 str2 使用 new 创建对象,分别在堆上创建了不同的对象。两个引用指向堆中两个不同的对象,所以为 false。

关于字符串 + 号连接问题:

对于字符串常量的 + 号连接,在程序编译期,JVM就会将其优化为 + 号连接后的值。所以在编译期其字符串常量的值就确定了

String a = "a1";   
String b = "a" + 1;   
System.out.println((a == b)); //result = true  
 
String a = "atrue";   
String b = "a" + "true";   
System.out.println((a == b)); //result = true 
 
String a = "a3.4";   
String b = "a" + 3.4;   
System.out.println((a == b)); //result = true 

关于字符串引用 + 号连接问题:

对于字符串引用的 + 号连接问题,由于字符串引用在编译期是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象

public static void main(String[] args){
       String str1 = "a";
	   String str2 = "ab";
	   String str3 = str1 + "b";
	   System.out.print(str2 == str3);//false
    }

对于上边代码,str3 等于 str1 引用 + 字符串常量“b”,在编译期无法确定,在运行期动态的分配并将连接后的新地址赋给 str3,所以 str2 和 str3 引用的内存地址不同,所以 str2 == str3 结果为 false

经过 jad 反编译工具反编译代码后,代码如下

public class TestDemo
{
 
    public TestDemo()
    {
    }
 
    public static void main(String args[])
    {
        String s = "a";
        String s1 = "ab";
        String s2 = (new StringBuilder()).append(s).append("b").toString();
        System.out.print(s1 = s2);
    }
}

发现 new 了一个 StringBuilder 对象,然后使用 append 方法优化了 + 操作符。new 在堆上创建对象,而 String s1=“ab”则是在常量池中创建对象,两个应用所指向的内存地址是不同的,所以 s1 == s2 结果为 false。

注:我们已经知道了字符串引用的 + 号连接问题,其实是在运行期间创建一个 StringBuilder 对象,使用其 append 方法将字符串连接起来。这个也是我们开发中需要注意的一个问题,就是尽量不要在 for 循环中使用 + 号来操作字符串。看下面一段代码:

public static void main(String[] args){
        String s = null;
        for(int i = 0; i < 100; i++){
            s = s + "a";
        }
    }

 在 for 循环中使用 + 连接字符串,每循环一次,就会新建 StringBuilder 对象,append 后就“抛弃”了它。如果我们在循环外创建StringBuilder 对象,然后在循环中使用 append 方法追加字符串,就可以节省 n-1 次创建和销毁对象的时间。所以在循环中连接字符串,一般使用 StringBuilder 或者 StringBuffer,而不是使用 + 号操作。

public static void main(String[] args){
        StringBuilder s = new StringBuilder();
        for(int i = 0; i < 100; i++){
            s.append("a");
        }
    }

使用final修饰的字符串

public static void main(String[] args){
        final String str1 = "a";
        String str2 = "ab";
        String str3 = str1 + "b";
        System.out.print(str2 == str3);//true
    }

final 修饰的变量是一个常量,编译期就能确定其值。所以 str1 + "b"就等同于 "a" + "b",所以结果是 true。

String对象的intern方法。

public static void main(String[] args){
        String s = "ab";
        String s1 = "a";
        String s2 = "b";
        String s3 = s1 + s2;
        System.out.println(s3 == s);//false
        System.out.println(s3.intern() == s);//true
    }

通过前面学习我们知道,s1+s2 实际上在堆上 new 了一个 StringBuilder 对象,而 s 在常量池中创建对象 “ab”,所以 s3 == s 为 false。但是 s3 调用 intern 方法,返回的是s3的内容(ab)在常量池中的地址值。所以 s3.intern() == s 结果为 true。

String s = new String(123) 究竟创建了几个对象

这里先说结论,第一次创建了两个,第二次创建了一个。

我们来分析这条语句。首先s是一个引用变量,用来接收new的String,因此s不是一个对象。new String是典型的创建一个对象,放到堆中。而"123"也是个对象,123在编译期就可以确定,因此是放到常量池。上面这句代码可以拆成下面两条。

String temp = “123”;
String s = new String(temp);

这样就很容易理解了,String temp = "123"毫无疑问是创建了一个对象,new String(temp)又创建了一个对象,因此是两个。

那么创建一个对象的结论是从何而来的?

首先来通过代码证明一下常量池是否存在。

String s1 = “123”;
String s2 = “123”;
System.out.println(s1 == s2);

s1和s2都是字符串对象,通过==比较的是二者地址,运行结果为true,说明二者地址相同,是同一个对象。

在 String s1 = “123”;这条语句中,创建了一个对象"123",放入常量池中,下面的String s2 = “123”;从常量池中取出了123并赋值给s2,因此s2这条语句没有创建对象。

String temp = “123”;
String s1 = new String(temp);

String temp2 = “123”;
String s2 = new String(temp2);

在这个代码中,上面的代码创建了两个对象,分别是"123"和new String(“123”),接着, 123被存入常量池中。下面的代码取出123并new String(“123”),只创建了一个对象。

结语

在 String s = new String(“123”) 这条语句中

当常量池中不存在123时,创建了两个对象。

当常量池中存在123时,创建了一个对象。

 

 

文章参考:https://blog.csdn.net/rongtaoup/article/details/89142396

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值