Java核心技术07 | int和Integer的区别

Java语言虽然号称一切都是对象,但原始数据类型是例外。

关于自动拆箱和装箱

我们都知道 int 和 Integer 可以自动相互转换,这是 Java 给我们提供的一种语法糖,语法糖可以简单理解为Java平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的。

Integer integer = 1;
int unboxing = integer ++;

这是一段普通的代码,但它完整包含了三个动作:将一个原始数据类型转换成包装类型、将一个包装类型转换成一个原始数据类型、对一个包装类型进行直接的运算。

反编译输出:

1: invokestatic  #2                  // Method
java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: invokevirtual #3                  // Method
java/lang/Integer.intValue:()I
  • 自动拆箱装箱是在编译期完成的
  • javac 将装箱操作用 Integer.valueOf() 代替了,将拆箱用 Integer.intValue() 代替了
  • 包装对象的运算,要先拆箱成原始数据类型,进行运算完毕后再装箱

通过对这个反编译例子的研究,我们就明白,在以后编程中,需要进行大量计算的地方,应该使用原始数据类型,在性能敏感的场合,优先使用原始数据类型。原则上,建议避免无意无意中的装箱拆箱行为。

在Java5中新增了静态工厂方法valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照Javadoc,这个值默认缓存是-128到127之间。

这种缓存机制并不是只有Integer才有,同样存在于其他的一些包装类,比如:

  1. Boolean,缓存了true/false对应实例,确切说只会返回两个常量实例Boolean.TRUE/FALSE
  2. Short,同样是缓存了-128到127之间的数值
  3. Byte,数值有限,所以全部被缓存
  4. Character,缓存范围'\u0000'到'\u007F'

关于 Integer 源码

整体看一下 Integer 类的源码,可以发现这个类主要是由几个常量,两个装箱拆箱方法,一些进制转换方法,一些位操作方法组成。

  1. 类和常量都被 final 修饰了起来。

很容易由此联想到这是一个不可变类 (immutable),由此想到不可变类的设计原则:

  • 类标识打上 final 标志
  • 类变量使用 final 修饰
  • 提供构造或者工厂方法设置类变量的初始值,不提供 set 方法
  • 构造时,对引用类型使用深拷贝,避免外部不确定因素的影响
  • 将集合改为不可变集合,比如使用 Java 9 List.of() 方法
  1. 特别的常量

Integer 类除了提供最大、最小的常数外,还提供了SIZE和BYTES这样的常量。后面这两个常量,值得提一下。

如果我们写过 c 或者 c++ 语言,就会知道,在这两种语言中, int 类型在 32 位和 64 位系统中是不确定的。而在 Java 中,我们无需担忧这种不同,因为在 Java 语言规范中明确规定了各种基本类型的长度。

这也是 Java 实现它的承诺——一次书写,到处运行,的一个细节。

  1. 关于缓存

Integer的缓存范围虽然默认是-128到127,但是在特别的应用场景,可以更改为更大的数值。

java.lang.Integer源码之后总实现在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;
        }

在生成缓存的这段代码中,我们注意到了一个细节,范围的下限确实是 -128,但上限是可以调节的。
如果我们的程序需要更大的缓存范围,我们可以通过在启动 JVM 时增加参数-XX:AutoBoxCacheMax=N,来设置这个缓存范围的上限。

对于缓存,还可以同样观察下其他基本类型的包装类,都同样做了一个小范围的缓存。

关于占用空间

如果我们要创建 10 万个整数,那么光是对象头的占用空间,Integer 就要比 int 多出一个数量级。这是对象机制不可避免带来的问题,我们在编写操作大量数据的代码时,也应该考虑占用空间的问题。

关于线程安全

我们都知道,不可变类是实现线程安全共享对象的一种方式。但是原始元素类型的运算就不是线程安全的。如果多个线程同时对一个 int 对象做运算,就可能引发并发问题。

class Counter {
    private final AtomicLong counter = new AtomicLong();  
    public void increase() {
        counter.incrementAndGet();
    }
}

如果有线程安全计算的需要,可以使用 AtomicInteger 这样的线程安全类进行计算。

但,如果不想涉及到类,想直接在原始数据类型上做并发操作,也是有办法的。Java 提供了 AtomicIntegerFieldUpdater 这样的类进行 cas 安全操作。下面是使用原始数据类型实现一个计数器的方式。

 class CompactCounter {
    private volatile long counter;
    private static final AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter");
    public void increase() {
        updater.incrementAndGet(this);
    }
}

关于Java原始类型和引用类型的局限性

Java 走过了这么多年的历程,这种类型系统的设计已经是很久前的了,现在也逐渐暴露了一些缺点。

  • 原始数据类型不能和泛型完美配合。

Java 的泛型机制,是一种伪泛型,它在编译期将类型转换为特定的类型。这就要求相应类型必须可以转换为 Object。

  • 无法高效地表达数据,也不便于表达复杂的数据结构,比如vector和tuple

我们知道java的对象都是引用类型,如果是一个原始数据类型数组,它在内存里是一段连续的内存,而对象数组则不然,数据存储的是引用,对象往往是分散地存储在堆的不同位置。这种设计虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代CPU缓存机制。

补充

原始数据类型和 Java 泛型并不能配合使用,也就是Primitive Types 和Generic 不能混用,于是JAVA就设计了这个auto-boxing/unboxing机制,实际上就是primitive value 与 object之间的隐式转换机制,否则要是没有这个机制,开发者就必须每次手动显示转换,那多麻烦。

但是primitive value 与 object各自有各自的优势,primitive value在内存中存的是值,所以找到primitive value的内存位置,就可以获得值;不像object存的是reference,找到object的内存位置,还要根据reference找下一个内存空间,要产生更多的IO,所以计算性能比primitive value差,但是object具备generic的能力,更抽象,解决业务问题编程效率高。

于是JAVA设计者的初衷估计是这样的:如果开发者要做计算,就应该使用primitive value。如果开发者要处理业务问题,就应该使用object,采用Generic机制;反正JAVA有auto-boxing/unboxing机制,对开发者来讲也不需要注意什么。然后为了弥补object计算能力的不足,还设计了static valueOf()方法提供缓存机制,算是一个弥补。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值