本文转载自个人掘金博客:https://juejin.im/post/5ec52a3b51882542f010a898
前言
这篇文章主要从使用角度,源码角度以及JVM内存位置等角度深入解析Java的基本数值包装类。
1. 包装类
1.1 包装类的定义:
Java中每一种基本类型都会对应一个唯一的包装类(位于java.lang.*package中),基本类型与其包装类都可以通过包装类中的静态或者成员方法进行转换。每种基本类型及其包装类的对应关系如下表所示。
| 基本数据类型 | 包装类 |
| ------------ | --------- |
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
- 需要明确的是这些包装类都是一个java类,所以这些类的对象实例都是在堆上分配。
- 所有的包装类都是final修饰的,也就是它们都是无法被继承和重写的。
- 包装类实例化
Integer i = new Integer(1);
有连个两个部分,首先在堆上实例化new Integer(1)这个对象实例,然后变量i作为堆上该对象实例的地址或者引用被保存。
1.2 与基础数值类型的区别
- 作为类变量或是类成员变量的默认值不同,如Integer默认值为null,int的默认值为0。
- 在JVM的内存位置不同,包装类的对象实例在堆上分配,基本数值类型则有如下3种情况:
- 在方法中时,存放在虚拟机栈的栈帧中的局部变量表中;
- 作为类的成员变量时,存放在栈中;
- 作为类的静态变量/常量时,存放在方法区中。
- 包装类型可用于泛型,而基本类型不可以。因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个特例。
- 包装类必须实例化之后才能使用(java 1.5会自动的进行装箱),基础数据类型则不用。
- 包装类型的变量作为一个指针(引用),指针占用的内存在64位虚拟机上8个字节,如果默认开启指针压缩是4个字节,
-XX:CompressedOops
。实例化对象的内存则需要根据包装类型指针压缩情况以及padding的情况来计算。基本数值类型的变量所占内存就是其内存大小。
2. 包装类的使用
包装类的使用我理解大致分为两个方面:
2.1 包装类提供的静态函数
这里以Integer包装类为例,大致展示下其提供的各种非常有用的API:
1. String toString(int i, int radix),将一个十进制整数转化为radix进制的字符串
2. String toHexString(int i),将一个十进制整数转化为16进制的字符串
3. String toOctalString(int i),将一个十进制整数转化为8进制的字符串
4. String toBinaryString(int i),将一个十进制整数转化为2进制的字符串
5. Integer valueOf(String s, int radix)/int parseInt(String s, int radix),读取一个radix进制字符串
6. Integer valueOf(String s)/int parseInt(String s),读取一个radix进制字符串
7. int compareTo(Integer anotherInteger),this与另一个Integer进行比较
8. int compare(int x, int y),两个int进行比较
9. int lowestOneBit(int i),返回i最低位的1的位数
这里仅展示了部分博主觉得常用的API,还有一些其他有用API值得读者自己去查看源码。
2.2 包装类的实例化使用
我们将根据下面一段demo代码的运行结果来介绍包装类使用过程中的注意事项:
@Test
public void testCase01() {
Integer intOne1 = 1;
Integer intOne2 = 1;
Integer intOne3 = new Integer(1);
Integer two = 2;
Integer three = 3;
Integer intNum1 = 321;
Integer intNum2 = 321;
int intNum3 = 321;
Long longTwo = 2l;
long ltwo = 2l;
// case1: == 比较包装类实例化对象地址,IntegerCache缓存Integer.valueOf()入口的 -128 ~ 127
System.out.println(intOne1 == intOne2); // true
// case2:
System.out.println(intOne1 == intOne3); // false
// case3: 超出Integer常量池缓存范围,地址不相同
System.out.println(intNum1 == intNum2); // false
// case4:
System.out.println(intOne1.equals(intOne3)); // true
// case5: a.equals(b),1. 保证b是和a一样的包装类 2.比较数值
System.out.println(intOne1.equals(intOne2)); // true
// case6: == 一边有表达式或者基本类型时,会先进行拆箱,然后比较具体数值
System.out.println(three == (intOne1 + two)); // true
// case7:
System.out.println(intNum1 == intNum3); // true
// case8:
System.out.println(intNum1.equals(intNum2)); // true
// case9: 自动装箱
System.out.println(intNum1.equals(intNum3)); // true
// case10: 类型不一致
System.out.println(two.equals(longTwo)); // false
// case11: 自动装箱,类型不一致
System.out.println(two.equals(ltwo)); // false
// case12: 自动拆箱,然后比较具体数值
System.out.println(longTwo == intOne1 + intOne1); // true
// case13: 自动装箱,类型不一致
System.out.println(longTwo.equals(intOne1 + intOne1)); // false
// case14: 自动装箱,类型一致
System.out.println(two.equals(intOne1 + intOne1)); // true
// case15: 容量小的类型可自动(隐式)转换为容量大的数据类型,byte,short,char → int → long → float → double
System.out.println(2 == 2L); // true
System.out.println(2 == 2d); // true
System.out.println((short)2 == 2f); // true,(int) 2 先强转成(short)2,然后在自动转成float
}
2.2.1 自动装箱和自动拆箱
把基本类型转换成包装类型的过程叫做装箱(boxing)。反之,把包装类型转换成基本类型的过程叫做拆箱(unboxing)。Java SE5 以后提供了自动装箱与自动拆箱的功能。
Integer integerVal = 10; // 自动装箱
int intVal = integerVal; // 自动拆箱
上面这段代码使用 jd-gui反编译后的结果如下所示:
Integer integerVal = Integer.valueOf(10);
int intVal = integerVal.intValue();
对于Integer类型的包装类:
- 自动装箱是通过 Integer.valueOf() 完成的。
- 自动拆箱是通过 Integer.intValue() 完成的。
2.2.2 "=="运算符的使用
-
“==”比较的是包装类实例化变量的地址。
-
Integer包装类内置IntegerCache
-
默认会缓存 -128 ~ 127之间的Integer包装类的实例对象。
-
通过Integer.valueOf()的入口才会访问到这些缓存的对象实例。
-
自动装箱会访问到,自己手动new Integer(1)不会访问到。
-
-
如果"=="运算符的一边有表达式或者基本数据类型,会先进行拆箱,然后比较具体数值。
2.2.3 equals方法的使用
Integer.equals方法的源码:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
- 包装类的equals方法首先会进行传入变量类型判断。
- 在类型相同的情况下才会进行具体数值的比较。
- 如果equals的入参是一个基本类型,会先进行装箱。
2.2.4 基本数据类型转换
整型,字符型,浮点型的数据在混合运算中相互转换,转换时遵循以下原则:
- 容量小的类型可自动转换(隐式转换)为容量大的数据类型;byte,short,char → int → long → float → double。byte,short,char之间不会相互转换,他们在计算时首先会转换为int类型。boolean 类型是不可以转换为其他基本数据类型。
- 容量大的类型转容量小的数据类型需要强制转化(显示转化)。
3. 包装类的Cache
通过java.lang.*package的源码阅读可以发现:
- Boolean,Character ,Byte,Short,Integer,Long等基本类型的包装类具有缓存,Float,Double等基本类型的包装类则没有缓存。
- 通过**包装类Class.valueOf()**的入口(即自动装箱的方法)才会获取到这些缓存的对象实例,其他如 new Integer(1)的方式则是重新在堆上实例化一个Integer的对象实例。
- 不同包装类的缓存范围不完全相同,其中只有Integer的缓存上限可以配置。
3.1 Boolean类型的缓存
Boolean类型的缓存就直接是2个静态常量,缓存的范围就是:true, false
boolean: 1位,范围 true or false
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
3.2 Character类型的缓存
Character类型的缓存是通过静态内部类CharacterCache实现,缓存的范围是: [0, 127],即标准ASCII码表的范围。 char:16位,2个字节,范围[0, 2^16 - 1]
,[\u0000, \uffff]。
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
3.3 Byte类型的缓存
Byte类型的缓存是通过静态内部类ByteCache实现,缓存的范围是: [-128, 127]。
byte: 8位,1个字节,范围[-2^7, 2^7 - 1]
即 [-128, 127],所以Byte缓存的范围就是它本身能表示的范围。
private static class ByteCache {
private ByteCache(){}
static final Byte cache[] = new Byte[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Byte((byte)(i - 128));
}
}
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}
3.4 Short类型的缓存
Short类型的缓存是通过静态内部类ShortCache实现,缓存的范围是: [-128, 127]。
short: 16位,2个字节,范围[-2^15, 2^15 - 1]
。
private static class ShortCache {
private ShortCache(){}
static final Short cache[] = new Short[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Short((short)(i - 128));
}
}
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
3.5 Integer类型的缓存
Integer类型的缓存是通过静态内部类IntegerCache实现,默认缓存的范围是: [-128, high(默认为127)]。其中缓存的上限high可以通过JVM的-XX:AutoBoxCacheMax=<size>
命令配置。
int: 32位,4个字节,范围[-2^31, 2^31 - 1]
。
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer 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];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
3.6 Long类型的缓存
Long类型的缓存是通过静态内部类LongCache实现,缓存的范围是: [-128, 127]。 long: 64位,8个字节,范围[-2^63, 2^63 - 1]
。
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
4. 包装类的内存布局
4.1 普通Java对象的内存布局
首先了解下Java对象的内存布局。一个Java对象在内存中可以分为三部分:
- 对象头:普通Java类型的对象头,分为两个部分:
- Mark Word: 存储对象自身的运行时数据,如hashcode、gc分代年龄 ,锁记录等。
- Klass Word: 存储对象的类型指针,该指针指向方法区它的类元数据,JVM通过这个指针确定对象是哪个类的实例 。
- 实例数据:即Java的成员字段,包括基本类型和对象引用。
- 对齐填充:只用作占位对齐字节,不一定存在。 一个对象的内存布局示意如下:
|------------------------|-----------------|---------|
| Object Header | Instance Data | Padding |
|-----------|------------|-----------------|---------|
| Mark Word | Klass Word | field1|filed2| | Padding |
|-----------|------------|-----------------|---------|
4.2 Java包装类的内存布局
这里通过jol工具来分析对象所占的内存,其Maven依赖如下:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
使用方式如下:
@Test
public void testCase01() {
System.out.println(ClassLayout.parseInstance(new Integer(1)).toPrintable());
}
这里的进行研究测试64位JVM虚拟机,JDK8。默认开启-XX:CompressedOops选项。
这里以Integer包装为例进行分析
4.2.1 开启指针压缩
上面代码输出结果为:
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 3e 22 00 f8 (00111110 00100010 00000000 11111000) (-134208962)
12 4 int Integer.value 1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
还原其内存布局,可以发现包装类的对象实例所占内存为16个字节,在加上包装类引用变量指针的4个字节,一共20个字节,比int的4个字节的内存多5倍。
|----------------------------------------|----------------|
| Object Header | Instance Data |
|-------------------|--------------------|----------------|
| Mark Word(8 byte) | Klass Word(4 byte) | value(4 byte) |
|-------------------|--------------------|----------------|
4.2.2 关闭指针压缩
VM options配置-XX:-UseCompressedOops
上面代码输出结果为:
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 60 96 3c 25 (01100000 10010110 00111100 00100101) (624727648)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 int Integer.value 1
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
还原其内存布局,可以发现包装类的对象实例所占内存为24个字节,在加上包装类引用变量指针的8个字节,一共32个字节,比int的4个字节的内存多8倍。
|----------------------------------------|----------------|-----------------|
| Object Header | Instance Data | Padding |
|-------------------|--------------------|----------------|-----------------|
| Mark Word(8 byte) | Klass Word(8 byte) | value(4 byte) | Padding(4 byte) |
|-------------------|--------------------|----------------|-----------------|
参考与感谢
老哥们,觉得总结的有点用的,点个赞呗~
- https://juejin.im/post/5d8ff563f265da5bb252de76
- https://juejin.im/post/5b624f4d518825068302aee9
- https://juejin.im/post/5d628c50e51d4561c75f2822
- https://blog.youkuaiyun.com/xialei199023/article/details/63251295
- https://cloud.tencent.com/developer/article/1097119
- https://www.jianshu.com/p/ad505f9163b2