1 前言
我们都知道,java提供了8种基本数据类型供我们使用,此外,java还提供了这些基本数据类型所对应的包装类,而且通过自动装箱和自动拆箱机制能够轻松地完成基本数据类型和包装类之间的转换。
- 自动装箱:自动将基本数据类型转换为包装类。
- 自动拆箱:自动将包装类转换为基本数据类型。
2 Integer
2.1 总体结构
首先,我们来看一下Integer类的总体结构,如下图所示:
Integer类图结构
- Integer继承了Number类,并重写了Number类intValue()、longValue()、floatValue()等方法来完成对一些基本数据类型的转换
- Integer类实现了Comparable接口,这使得我们可以重写compareTo方法来自定义Integer对象之间的比较操作
2.2 注释
从Integer类的注释中我们可以获取到以下信息:
- Integer类将原始类型int的值包装在一个对象中,Integer类的对象使用一个int类型的字段value来表示对象的值
- 此外,Integer类还提供了一系列的方法来完成int到String,int到其他基本数据类型,String到int的转换
和String类一样,Integer类也是不可变类,Integer类使用final进行修饰,而且用于表示Integer对象值的字段value也使用了final进行修饰,Java中的所有包装类都是不可变类。
2.3 自动装箱、拆箱原理
通过以下一段简单的代码我们就能了解到自动装箱、拆箱的过程:
public class IntegerDemo {
public static void main(String[] args) {
// 自动装箱
Integer numberI = 66;
// 自动拆箱
int number = numberI;
}
}
那么包装类的自动装箱、拆箱究竟是如何实现的呢?
我们可以通过javap反编译或者通过idea中的插件jclasslib来了解这一过程,如下图所示:
自动装箱、拆箱
可以看到,Integer类是通过调用自身内部的valueOf()方法来实现自动装箱的,而自动拆箱则是通过调用继承自Number类的intValue()方法来实现的。
2.4 缓存
在《阿里巴巴 Java 开发手册》中有一段关于包装对象之间值的比较问题的规约:就是所有整型包装类对象之间值的比较,全部使用 equals 方法比较。而手册中对这条规约的说明就是:对于 Integer var = ? 在 - 128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache() 方法中产生的,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,因此推荐使用 equals 方法进行值的判断。
让我们来看一下这样一段代码:
Integer a = 100, b = 100, c = 150, d = 150;
System.out.println(a == b);
System.out.println(c == d);
有了上文的分析,我们很轻松地就能知道答案:true,false。这是因为对于 - 128 至 127 范围内的值,Integer对象会使用缓存,因此b会复用a对象,而超出了缓存区间的 150 则不会使用缓存,因此会创建新的对象。
从上文对自动装箱的分析我们可以得知,每次对Integer对象进行赋值都会调用其valueOf()方法,接下来我们从源码来分析Integer类的缓存机制:
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以得知,Integer的缓存机制是通过它内部的一个静态类IntegerCache来实现的,在IntegerCache.low和IntegerCache.high之间的值将直接使用IntegerCache的cache数组中缓存的值,否则会创建一个新的对象。同时,从源码的注释中我们还可以得知:如果不要求必须新建一个整型对象,缓存最常用的值(提前构造缓存范围内的整型对象),会更省空间,速度也更快。因此Integer的缓存是为了减少内存占用,提高程序运行的效率而设计的。
进一步了解IntegerCache的源码,我们还可以知道缓存的区间其实是可以设置的:
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
// 最小值固定为-128
static final int low = -128;
static final int high;
static final Integer cache[];
// 初始化缓存数组
static {
// 最大值可以通过属性来配置
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);
// 最大的缓存数组大小为Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// 如果属性值不能转换为int,就忽略它.
}
}
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() {}
}
从注释中我们可以得知:
- 缓存是为了支持自动装箱对象的标识语义,在自动装箱时可以复用这些缓存的对象,而 -128 到 127 的范围是基于JLS的要求而设定的
- 首次使用时会初始化缓存。缓存的大小可以通过虚拟机参数
-XX:AutoBoxCacheMax=<size>}
来控制,在VM初始化期间,java.lang.Integer.IntegerCache.high可以设置并保存在sun.misc.VM类中的私有系统属性中(-Djava.lang.Integer.IntegerCache.high=<value>
),未指定则为 127
更多细节可以参考 JLS 的Boxing Conversion 部分的相关描述。
3 Long
3.1 源码分析
有了上文对于Integer类的分析,Long内部的自动装箱、拆箱以及缓存机制也就大同小异了:
/**
* Returns a {@code Long} instance representing the specified
* {@code long} value.
* If a new {@code Long} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Long(long)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* Note that unlike the {@linkplain Integer#valueOf(int)
* corresponding method} in the {@code Integer} class, this method
* is <em>not</em> required to cache values within a particular
* range.
*
* @param l a long value.
* @return a {@code Long} instance representing {@code l}.
* @since 1.5
*/
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);
}
与Integer不同的是,Long的valueOf()方法并没有被要求缓存特定范围的值,而且通过LongCache我们可以得知Long的缓存范围被固定在了 -128 到 127 ,并不能通过参数来修改缓存的范围:
private static class LongCache {
private LongCache(){}
// 缓存,范围从 -128 到 127,+1 是因为有个 0
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
// 缓存 Long 值,注意这里是 i - 128 ,所以在拿的时候就需要 + 128
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
3.2 面试题
为什么使用 Long 时,大家推荐多使用 valueOf 方法,少使用 parseLong 方法?
因为 Long 本身有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法就没有这个机制。
4 总结
通过本文,我们学习了Integer、Long的自动装箱、拆箱原理,还学习了它们内部的缓存机制,缓存是一种重要且常见的性能提升手段,在很多应用场景诸如Spring、数据库、Redis中都有广泛运用,因此了解它的机制对于我们的学习工作都有很大的帮助。本文分析了Integer、Long内部的一些巧妙设计,对于其他包装类来说也是互通的,本文就不再赘述了,有兴趣的读者可以自行去研究。
注:以上分析皆基于JDK 1.8版本来进行。
网友评论