Java常用类之包装类(Wrapper Class)

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和拆箱后的aint类型的值并相加,将相加后的值压回到栈中(还是int)->调用Integer.valueOf(),将结果装箱-> 存储新的包装类的地址值到变量a

小结我们可以看出对包装类进行运算,则需要先拆箱,后装箱。这一过程还需要向堆中申请空间。相比而言,基本类型的运算则高效很多,基本数据类型的运算,都是在栈中进行。

从内存占用角度理解包装类的+= ;使用JDK自带的Jconsole工具,来查看前面两段代码的内存占用。

Long(包装类)的内存占用情况

基本数据类型的内存占用情况

程序刚开始执行时,仅占用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 {

包装类和基本类数据类型的转换(以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);//通过构造器的方式

    }
}

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值