1、什么是装箱、拆箱
自JDK1.5开始, 引入了自动装箱/拆箱这一语法。之前要生成一个数据为10的Integer对象,
Integer i = new Integer(10);
提供了自动装箱后,生成一个数据为10的Integer对象如下:
Integer i = 10;
Integer i = 10;//装箱
int n = i;//拆箱
总结:
装箱:自动将基本数据类型转换为包装器类型
拆箱:自动将包装器类型转换为基本数据类型
下表是基本数据类型对应的包装器类型:
byte(1字节) | Byte |
short(2字节) | Short |
char(2字节) | Character |
int(4字节) | Integer |
long(8字节) | Long |
float(4字节) | Float |
double(8字节) | Double |
boolean(未定) | Boolean |
float跟double区别:
1、 float是单精度浮点数
double是双精度浮点数
2、内存中占有的字节数不同
float在虚拟机中占4位
double占8位
3、有效数字范围
float有效数字是8位
double有效数字是16位
4、程序中处理的速度
cup处理float单精度速度比double快
如果不声明,带小数的默认是double,要用float的话,需要强转,比如:float f = 1.3f或float f=(float)1.3
double定义:double a2= 0d;double a1 = 0.0;
2、装箱和拆箱实现原理
public class Demo {
public static void main(String[] args) {
Integer i = 10;
int n = i;
}
}
javap -c XX反编译后:
使用
可以看到
Integer i = 10;调用的Integer.valueOf(int)方法
int n = i;调用Integer.intValue方法
总结:
装箱:调用XX的valueOf方法
拆箱:调用XX的intValue方法
Integer的装箱过程源码:
public static Integer valueOf(int i) {
//判断i是否在-128和127之间,存在则从IntegerCache中获取包装类的实例,否则new一个新实例
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[],静态变量存放在常量池中
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() {}
}
作者:jijs
链接:https://www.jianshu.com/p/0ce2279c5691
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
可以看出当进行自动装箱的数值在[-128, 127]之间时, 调用valueOf()方法返回的是Integer缓存中已存在的对象引用。否则每次都是new一个新的包装类实例。
下面是java8种基本类型的自动装箱代码实现。
//boolean自动装箱成Boolean
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
//byte自动装箱成Byte
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}
//short自动装箱成Short
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);
}
//char自动装箱成Character
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
//int自动装箱成Integer
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//long自动装箱成Long
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);
}
//double自动装箱成Double
public static Double valueOf(double d) {
return new Double(d);
}
//float自动装箱成Float
public static Float valueOf(float f) {
return new Float(f);
}
只有double和float的自动装箱代码没有使用缓存,每次都是new 新的对象。对于Boolean类型,只有true,false,所以直接走缓存,Byte是直接全部缓存了。Short、Integer、Long、Boolean、Character这六种包装类型在进行自动装箱时都使用了缓存策略。使用缓存策略是因为,缓存的这些对象都是经常使用到的(如字符、-128至127之间的数字)小数字,防止每次自动装箱都创建一次对象的实例,可以节省空间,是一种叫享元模式的设计模式。
注意:如果用new Integer(10),每次创建的都是一个新的对象。建议使用valueOf进行构建对象,这样可以用到缓存策略。
例子:
public class Demo {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
Double i = 1.0;
Double j = 1.0;
Boolean k = true;
Boolean l = true;
//数值在[-128, 127]范围内,自动装箱时都是从缓存中获取对象引用,所以结果为true
System.out.println(c==d);
//数值在[-128, 127]范围外,自动装箱时每次都是new新的对象,所以结果为false
System.out.println(e==f);
//当"=="运算符的两个操作数都是包装器类型的引用,则比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程), 所以结果为true
System.out.println(c==(a+b));
//对于包装类型,当equals()方法比较的是同一类型时(比如Integer与Integer比较),实际比较的是他们的数值是否相等。如比较的不是同一类型,则不会进行类型转换,直接返回false。所以结果为true
System.out.println(c.equals(a+b));
//因为有算术运算,自动拆箱后再比较数值,所以结果为true
System.out.println(g==(a+b));
//因为equals()方法比较的是不同包装类型,不会进行类型转换,所以结果为false
System.out.println(g.equals(a+b));
//因为a+h先触发自动拆箱,a转为int类型后,需要隐式向上提升类型为long后再进行运算,最后再自动装箱转为Long包装类型,且两边数值相等,所以结果为true
System.out.println(g.equals(a+h));
//Double类没有缓存,每次都是new一个新的实例,所以结果为false
System.out.println(i == j);
//Boolean自动装箱,指向的都是同一个实例,所以结果为true
System.out.println(k == l);
}
}
Integer类的equals()方法的源代码:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
注意:当"=="运算符的两个操作数都是包装器类型的引用,则比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会先触发自动拆箱的过程)。对于包装类型,当equals()方法比较的是同一类型时(比如Integer与Integer比较), 实际比较的是他们的数值是否相等; 如比较的不是同一类型(比如Integer与Long比较), 则不会进行类型转换,直接返回false。
3、拆箱、装箱的时机
1)直接赋值:
2)函数参数:
//自动拆箱
public int getNum1(Integer num) {
return num;
}
//自动装箱
public Integer getNum2(int num) {
return num;
}
3)集合操作
在java中,泛型只能是包装类型,但是存储数据的时候,一般是直接存储对应的基本数据类型,有一个自动拆箱的过程。
4)运算符计算
当基本数据类型跟包装数据类型运算的时候,先将包装类型自动拆箱转换为基本数据类型,然后在与基本数据类型的数据进行运算。
Integer a = null;
int b = a;// int b = a.intValue();
注意:运行时会报空指针。因为自动拆箱会调用intValue方法,但是此时a是null,所以会抛异常。平时在使用的时候,注意非空判断即可。
4、拆箱、装箱带来的问题
1)自动拆箱下算术运算引起的空指针问题
public class Demo {
private Double distinct;
private void setParam(Double dSrc, boolean flag) {
this.distinct = (flag) ? dSrc : 0d;
}
}
当对包装类进行诸如三目运算符的算术运算时, 当数据类型不一致时, 编译器会自动拆箱转换为基本类型再进行运算, 所以当dSrc传入null值时, 调用doubleValue()方法拆箱就会报NP空指针异常。
这里我们可以在进行算术运算时, 统一数据类型, 避免编译器进行自动拆箱, 来解决拆箱下三目运算符的空指针问题。还是上面这个栗子, 我们将 this.distinct = (flag) ? dSrc : 0d; 修改成 this.distinct = (flag) ? dSrc : Double.valueOf(0); 即可解决, 重新反编译后如下, 因为类型一致, 没有再进行自动拆箱:
2)内存浪费
Integer sum = 0;
for(int i=1000; i<10000; i++){
sum+=i;
}
当在循环中对包装类型进行算术运算 sum = sum + i; 时, 会先触发自动拆箱, 进行加法运算后, 再进行自动装箱, 且因为运算后的sum数值不在缓存范围之内, 所以每次都会new一个新的Integer实例。所以上面的循环结束后, 将会在内存中创建9000个无用的Integer实例对象, 这样会大大降低程序的性能, 增加GC的开销, 所以我们在写循环语句时一定要正确的声明变量类型, 避免因为自动装箱而引起不必要的性能问题。
3)重载与自动装箱
在JDK1.5之前, 没有引入自动装箱/拆箱这一语法糖, 当方法重载时, test(int num) 与 test(Integer num) 的形参没有任何关系。JDK1.5之后, 当调用重载的方法时。
public static void testAutoBoxing(int num) {
System.out.println("方法形参为原始类型");
}
public static void testAutoBoxing(Integer num) {
System.out.println("方法形参为包装类型");
}
public static void main(String[] args) {
int m = 2;
testAutoBoxing(m);
Integer n = m;
testAutoBoxing(n);
}
运行结果如下:
很明显, 当调用重载的方法时, 编译器不会对传入的实参进行自动装箱操作。
最典型的就是ArrayList中出现的remove方法,它有remove(int index)和remove(Object obj)方法,如果此时恰巧ArrayList中存储的就是Integer元素,是不会出现自动装箱的。
4)==比较
因为自动装箱机制,有一部分数据返回的是true,但是在更多的空间中,数值相同的包装类型对象比较的结果为false。如果需要比较,可以考虑使用equals比较或者将其转换成对应的基本类型再进行比较可以保证结果的一致性。
参考资料:
链接:深入剖析Java中的装箱和拆箱 - 海 子 - 博客园