今天早上在学习公司代码,然后准备学习下MVP框架,于是找了个简单的MVP框架例子,结果在框架中,发现了一个类叫SparseArray的类,秉持着一种遇到问题就深究下去的精神,我就转去看SparseArray相关的知识,结果发现了一片新天地,顺带研究了一番Android里的几个集合类,主要是SparseArray和ArrayMap,然后我就想,Java里不是有了很多集合类了嘛,比如HashMap,TreeMap,ArrayList,LinkList等等,为啥Android还要再弄一个呢,何况Android本身就是基于Java的,于是我又屁颠屁颠去找相关资料,结果,又发现一个小岛(这个应该不是新大陆,额嘿嘿!),这个小岛就是Java装箱和拆箱,,为了弄清楚Android为啥还要专门弄个自己的集合类,于是就有了这篇文章,关于问题的答案就放在文章结语中吧!!
public static void main(String[] args) {
int i0=10;
int i1=10;
int i2=500;
int i3=500;
Integer i4=new Integer(10);
Integer i5=new Integer(10);
Integer i6=new Integer(500);
Integer i7=new Integer(500);
System.out.println("i0==i1? "+(i0==i1));
System.out.println("i2==i3? "+(i2==i3));
System.out.println("i4==i5? "+(i4==i5));
System.out.println("i6==i7? "+(i6==i7));
}
这是一个很简单的例子,我们来看一下它的运行结果
i0i1? true
i2i3? true
i4i5? false
i6i7? false
怎么样,和预想的是一样的吗,这里主要就是一个知识点,Java中,基本类型的比较的是值,而封装类型比较的是对象的地址,所以后面两个是false。
好了,我们再把这个代码改一改
public static void main(String[] args) {
Integer i8 = 40;
Integer i9 = 40;
Integer i10 = 500;
Integer i11 = 500;
Double d0 = 40.0;
Double d1 = 40.0;
Double d2 = 500.0;
Double d3 = 500.0;
System.out.println("i8==i9? " + (i8 == i9));
System.out.println("i10==i11? " + (i10 == i11));
System.out.println("d0==d1? " + (d0 == d1));
System.out.println("d2==d3? " + (d2 == d3));
}
让我们再来看看结果,是不是你预想中的样子呢
i8i9? true
i10i11? false
d0d1? false
d2d3? false
嘿嘿,你现在可能就有点迷了,没关系,接着往下看
public static void main(String[] args) {
Integer i12 = new Integer(40);
Integer i13 = new Integer(40);
Integer integer0 = new Integer(0);
System.out.println("i12==i13? " + (i12 == i13));
System.out.println("i12==i13+integer0? " + (i12 == i13 + integer0));
}
结果可能会是什么呢…
i12i13? false
i12i13+integer0? true
怎么样,猜对了嘛,是不是一脸蒙加上&%¥#@*,没事,下面我们来找下原因,看看到底为啥答案回事这样的。
在欣赏美(cao)妙(dan)的Java源码前,我们首先需要知道的是,jdk源码偷偷在哪里给我们做了装箱和拆箱的工作,答案就是valueOf()和xxxValue()这两个方法,你会发现,不管是Integer还是Double还是Short等,都有着两个方法,其中在Integer中,xxxValue叫做intValue,其它的类似,好了,我们现在知道了装箱拆箱的源码在哪,我们再去源码一探究竟。
首先是Integer的
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在Integer中,valueOf有三个重载方法,但是最终都会转到上面参数为int的方法,代码不多,我们简单看下,首先判断拿到的i是否在某个范围内,如果满足添加条件的话,则返回一个数组对应下标的值,我们暂且先不管这个数组是啥,然后如果不满足条件的话,则直接用i来new一个新的Integer对象。
大致流程就是上面说的,接下来我们再定位到IntegerCache类里,这个是Integer类的一个内部类,我们看看里面是些啥
private static class 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;
}
private IntegerCache() {}
}
代码也不多,静下心来一行一行读,首先它声明了三个静态常量,一个low赋了初值-128,一个high没有赋值,默认为0(这里也很容易想到high就是127),一个Integer数组(取名为cache,多多少少都可以猜想到是用来干嘛的,~~~)。
接下来,是一个静态代码块,保证只会加载一次,在这部分代码里面,首先做的就是为high赋值,因为声明的时候只给low赋值了,然后通过sun.misc.VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”)获取一个值,这个值是干嘛的呢,这个其实是获取一个私有配置文件里的值,这个值可以去配置修改,在JVM初始化的时候,就会加载,得到这个值赋值给i后,再将i和127做比较,取最大的赋值给i,然后再将新得到的i值和Integer的最大值相比较,取较小者,然后赋值给h,最后再赋值给high。
简单点说,就是high的值是取自于配置文件中的一个值,但是这个值可能很小,也可能很大,毕竟是一个文件,我想配置为多少就配置为多少,然后拿到这个值后会做个限定,如果它小于127,那么就取127,如果它大于MAX_VALUE,那么就取值MAX_VALUE+low-1。
好,接着往下看,随后为数组cache开辟了地址空间,大小为high-low+1,然后再从0下标开始初始化对应的值从low开始(也就是-128)递增。至此结束。
在开始接下来的讨论前,假定配置文件的值是默认的,也就是最终high取值127,那么这个cache数组到底起到了什么作用呢?结合上面的valueOf代码片段你会发现,如果给定的基本类型int值在-128到127之间的话,就会直接去cache数组里取,如果不在这个范围的话,那么就会创新的对象,到这里你也就明白了为啥用40和500这两个基本类型声明包装类型Integer时(装箱),不同的值居然结果不一样的原因。
那么问题来了,这样做有啥好处呢?你想想这个问题,你平常使用的int类型是不是大都在-128到127之间,也就说这个范围是一个“热”范围,为了节约内存开销,在新声明的值在-128-127之间的Integer包装类型时,直接从预加载的缓存中去取,这个缓存机制上面就已经解释了,就是从一个cache数组中取,不在-128-127这个范围的才需要重新new对象,而new对象是需要内存的,如果没有这个缓存机制,那么只要是遇到需要装箱的情形,那么全部都要声明新的Integer对象,而且很多还是重复的,浪费了大量的内存,这当然不是我们想要看到的。(不禁感叹,jdk源码设计者真的是煞费苦心啊!)
至此,弄清楚了装箱的原理,下面我们再去intValue方法看看,拆箱是怎么做的,源码如下
public int intValue() {
return value;
}
嘿嘿,只有一行代码呢,真的简单,这个value值是一个内部变量,在声明Integer对象的时候用的,这里直接返回即可得到拆箱后的结果,不过你要是细心的话,你会发现还有类似的shotValue、longValue等方法,代表也可以直接拆箱成short等类型的。
同理,明白了Integer的装箱原理之后,相信你也应该猜到了Byte或者Short或者Long的实现原理,我们就不一一去看了,一共八种基本类型,其中byte,short,char,int,long这五种基本类型的装箱实现原理都和int类似,虽然它们之间有一点点区别,但是大同小异,我们就不一一去看了,然后还剩下boolean和float和double,我们先看Boolean里的
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
嗯,看着挺清爽的,比Integer的简单多了,嘿嘿,这个,我就不多解释了吧。注释这里的TRUE是大写的哦,和小写的true不一样,算了还是啰嗦一下吧,不放心啊,这里的TRUE是一个对象,声明如下
public static final Boolean TRUE = new Boolean(true);
可以看到是一个静态常量对象,保证不会有重复的对象,不消耗内存。因为Boolean是一个比较奇葩的,毕竟只有两个值,所以也不用像Integer那样去弄个缓存机制。
接下来就是Float和Double,将这两个放在一起是因为这两个是类似的,相比较于Integer,它们的区别就是,没有采用缓存机制,也就是没有设置“热”范围,仔细思考即可知道,这个要设置“热”范围的话,其实是不好处理的,因为精度问题可能还得不偿失,加上本身Float和Double在开发中相对用的比较少,所以没有设置缓存策略,我们还是象征性的看一下Double的valueOf方法意思一下吧
public static Double valueOf(double d) {
return new Double(d);
}
简单粗暴,不管你是哪个值,统统创建新的包装类型对象,Float一模一样。
好了,到现在,再回过头去看看之前的测试用例,你就明白为啥同样是40的值,Integer就是true,而Double就是false,原因就是Integer的装箱采用了缓存策略,但是Double没有作处理。
然后是最后一个测试用例的解释:为啥我在比较两个值相等的对象时,为false,但是我再后者加了一个值为0的对象时,再比较就是true了呢,真的是不明觉厉呀,于是,就又引出了一个问题,何时会发生装箱和拆箱呢?
其实这个时机是很多的,怎么解释呢,明白原理之后,其实你就知道只要是遇到需要将基本类型转换为包装类型时就需要装箱,将包装类型转换为基本类型时就需要拆箱。秉持着这个原则就不会有啥疑问了。
这时,我们再看刚才测试用例的最后一个,比较的代码是i12 == i13 + integer0,首先看等号后面,两个Integer对象相加时,肯定是没法直接相加的,所以java编译器就会进行拆箱的过程,将其拆箱为基本类型int,然后得到相加后的和,这个和仍然是基本类型int,所以在和前者比较时,也没法比较,再将前者也拆箱为基本类型int(也就是int和Integer比较时,会将Integer拆箱),然后==号比较2个int的值,发现相等,于是结果为true。
看到这里,已经对装箱和拆箱有了比较全面的了解,但是我还有个疑问,这样装箱拆箱的,对于实际的开发者来说,有啥影响呢?好像我之前不知道这些东西也没有关系,仍然在做我的工作,知道不知道对我没啥影响,那我知道这个东西干嘛?
嘿嘿,首先需要明白的一个问题就是,这样做肯定是有道理的。那么道理何在呢?看个例子
Integer sum = 0;
//int sum = 0; 一样?
for (int i = 0; i < 100; i++) {
sum += i;
}
用包装类声明的sum在循环迭代时,由于是包装类,无法直接与基本类型int类型的i相加,所以需要先拆箱,再加,再装箱,每次相加,都需要进行这个过程,那么时间效率和用int来声明相比,就不是一个档次的了,所以在开发中,要注意这些细节问题。
好了,我们回到文章开篇的那个问题,也就是SparseArray、ArrayMap的诞生原因,平常我们在Java中使用的都是HashMap等来保存数据,但是有一个严重的问题就是HashMap里的key以及value都是泛型,那么就会不可避免的遇到装箱和拆箱的过程,而这个过程是很耗时的,所以为了规避装箱拆箱提高效率,于是诞生了SparseArray等集合类,但是SparceArray效率高也是有条件的,它适用于数据量比较小的场景,而在Android开发中,大部分场景都是数据量比较小的,在数据很大的情况下,当然还是hashmap效率较优,至于具体SparseArray是怎么实现的,这个我准备单独弄一篇文章,有兴趣的可以关注下。
好了,至此文章也完结了,再说点题外话,不得不说,Java的设计是很优秀的,毕竟一个团队的心血,这不,今天就学到了一个看似普通的Integer类中的缓存设计思想,所以,小小结语:没事就看看源码,说不定你就发现了一个优秀的设计呢!!!
目前我已经工作快一年拉,因为各种新鲜的事情,博客从去年毕业就没更新了,不过最近已经蓄势待发,并准备好重新归来,哈哈哈哈哈,所以后续会继续给大家分享工作中的开发经验和心得,以及各种工作中实用的方法技巧等等等等,不管你是学生时代的骚年、还是初入职场的小白、抑或身怀奇门遁甲之术的大佬,都欢迎继续关注和交流,同时最最最最重要的,我已经开通微信公众号啦,后续这些分享将会优先在公众号放送,当然啦,也会及时更新到博客里面,欢迎关注公众号呀,另外公众号还会定期有福利放送哦,惊喜多多,还在等什么,快来关注吧!!