字符串常量池与包装类详解

6 篇文章 0 订阅

字符串常量池

设计思想

JVM为了提升性能和减少内存开销,避免重复创建字符串,其维护了一块特殊的内存空间,即字符串常量池。当需要使用字符串时,先去检查字符串常量池是否存在该字符串,若存在,则直接返回该字符串的引用地址;若不存在,则在字符串常量池中创建字符串对象,并返回对象的引用地址。

String a = "abc";	// 放至常量池
String b = "abc";	// 从常量池中取出
System.out.println(a == b);	// trzue

注意:在 JDK7 之前,字符串常量池位置在永久代(方法区)中,此时字符串常量池存放的是对象及其引用。到 JDK7 时,字符串常量池被移动至堆中,此时字符串常量池只存放引用,字符串对象在堆中。


所处内存区域

  • 在 JDK 1.7 之前,运行时常量池(包括字符串常量池)存放在方法区,此时HotSpot虚拟机对方法区的实现为永久代。

  • 在 JDK 1.7 时,字符串常量池被从方法区转移至 Java 堆中,注意并不是运行时常量池,而是字符串常量池被单独转移到堆,运行时常量池剩下的东西还是方法区中,也就是HotSpot的永久代

  • 在 JDK 1.8 时,方法区(HotSpot 的永久代)被彻底移除了(JDK 7 就已经开始了),使用在本地内存中实现的元空间来代替。此时字符串常量池还在堆中,只不过方法区的实现从永久代变为了元空间,并将 JDK 1.7 中永久代剩余的内容(运行时常量池、类型信息)全部移到元空间


String 类型的变量和常量做“+”运算时发生了什么?

String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";	//常量池中的对象
String str4 = str1 + str2; 		//在堆上创建的新的对象
String str5 = "string";			//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。

并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。

在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。

对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string";

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  1. 基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量
  2. final 修饰的基本数据类型和字符串变量
  3. 字符串通过 “+” 拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

而引用的值在程序编译期无法确定的,编译器无法对其进行优化。

因此,str1str2str3 都属于字符串常量池中的对象。

对象引用和 “+” 的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

String str4 = new StringBuilder().append(str1).append(str2).toString();

因此,str4 并不是字符串常量池中存在的对象,属于堆上的新对象。

不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";	// 常量池中的对象
String d = str1 + str2; 	// 常量池中的对象
System.out.println(c == d);	// true

final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就想到于访问常量。

可以通过查看字节码文件验证:

但如果编译器在运行时才能知道其确切值的话,就无法对其优化,即只要其中有一个是变量,结果就在堆中。


new String()会创建几个对象

  • 首先来看一个例子:
String str1 = "abc";
String str2 = new String("abc");
String str3 = new String("abc");
System.out.println(str1 == str2);	//false
System.out.println(str2 == str3);	//false

代码运行之后输出均为false,原因是:

// 从字符串常量池中拿对象
String str1 = "abc";

此时 JVM 会先检查字符串常量池有没有"abc",若存在,则str1直接指向常量池中的"abc";若不存在,则在常量池中创建一个,然后 str1 指向字符串常量池中的对象。

// 直接在堆内存中创建新的对象
String str2 = new String("abc");
String str3 = new String("abc");

使用 new String() 会在堆中创建一个字符串对象,然后检查字符串常量池中是否存在字符串值相同的字符串对象,若没有,则在字符串常量池中也创建一个值相同的字符串对象,最后返回堆中该字符串对象的地址

故使用 new String() 会创建1或2个对象。

  • 那换一个例子:
String str = new String("a") + new String("b");

此时创建了6个对象:

1、new StringBuilder(因为出现连接操作,且连接的是变量)

2、堆中的"a"

3、常量池中的"a"

4、堆中的"b"

5、常量池中的"b"

6、堆中的"ab"(通过toString()方法在堆中创建,但不会在字符串常量池中创建)


关于intern()

String s = new String("1");
s.intern();		// 本质上这行代码没啥用,因为字符串常量池已经存在"1"了(由于new String)
String s2 = "1";
System.out.println(s == s2);	// jdk6:false  jdk7/8:false

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);	// jdk6:false  jdk7/8:true

在JDK1.6和 JDK1.7以后intern函数有不同的处理:

在JDK1.6中,intern的处理是:先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量地址,如果没有找到,则在字符串常量池创建该常量并返回该对象地址;

在JDK1.7中,intern的处理是:先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量地址;如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,之后拿到的是该字符串常量的引用,实际对象存储在堆中。

使用intern方法,当常量池不存在字符串常量时:

JDK 1.7之前(不包括1.7)intern方法会在常量池创建对象,并返回对象的引用;JDK 1.7及以后,字符串常量池被从方法区拿到了堆中,使用intern方法时 JVM 不会在常量池中创建该对象,而是将堆中这个对象的引用直接放到常量池中,减少不必要的内存开销。


包装类以及对应的常量池

包装类型是什么?基本类型和包装类型有什么区别?

Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,把基本类型转换成包装类型的过程叫做装箱(boxing);反之,把包装类型转换成基本类型的过程叫做拆箱(unboxing),使得二者可以相互转换。

Java 为每个原始类型提供了包装类型:

  • 原始类型: boolean,char,byte,short,int,long,float,double

  • 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

基本类型和包装类型的区别主要有以下几点:

1、包装类型可以为 null,而基本类型不可以。它使得包装类型可以应用于 POJO 中,而基本类型则不行。那为什么 POJO 的属性必须要用包装类型呢?《阿里巴巴 Java 开发手册》上有详细的说明, 数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱(将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值),就会抛出 NullPointerException 的异常。

2、包装类型可用于泛型,而基本类型不可以。泛型不能使用基本类型,因为使用基本类型时会编译出错。

List<int> list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType
List<Integer> list = new ArrayList<>();

因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个特例。

3、基本类型比包装类型更高效。基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。 很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间。


什么是自动装箱、自动拆箱

自动装箱:将基本数据类型转化为包装类对象

Integer i = 9;	==>>	Integer i = Integer.valueOf(9)

9是属于基本数据类型的,原则上它是不能直接赋值给一个对象Integer的。引入了自动装箱/拆箱机制,就可以进行这样的声明,自动将基本数据类型转化为对应的封装类型,成为一个对象以后就可以调用对象所声明的所有的方法。

自动拆箱:将包装类对象转化为基本数据类型

Integer i = 9;
int j = i;	===>>	通过使用 Integer.intValue()	

// ===================

Integer i = 9;
System.out.print(i++);

因为对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除

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);// true
System.out.println(i1 == i2 + i3);//true
System.out.println(i1 == i4);// false
System.out.println(i4 == i5);// false
System.out.println(i4 == i5 + i6);// true
System.out.println(40 == i5 + i6);// true

i1 , i2, i3 都是常量池中的对象,i4 , i5 , i6 是堆中的对象。

i4 == i5 + i6 为什么是 true 呢?因为, i5i6 会进行自动拆箱操作,进行数值相加,即 i4 == 40Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。


包装类常量池

Byte, Short, Integer, Long 这 4 种包装类默认创建了数值 [-128, 127] 的相应类型的缓存数据,Character 创建了数值在 [0, 127] 范围的缓存数据,Boolean 直接返回 True or False

两种浮点数类型的包装类 Float, Double 并没有实现常量池技术。

Integer
  • Integer缓存范围: [-128, 127]

Integer的equals方法被重写过,比较的是内部value的值;

使用 == 如果在[-128, 127]会被cache缓存,超过这个则比较的是对象是否相同。

Integer源码:

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

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];	// cache数组已存储好 [-128, 127]的Integer对象
    
    static {
        int h = 127;
        // ......
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);	// 创建[-128, 127]的Integer对象
        // ......
    }
}
判断Integer值是否相同时,推荐使用equals方法或自动拆箱

1、使用equals方法

Integer x = 128;
Integer y = 128;
System.out.println(x == y);	//结果为false

因为128超过了缓存区,故x、y实际上都创建了值为128的Integer对象,地址肯定不一样

Integer x = 128;
Integer y = 128;
System.out.println(x.equals(y));//结果为true

equals方法直接比较Integer对象的value属性值

2、自动拆箱

Integer x = 128;
Integer y = 128;
int z = y;
System.out.println(x == z);	//结果为true

先把运算比较的两个变量其中一个用int类型代替,==比较时x便自动拆箱转化为int


补充:Java中的常量池

常量池分类

Java中常量池可分为三种:全局字符串常量池class文件常量池运行时常量池。其中字符出常量池就是全局字符串常量池

关于这些常量池的详细解释可参考:Java中几种常量池的区分


参考资料

JavaGuide/Java内存区域.md at master · Snailclimb/JavaGuide (github.com)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值