Java中new一个对象,会把这个对象放在堆里,所有类都是object子类,通过stack指向堆。由于Java中对象很多都是不长久的,所以一直放在堆中不高效;且Java是一个面相对象语言,基本类型并不具有对象性质,为了让基本类型也具有对象特征,出现了包装类型(如在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使它具有了对象性质,并且为其添加了属性和方法,丰富了基本类型操作。8种基本数据类型引用类型——包装类,有了类的特点,就可以调用类中方法。使用包装类的好处:(基本数据类型的包装类的作用是)
作为和基本数据类型对应的类类型存在,方便涉及到对象的操作。
包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法。
这样八种基本数据类型对应的类统称为包装类(Wrapper Class),包装类均位于java.lang包。
int——Integer
char——Character
boolean——Boolean
byte——Byte
short——Short
long——Long
float——Float
double——Double
声明方式不同:基本类型不用new关键字,而包装类型需要用new关键字来在堆中分配存储空间;存储方式及位置不同:基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
初始值不同:基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;
使用方式不同:基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到
当然这样做也不是没有代价,装箱和拆箱的性能差距,在大数据和大并发的环境中会被体现出来。
广告系统中大量存在如下类型的数据:广告Id: long;广告价格: double;app的Id : int ...
由于Java提供了默认box unbox的操作,例如更新某个广告当前的价格就需要数据结构
Map<Long, Double>,这个时候就自动从long -> Long , double -> Double.
更新操作还不太明显,但查询广告价格几乎每一个请求都会有,这个时候box, unbox就会大大降低性能。
对此,线上代码没有使用Java原生jdk中Map、Set、List等结构,而是使用了Eclipse Collections。Eclipse Collections起源于2004年在高盛公司内部开发一个名为Caramel集合框架。这个框架可以使得Map<long,Object>这样结构,不存在box和unbox。在广告下发引擎中,上线后缩短了5ms latency.
JDK 中的所有收集类都将数据作为对象保存。如果开发人员有一组要存储在 ArrayList 中的 int 值,则无法完成。当然,除非他们使用相应的包装器类或利用 Java 中的自动装箱功能。
用List来保存Integer类的数值,在做性能优化的时候想到了装箱/拆箱的性能损失,特意实验了一下。以下代码从0开始一直加到int类型所能表达的最大值。
long start = System.currentTimeMillis();
Long sum = 0L; // 使用包装类相加
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)/1000.0);
// 输出:
// 2305843005992468481
// 耗时:15.175
start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // 使用基本数据类型相加
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)/1000.0);
// 输出:
// 2305843005992468481
// 耗时:1.643
两者代码区别仅仅在于前者的sum为包装类Long,后者的sum为基本类型long
原因分析,简而言之:
装箱和拆箱造成的性能损失。装箱会在堆空间中创建包装类,频繁的创建会导致导致堆空间碎片很多
装箱(boxing):基本数据类型->包装类型(以Integer为例)
int a = 5;
// 构造函数法
Integer a1 = new Integer(a); // 数值类型转包装类
Integer a2 = new Integer(5);
Integer a3 = new Integer("5"); // 字符串类型转包装类// Integer.valueOf()
Integer a4 = Integer.valueOf(a);
Integer a5 = Integer.valueOf(5);
需要注意的是,包装类初始化会在堆中申请空间。不管使用new Integer()还是Integer.valueOf()。因为Integer.valueOf()本质上是通过工具类的形式,创建了新的Integer对象,不过是要先查询创建的数值是否在缓存池(-128, 127)中。
publicstatic Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
returnnew Integer(i);
}
拆箱(unboxing):包装类->基本数据类型(以Integer为例)
Integer b = new Integer(5); // 假设我们已经有了一个包装类// 调用xxValue()方法int b1 = b.intValue(); // 转化为int类型double b2 = b.doubleValue(); // 转化为double类型long b3 = b.longValue(); // 转化为long类型
字节码角度理解包装类+=发生了什么?
以一段简单的代码为例:
Integer a = 5;
a+=1;
其字节码内容为(字节码的行号不一定连续):
0 iconst_5
1 invokestatic #16 <java/lang/Integer.valueOf>4 astore_1
5 aload_1
6 invokevirtual #22 <java/lang/Integer.intValue>9 iconst_1
10 iadd
11 invokestatic #16 <java/lang/Integer.valueOf>14 astore_1
我们可以看出:Integer a = 5对应字节码的0,1,4:把常量5压入栈中->隐式调用了Integer,valueOf()->存储新的包装类的地址值到变量a
a+=1对应了6-14行:加载变量a -> 拆箱为int基本类型(调用intValue)-> 把常量1压入栈中 -> 弹出1和拆箱后的a的int类型的值并相加,将相加后的值压回到栈中(还是int)->调用Integer.valueOf(),将结果装箱-> 存储新的包装类的地址值到变量a
小结:我们可以看出对包装类进行运算,则需要先拆箱,后装箱。这一过程还需要向堆中申请空间。相比而言,基本类型的运算则高效很多,基本数据类型的运算,都是在栈中进行。
从内存占用角度理解包装类的+= ;使用JDK自带的Jconsole工具,来查看前面两段代码的内存占用。
Long(包装类)的内存占用情况
![](https://i-blog.csdnimg.cn/blog_migrate/176b473bf372b7e557b597ff9896c255.png)
基本数据类型的内存占用情况
![](https://i-blog.csdnimg.cn/blog_migrate/c48e16ddbf52e8b002fc48d872027df3.png)
程序刚开始执行时,仅占用8M左右的内存。使用包装类存储运算结果,会导致内存占用高达80M,峰值达到138M,并且年轻代进行了505次垃圾回收;使用基本数据类型时,内存占用始终保持在8M左右,并且没有垃圾回收。
因此,我们可以得出:对包装类进行频繁的运算会占用很多内存空间,导致执行效率不高。
增强型foreach循环遍历包装类的集合
我们知道了包装类进行运算会使得效率低下,那么在遍历的时候,我们要怎么做呢?哪种写法会好些呢?
例如下面这几种相加的方法有何区别呢?
// 使用流生成0到40000000的List
List<Long> arr = LongStream.rangeClosed(0, 40000000).boxed().collect(Collectors.toList());
// ①在for中取取变量时为基本类型, 结果存放为基本类型long start = System.currentTimeMillis();
long sum = 0;
for(long l:arr) { // 临时变量l在栈中创建,直接将arr中的元素转化为LongValue
sum += l;
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)/1000.0);
// ② 在for中取取变量时为包装类型,结果存放为基本类型
start = System.currentTimeMillis();
long Sum = 0L;
for(Long l:arr) { // 临时变量l在堆中创建
Sum += l; // 相加时转时调用l.longValue()
}
System.out.println(Sum);
end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)/1000.0);
// ③ 在for中取取变量时为包装类型,结果存放为包装类
start = System.currentTimeMillis();
Long SUM = 0L;
for(Long l:arr) {// 临时变量l在堆中创建
SUM += l; // l和SUM都调用longValue(),完成相加后再装箱
}
System.out.println(SUM);
end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)/1000.0);
// 输出://800000020000000//①耗时:0.269//800000020000000//②耗时:0.326//800000020000000//③耗时:1.089
字节码文件此处就不贴了,带上循环显得有些繁琐。变量重复使用还需要局部变量表。
第一种方式速度最快,其局部变量l为基本类型,仅需在栈中申请空间;结果Sum也保存在栈中;拆箱发生在元素取出时。
第二种方式比第一种略慢,局部变量l为包装类,需在堆中申请空间;但其结果也保存在栈中;拆箱发生在两数相加时。
第三种方式最慢,其局部变量l为包装类,需要在堆中申请空间;其结果SUM也需要在堆中申请空间;要命的是,两数相加时均发生拆箱操作,相加之后又要创建新包装类。
总结
包装类在进行计算时(包装类与包装类/包装类与基本类型)都会自动拆箱。
其结果如果仍然用包装类存储,则会再次发生装箱。
频繁的拆箱装箱会导致内存碎片过多,引发频繁的垃圾回收,影响性能。
虚线箭头表示实现关系,实线箭头表示继承关系
public interface Comparable<T>
public class AssistantUserEntity implements Serializable {
![](https://i-blog.csdnimg.cn/blog_migrate/dcf1b735d228d64ce53142440245b66f.png)
包装类和基本类数据类型的转换(以int 和Integer为例,其他的类推),在jdk5以前是手动的装箱和拆箱(装箱就是基本数据类型到包装类型,反之,拆箱)
自动装箱底层调用的是valueOf方法,比如Integer.valueOf() ; 包装类型和String类型的相互转换(以Integer转String为例) ,代码如下:
package com.xjedu.wrapper;
public class Main {
public static void main(String[] args) {
//演示int和Integer的装箱和拆箱
//jdk5以前是手动装箱和拆箱
//手动装箱(将基本数据类型转成包装类)
int n1 = 100;
Integer integer = new Integer(n1);
//或者这样操作
Integer integer1 = Integer.valueOf(n1);
//手动拆箱(Integer到int)
int i = integer.intValue();
//jdk5以后的方式,就可以自动装箱自动拆箱了
int B2 = 200;
Integer a=11;
//自动装箱int到Integer
Integer integer2 = B2;//底层还是用了new的思想,使用的是Integer.valueOf(B2);
//自动拆箱Integer到int
int B3 = integer2;//底层还是用了new的思想,使用的是integer.intValue();方法。
//举例
Double d = 100d;//对的,使用的是自动装箱 Double.valueOf(100d);
Float f = 1.5f;//与上面的同理
//这两个输出的结果相同吗,为什么?
Object obj1 = true? new Integer(1) : new Double(2.0);
//三元运算符(是一个整体,精度最高的是Double,因此会提升精度)
System.out.println(obj1);//此处输出的是1.0而不 是1
Object obj2;
if(true)
obj2 = new Integer(1);
else
obj2 = new Double(2.0);
System.out.println(obj2);//此处输出的就是1,而不是1.0,因为这是分别计算的
//包装类型和String类型的相互转换(以Integer转String为例)
Integer i1 = 100;//自动装箱
//方式1
String str1 = i1 +"";
//方式2
String str2 = i1.toString();
String str3 = String.valueOf(i1);
//String 到包装类(Integer)
String str4 = "12345";
Integer i2 = Integer.parseInt(str4);//使用到自动装箱
//还可以这样
Integer i3 = new Integer(str4);//通过构造器的方式
}
}